**Motivations :** * Create a complete simulator for 4NK Waste & Water modular waste treatment infrastructure * Implement frontend-only application with client-side data persistence * Provide seed data for wastes and natural regulators from specifications **Root causes :** * Need for a simulation tool to configure and manage waste treatment projects * Requirement for localhost-only access with persistent client-side storage * Need for initial seed data to bootstrap the application **Correctifs :** * Implemented authentication system with AuthContext * Fixed login/logout functionality with proper state management * Created placeholder pages for all routes **Evolutions :** * Complete application structure with React, TypeScript, and Vite * Seed data for 9 waste types and 52 natural regulators * Settings page with import/export and seed data loading functionality * Configuration pages for wastes and regulators with CRUD operations * Project management pages structure * Business plan and yields pages placeholders * Comprehensive UI/UX design system (dark mode only) * Navigation system with sidebar and header **Page affectées :** * All pages: Login, Dashboard, Waste Configuration, Regulators Configuration, Services Configuration * Project pages: Project List, Project Configuration, Treatment Sites, Waste Sites, Investors, Administrative Procedures * Analysis pages: Yields, Business Plan * Utility pages: Settings, Help * Components: Layout, Sidebar, Header, base components (Button, Input, Select, Card, Badge, Table) * Utils: Storage, seed data, formatters, validators, constants * Types: Complete TypeScript definitions for all entities
239 lines
7.8 KiB
TypeScript
239 lines
7.8 KiB
TypeScript
import { useState } from 'react'
|
|
import { useStorage } from '@/hooks/useStorage'
|
|
import { Waste } from '@/types'
|
|
import Card from '@/components/base/Card'
|
|
import Button from '@/components/base/Button'
|
|
import Input from '@/components/base/Input'
|
|
import Select from '@/components/base/Select'
|
|
import Table from '@/components/base/Table'
|
|
import Badge from '@/components/base/Badge'
|
|
import { formatDate } from '@/utils/formatters'
|
|
import { validateRequired, validateNumber, validatePercentage } from '@/utils/validators'
|
|
import './WasteConfigurationPage.css'
|
|
|
|
export default function WasteConfigurationPage() {
|
|
const { data, addEntity, updateEntity, deleteEntity } = useStorage()
|
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
const [formData, setFormData] = useState<Partial<Waste>>({
|
|
name: '',
|
|
originType: 'animals',
|
|
originSubType: '',
|
|
originUnitsPer1000m3Methane: 0,
|
|
bmp: 0,
|
|
waterPercentage: 75,
|
|
regulationNeeds: [],
|
|
maxStorageDuration: 30,
|
|
})
|
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
|
|
|
const wastes = data?.wastes || []
|
|
|
|
const originTypeOptions = [
|
|
{ value: 'animals', label: 'Animals' },
|
|
{ value: 'markets', label: 'Markets' },
|
|
{ value: 'restaurants', label: 'Restaurants' },
|
|
{ value: 'other', label: 'Other' },
|
|
]
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
const newErrors: Record<string, string> = {}
|
|
newErrors.name = validateRequired(formData.name)
|
|
newErrors.bmp = validateNumber(formData.bmp, 0.1, 1.0)
|
|
newErrors.waterPercentage = validatePercentage(formData.waterPercentage)
|
|
newErrors.originUnitsPer1000m3Methane = validateNumber(formData.originUnitsPer1000m3Methane, 0)
|
|
newErrors.maxStorageDuration = validateNumber(formData.maxStorageDuration, 1)
|
|
|
|
setErrors(newErrors)
|
|
|
|
if (Object.values(newErrors).some((error) => error !== undefined)) {
|
|
return
|
|
}
|
|
|
|
const waste: Waste = {
|
|
id: editingId || crypto.randomUUID(),
|
|
name: formData.name!,
|
|
originType: formData.originType!,
|
|
originSubType: formData.originSubType,
|
|
originUnitsPer1000m3Methane: formData.originUnitsPer1000m3Methane!,
|
|
bmp: formData.bmp!,
|
|
waterPercentage: formData.waterPercentage!,
|
|
regulationNeeds: formData.regulationNeeds || [],
|
|
regulatoryCharacteristics: formData.regulatoryCharacteristics,
|
|
maxStorageDuration: formData.maxStorageDuration!,
|
|
createdAt: editingId ? wastes.find((w) => w.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
}
|
|
|
|
if (editingId) {
|
|
updateEntity('wastes', editingId, waste)
|
|
} else {
|
|
addEntity('wastes', waste)
|
|
}
|
|
|
|
resetForm()
|
|
}
|
|
|
|
const resetForm = () => {
|
|
setFormData({
|
|
name: '',
|
|
originType: 'animals',
|
|
originSubType: '',
|
|
originUnitsPer1000m3Methane: 0,
|
|
bmp: 0,
|
|
waterPercentage: 75,
|
|
regulationNeeds: [],
|
|
maxStorageDuration: 30,
|
|
})
|
|
setEditingId(null)
|
|
setErrors({})
|
|
}
|
|
|
|
const handleEdit = (waste: Waste) => {
|
|
setFormData(waste)
|
|
setEditingId(waste.id)
|
|
}
|
|
|
|
const handleDelete = (id: string) => {
|
|
if (confirm('Are you sure you want to delete this waste type?')) {
|
|
deleteEntity('wastes', id)
|
|
}
|
|
}
|
|
|
|
const tableColumns = [
|
|
{
|
|
key: 'name',
|
|
header: 'Name',
|
|
render: (waste: Waste) => waste.name,
|
|
},
|
|
{
|
|
key: 'originType',
|
|
header: 'Origin Type',
|
|
render: (waste: Waste) => <Badge variant="info">{waste.originType}</Badge>,
|
|
},
|
|
{
|
|
key: 'bmp',
|
|
header: 'BMP (Nm³ CH₄/kg VS)',
|
|
render: (waste: Waste) => waste.bmp.toFixed(3),
|
|
},
|
|
{
|
|
key: 'waterPercentage',
|
|
header: 'Water %',
|
|
render: (waste: Waste) => `${waste.waterPercentage}%`,
|
|
},
|
|
{
|
|
key: 'maxStorageDuration',
|
|
header: 'Max Storage (days)',
|
|
render: (waste: Waste) => waste.maxStorageDuration,
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: 'Actions',
|
|
render: (waste: Waste) => (
|
|
<div className="table-actions">
|
|
<Button variant="secondary" onClick={() => handleEdit(waste)}>
|
|
Edit
|
|
</Button>
|
|
<Button variant="danger" onClick={() => handleDelete(waste.id)}>
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div className="waste-config-page">
|
|
<h1 className="page-title">Waste Configuration</h1>
|
|
|
|
<div className="page-content">
|
|
<Card title={editingId ? 'Edit Waste Type' : 'Add Waste Type'} className="form-card">
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="form-row">
|
|
<Input
|
|
label="Name *"
|
|
value={formData.name || ''}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
error={errors.name}
|
|
placeholder="Enter waste name"
|
|
/>
|
|
<Select
|
|
label="Origin Type *"
|
|
value={formData.originType || 'animals'}
|
|
onChange={(e) => setFormData({ ...formData, originType: e.target.value as Waste['originType'] })}
|
|
options={originTypeOptions}
|
|
/>
|
|
</div>
|
|
|
|
<Input
|
|
label="Origin Sub Type"
|
|
value={formData.originSubType || ''}
|
|
onChange={(e) => setFormData({ ...formData, originSubType: e.target.value })}
|
|
placeholder="Enter sub type (optional)"
|
|
/>
|
|
|
|
<div className="form-row">
|
|
<Input
|
|
label="BMP (Nm³ CH₄/kg VS) *"
|
|
type="number"
|
|
step="0.001"
|
|
min="0.1"
|
|
max="1.0"
|
|
value={formData.bmp || ''}
|
|
onChange={(e) => setFormData({ ...formData, bmp: parseFloat(e.target.value) || 0 })}
|
|
error={errors.bmp}
|
|
helpText="Biochemical Methane Potential (0.1 - 1.0)"
|
|
/>
|
|
<Input
|
|
label="Water Percentage (%) *"
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
value={formData.waterPercentage || ''}
|
|
onChange={(e) => setFormData({ ...formData, waterPercentage: parseFloat(e.target.value) || 0 })}
|
|
error={errors.waterPercentage}
|
|
helpText="Water content percentage (0-100%)"
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-row">
|
|
<Input
|
|
label="Origin Units per 1000m³ Methane *"
|
|
type="number"
|
|
min="0"
|
|
value={formData.originUnitsPer1000m3Methane || ''}
|
|
onChange={(e) => setFormData({ ...formData, originUnitsPer1000m3Methane: parseFloat(e.target.value) || 0 })}
|
|
error={errors.originUnitsPer1000m3Methane}
|
|
/>
|
|
<Input
|
|
label="Max Storage Duration (days) *"
|
|
type="number"
|
|
min="1"
|
|
value={formData.maxStorageDuration || ''}
|
|
onChange={(e) => setFormData({ ...formData, maxStorageDuration: parseInt(e.target.value) || 0 })}
|
|
error={errors.maxStorageDuration}
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-actions">
|
|
<Button type="submit" variant="primary">
|
|
{editingId ? 'Update' : 'Add'} Waste Type
|
|
</Button>
|
|
{editingId && (
|
|
<Button type="button" variant="secondary" onClick={resetForm}>
|
|
Cancel
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
</Card>
|
|
|
|
<Card title="Waste Types" className="table-card">
|
|
<Table columns={tableColumns} data={wastes} emptyMessage="No waste types configured" />
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|