3TE/src/pages/projects/ProjectConfigurationPage.tsx
Nicolas Cantu c7db6590f0 Initial commit: 4NK Waste & Water Simulator
**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
2025-12-09 19:09:42 +01:00

391 lines
14 KiB
TypeScript

import { useState, useEffect } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { useStorage } from '@/hooks/useStorage'
import { Project, SiteStatus, ProcedureStatus } 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 Badge from '@/components/base/Badge'
import { validateRequired, validateNumber, validateDate, validateDateRange } from '@/utils/validators'
import './ProjectConfigurationPage.css'
export default function ProjectConfigurationPage() {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const { data, addEntity, updateEntity } = useStorage()
const [formData, setFormData] = useState<Partial<Project>>({
name: '',
startDate: '',
endDate: '',
treatmentSiteId: '',
collectionSiteIds: [],
numberOfModules: 1,
transportBySite: false,
administrativeProcedures: [],
investments: [],
})
const [errors, setErrors] = useState<Record<string, string>>({})
const projects = data?.projects || []
const treatmentSites = data?.treatmentSites || []
const wasteSites = data?.wasteSites || []
const wastes = data?.wastes || []
const administrativeProcedures = data?.administrativeProcedures || []
const investors = data?.investors || []
const isEditing = !!id
const project = isEditing ? projects.find((p) => p.id === id) : null
useEffect(() => {
if (isEditing && project) {
setFormData(project)
} else {
// Set default dates (10 years from today)
const startDate = new Date().toISOString().split('T')[0]
const endDate = new Date()
endDate.setFullYear(endDate.getFullYear() + 10)
setFormData({
...formData,
startDate,
endDate: endDate.toISOString().split('T')[0],
})
}
}, [id, project])
const treatmentSiteOptions = treatmentSites.map((site) => ({
value: site.id,
label: `${site.name} (${site.status})`,
}))
const wasteSiteOptions = wasteSites.map((site) => ({
value: site.id,
label: site.name,
}))
const wasteOptions = wastes.map((waste) => ({
value: waste.id,
label: waste.name,
}))
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const newErrors: Record<string, string> = {}
newErrors.name = validateRequired(formData.name)
newErrors.startDate = validateDate(formData.startDate)
newErrors.endDate = validateDate(formData.endDate)
newErrors.treatmentSiteId = validateRequired(formData.treatmentSiteId)
newErrors.numberOfModules = validateNumber(formData.numberOfModules, 1)
newErrors.dateRange = validateDateRange(formData.startDate, formData.endDate)
setErrors(newErrors)
if (Object.values(newErrors).some((error) => error !== undefined)) {
return
}
const projectData: Project = {
id: id || crypto.randomUUID(),
name: formData.name!,
startDate: formData.startDate!,
endDate: formData.endDate!,
treatmentSiteId: formData.treatmentSiteId!,
collectionSiteIds: formData.collectionSiteIds || [],
numberOfModules: formData.numberOfModules!,
transportBySite: formData.transportBySite || false,
wasteCharacteristicsOverride: formData.wasteCharacteristicsOverride,
administrativeProcedures: formData.administrativeProcedures || [],
investments: formData.investments || [],
createdAt: isEditing && project ? project.createdAt : new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
if (isEditing) {
updateEntity('projects', id!, projectData)
} else {
addEntity('projects', projectData)
}
navigate('/projects')
}
const addAdministrativeProcedure = () => {
if (administrativeProcedures.length === 0) return
const newProcedures = [...(formData.administrativeProcedures || [])]
newProcedures.push({
procedureId: administrativeProcedures[0].id,
status: 'toDo',
})
setFormData({ ...formData, administrativeProcedures: newProcedures })
}
const removeAdministrativeProcedure = (index: number) => {
const newProcedures = [...(formData.administrativeProcedures || [])]
newProcedures.splice(index, 1)
setFormData({ ...formData, administrativeProcedures: newProcedures })
}
const updateAdministrativeProcedure = (index: number, field: 'procedureId' | 'status', value: string) => {
const newProcedures = [...(formData.administrativeProcedures || [])]
newProcedures[index] = {
...newProcedures[index],
[field]: value,
}
setFormData({ ...formData, administrativeProcedures: newProcedures })
}
const addInvestment = () => {
if (investors.length === 0) return
const newInvestments = [...(formData.investments || [])]
newInvestments.push({
investorId: investors[0].id,
status: 'toBeApproached',
amount: 0,
})
setFormData({ ...formData, investments: newInvestments })
}
const removeInvestment = (index: number) => {
const newInvestments = [...(formData.investments || [])]
newInvestments.splice(index, 1)
setFormData({ ...formData, investments: newInvestments })
}
const updateInvestment = (index: number, field: 'investorId' | 'status' | 'amount', value: string | number) => {
const newInvestments = [...(formData.investments || [])]
newInvestments[index] = {
...newInvestments[index],
[field]: value,
}
setFormData({ ...formData, investments: newInvestments })
}
return (
<div className="project-config-page">
<div className="page-header">
<h1 className="page-title">{isEditing ? 'Edit Project' : 'Create New Project'}</h1>
<Link to="/projects">
<Button variant="secondary">Back to Projects</Button>
</Link>
</div>
<form onSubmit={handleSubmit} className="project-form">
<Card title="Project Information" className="form-section">
<div className="form-row">
<Input
label="Project Name *"
value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
error={errors.name}
/>
<Input
label="Number of Modules *"
type="number"
min="1"
value={formData.numberOfModules || ''}
onChange={(e) => setFormData({ ...formData, numberOfModules: parseInt(e.target.value) || 1 })}
error={errors.numberOfModules}
/>
</div>
<div className="form-row">
<Input
label="Start Date *"
type="date"
value={formData.startDate || ''}
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
error={errors.startDate}
/>
<Input
label="End Date *"
type="date"
value={formData.endDate || ''}
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
error={errors.endDate || errors.dateRange}
/>
</div>
</Card>
<Card title="Sites" className="form-section">
<Select
label="Treatment Site *"
value={formData.treatmentSiteId || ''}
onChange={(e) => setFormData({ ...formData, treatmentSiteId: e.target.value })}
options={treatmentSiteOptions.length > 0 ? treatmentSiteOptions : [{ value: '', label: 'No treatment sites available' }]}
error={errors.treatmentSiteId}
helpText="Create a treatment site first if none available"
/>
<div className="multi-select-section">
<label className="input-label">Collection Sites *</label>
<div className="checkbox-list">
{wasteSiteOptions.map((option) => (
<label key={option.value} className="checkbox-item">
<input
type="checkbox"
checked={formData.collectionSiteIds?.includes(option.value) || false}
onChange={(e) => {
const currentIds = formData.collectionSiteIds || []
if (e.target.checked) {
setFormData({ ...formData, collectionSiteIds: [...currentIds, option.value] })
} else {
setFormData({ ...formData, collectionSiteIds: currentIds.filter((id) => id !== option.value) })
}
}}
/>
<span>{option.label}</span>
</label>
))}
</div>
{wasteSiteOptions.length === 0 && (
<p className="help-text">No waste sites available. Create waste sites first.</p>
)}
</div>
<div className="checkbox-item">
<input
type="checkbox"
checked={formData.transportBySite || false}
onChange={(e) => setFormData({ ...formData, transportBySite: e.target.checked })}
/>
<span>Transport by site</span>
</div>
</Card>
<Card title="Waste Configuration Override" className="form-section">
<Select
label="Waste Type (Override)"
value={formData.wasteCharacteristicsOverride?.wasteId || ''}
onChange={(e) =>
setFormData({
...formData,
wasteCharacteristicsOverride: {
...formData.wasteCharacteristicsOverride,
wasteId: e.target.value || undefined,
},
})
}
options={[{ value: '', label: 'Use default waste characteristics' }, ...wasteOptions]}
/>
<div className="form-row">
<Input
label="BMP Override (Nm³ CH₄/kg VS)"
type="number"
step="0.001"
min="0.1"
max="1.0"
value={formData.wasteCharacteristicsOverride?.bmp || ''}
onChange={(e) =>
setFormData({
...formData,
wasteCharacteristicsOverride: {
...formData.wasteCharacteristicsOverride,
bmp: e.target.value ? parseFloat(e.target.value) : undefined,
},
})
}
/>
<Input
label="Water Percentage Override (%)"
type="number"
min="0"
max="100"
value={formData.wasteCharacteristicsOverride?.waterPercentage || ''}
onChange={(e) =>
setFormData({
...formData,
wasteCharacteristicsOverride: {
...formData.wasteCharacteristicsOverride,
waterPercentage: e.target.value ? parseFloat(e.target.value) : undefined,
},
})
}
/>
</div>
</Card>
<Card title="Administrative Procedures" className="form-section">
{formData.administrativeProcedures?.map((proc, index) => (
<div key={index} className="procedure-item">
<Select
label={`Procedure ${index + 1}`}
value={proc.procedureId}
onChange={(e) => updateAdministrativeProcedure(index, 'procedureId', e.target.value)}
options={administrativeProcedures.map((p) => ({ value: p.id, label: p.name }))}
/>
<Select
label="Status"
value={proc.status}
onChange={(e) => updateAdministrativeProcedure(index, 'status', e.target.value as ProcedureStatus)}
options={[
{ value: 'toDo', label: 'To Do' },
{ value: 'done', label: 'Done' },
{ value: 'na', label: 'N/A' },
]}
/>
<Button type="button" variant="danger" onClick={() => removeAdministrativeProcedure(index)}>
Remove
</Button>
</div>
))}
<Button type="button" variant="secondary" onClick={addAdministrativeProcedure} disabled={administrativeProcedures.length === 0}>
Add Procedure
</Button>
</Card>
<Card title="Investments" className="form-section">
{formData.investments?.map((inv, index) => (
<div key={index} className="investment-item">
<Select
label={`Investor ${index + 1}`}
value={inv.investorId}
onChange={(e) => updateInvestment(index, 'investorId', e.target.value)}
options={investors.map((i) => ({ value: i.id, label: i.name }))}
/>
<Select
label="Status"
value={inv.status}
onChange={(e) => updateInvestment(index, 'status', e.target.value as SiteStatus)}
options={[
{ value: 'toBeApproached', label: 'To be approached' },
{ value: 'loiOk', label: 'LOI OK' },
{ value: 'inProgress', label: 'In progress' },
{ value: 'completed', label: 'Completed' },
]}
/>
<Input
label="Amount (€)"
type="number"
min="0"
value={inv.amount || ''}
onChange={(e) => updateInvestment(index, 'amount', parseFloat(e.target.value) || 0)}
/>
<Button type="button" variant="danger" onClick={() => removeInvestment(index)}>
Remove
</Button>
</div>
))}
<Button type="button" variant="secondary" onClick={addInvestment} disabled={investors.length === 0}>
Add Investment
</Button>
</Card>
<div className="form-actions">
<Button type="submit" variant="primary">
{isEditing ? 'Update' : 'Create'} Project
</Button>
<Link to="/projects">
<Button type="button" variant="secondary">
Cancel
</Button>
</Link>
</div>
</form>
</div>
)
}