Add transporters to sites and services to module components
**Motivations :** * Allow associating transporters to waste sites and treatment sites * Allow associating services to module components * Improve data relationships for better project management **Evolutions :** * Added SiteTransporterAssociation interface for site-transporter relationships * Added ModuleComponentServiceAssociation interface for component-service relationships * Added transporters field to TreatmentSite and WasteSite interfaces * Added services field to ModuleComponent interface * Updated WasteSitesPage and TreatmentSitesPage to manage transporters with primary flag and notes * Updated ModuleComponentsConfigurationPage to manage services with notes * Added CSS styles for transporter and service association sections * Added transporters and services columns in respective tables **Pages affectées :** * src/pages/projects/WasteSitesPage.tsx * src/pages/projects/TreatmentSitesPage.tsx * src/pages/configuration/ModuleComponentsConfigurationPage.tsx
This commit is contained in:
parent
0236c927e7
commit
7b8d2b1abb
@ -134,3 +134,58 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.regulators-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.regulator-association-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr 1fr auto;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regulators-summary {
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regulators-summary .error-text {
|
||||||
|
display: block;
|
||||||
|
color: var(--error);
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help-text {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-association-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 2fr auto;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useStorage } from '@/hooks/useStorage'
|
import { useStorage } from '@/hooks/useStorage'
|
||||||
import { ModuleComponent, ModuleComponentType, MonthlyValue, Dimensions, PowerSpecs, ProductionPowerSpecs } from '@/types'
|
import { ModuleComponent, ModuleComponentType, MonthlyValue, Dimensions, PowerSpecs, ProductionPowerSpecs, ModuleComponentRegulatorAssociation, ModuleComponentServiceAssociation } from '@/types'
|
||||||
import Card from '@/components/base/Card'
|
import Card from '@/components/base/Card'
|
||||||
import Button from '@/components/base/Button'
|
import Button from '@/components/base/Button'
|
||||||
import Input from '@/components/base/Input'
|
import Input from '@/components/base/Input'
|
||||||
@ -54,11 +54,15 @@ export default function ModuleComponentsConfigurationPage() {
|
|||||||
isOptional: false,
|
isOptional: false,
|
||||||
defaultDuration: 21,
|
defaultDuration: 21,
|
||||||
durationUnit: 'days',
|
durationUnit: 'days',
|
||||||
|
regulators: [],
|
||||||
|
services: [],
|
||||||
})
|
})
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
const components = data?.moduleComponents || []
|
const components = data?.moduleComponents || []
|
||||||
const wastes = data?.wastes || []
|
const wastes = data?.wastes || []
|
||||||
|
const regulators = data?.regulators || []
|
||||||
|
const services = data?.services || []
|
||||||
|
|
||||||
const selectedComponentType = COMPONENT_TYPES.find(t => t.value === formData.type)
|
const selectedComponentType = COMPONENT_TYPES.find(t => t.value === formData.type)
|
||||||
|
|
||||||
@ -84,6 +88,12 @@ export default function ModuleComponentsConfigurationPage() {
|
|||||||
newErrors.type = validateRequired(formData.type)
|
newErrors.type = validateRequired(formData.type)
|
||||||
newErrors.defaultDuration = validateNumber(formData.defaultDuration, 0)
|
newErrors.defaultDuration = validateNumber(formData.defaultDuration, 0)
|
||||||
|
|
||||||
|
// Validate total percentage of associated regulators
|
||||||
|
const totalRegulatorPercentage = (formData.regulators || []).reduce((sum, a) => sum + (a.percentage || 0), 0)
|
||||||
|
if (totalRegulatorPercentage > 100) {
|
||||||
|
newErrors.regulators = 'Total regulator percentage cannot exceed 100%'
|
||||||
|
}
|
||||||
|
|
||||||
setErrors(newErrors)
|
setErrors(newErrors)
|
||||||
|
|
||||||
if (Object.values(newErrors).some((error) => error !== undefined)) {
|
if (Object.values(newErrors).some((error) => error !== undefined)) {
|
||||||
@ -122,6 +132,8 @@ export default function ModuleComponentsConfigurationPage() {
|
|||||||
annualExploitationConsumablesCost: formData.annualExploitationConsumablesCost,
|
annualExploitationConsumablesCost: formData.annualExploitationConsumablesCost,
|
||||||
annualMaintenanceConsumablesCost: formData.annualMaintenanceConsumablesCost,
|
annualMaintenanceConsumablesCost: formData.annualMaintenanceConsumablesCost,
|
||||||
lifetime: formData.lifetime,
|
lifetime: formData.lifetime,
|
||||||
|
regulators: formData.regulators || [],
|
||||||
|
services: formData.services || [],
|
||||||
createdAt: editingId ? components.find((c) => c.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
|
createdAt: editingId ? components.find((c) => c.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
@ -142,6 +154,7 @@ export default function ModuleComponentsConfigurationPage() {
|
|||||||
isOptional: false,
|
isOptional: false,
|
||||||
defaultDuration: 21,
|
defaultDuration: 21,
|
||||||
durationUnit: 'days',
|
durationUnit: 'days',
|
||||||
|
regulators: [],
|
||||||
})
|
})
|
||||||
setEditingId(null)
|
setEditingId(null)
|
||||||
setErrors({})
|
setErrors({})
|
||||||
@ -209,6 +222,48 @@ export default function ModuleComponentsConfigurationPage() {
|
|||||||
header: 'Duration',
|
header: 'Duration',
|
||||||
render: (component: ModuleComponent) => `${component.defaultDuration} ${component.durationUnit}`,
|
render: (component: ModuleComponent) => `${component.defaultDuration} ${component.durationUnit}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'regulators',
|
||||||
|
header: 'Regulators',
|
||||||
|
render: (component: ModuleComponent) => {
|
||||||
|
if (!component.regulators || component.regulators.length === 0) {
|
||||||
|
return <span style={{ color: 'var(--text-secondary)' }}>None</span>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{component.regulators.map((assoc, idx) => {
|
||||||
|
const regulator = regulators.find((r) => r.id === assoc.regulatorId)
|
||||||
|
return (
|
||||||
|
<Badge key={idx} variant="info" style={{ marginRight: '4px' }}>
|
||||||
|
{regulator?.name || 'Unknown'} ({assoc.percentage}%)
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'services',
|
||||||
|
header: 'Services',
|
||||||
|
render: (component: ModuleComponent) => {
|
||||||
|
if (!component.services || component.services.length === 0) {
|
||||||
|
return <span style={{ color: 'var(--text-secondary)' }}>None</span>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{component.services.map((assoc, idx) => {
|
||||||
|
const service = services.find((s) => s.id === assoc.serviceId)
|
||||||
|
return (
|
||||||
|
<Badge key={idx} variant="success" style={{ marginRight: '4px' }}>
|
||||||
|
{service?.name || 'Unknown'}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
@ -635,6 +690,191 @@ export default function ModuleComponentsConfigurationPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Associated Regulators Section */}
|
||||||
|
<div className="form-section">
|
||||||
|
<h3 className="form-section-title">Associated Regulators</h3>
|
||||||
|
<p className="form-help-text">
|
||||||
|
Add regulators to this module component. Specify the percentage of total volume for each regulator.
|
||||||
|
</p>
|
||||||
|
<div className="regulators-list">
|
||||||
|
{(formData.regulators || []).map((association, index) => {
|
||||||
|
const regulator = regulators.find((r) => r.id === association.regulatorId)
|
||||||
|
return (
|
||||||
|
<div key={index} className="regulator-association-item">
|
||||||
|
<Select
|
||||||
|
label="Regulator"
|
||||||
|
value={association.regulatorId || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedRegulators = [...(formData.regulators || [])]
|
||||||
|
updatedRegulators[index] = {
|
||||||
|
...updatedRegulators[index],
|
||||||
|
regulatorId: e.target.value,
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, regulators: updatedRegulators })
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'Select a regulator' },
|
||||||
|
...regulators.map((r) => ({
|
||||||
|
value: r.id,
|
||||||
|
label: r.name,
|
||||||
|
})).filter(
|
||||||
|
(opt) =>
|
||||||
|
opt.value === association.regulatorId ||
|
||||||
|
!(formData.regulators || []).some((a) => a.regulatorId === opt.value)
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Volume Percentage (%)"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="0.1"
|
||||||
|
value={association.percentage || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedRegulators = [...(formData.regulators || [])]
|
||||||
|
updatedRegulators[index] = {
|
||||||
|
...updatedRegulators[index],
|
||||||
|
percentage: parseFloat(e.target.value) || 0,
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, regulators: updatedRegulators })
|
||||||
|
}}
|
||||||
|
helpText={regulator ? `Regulator: ${regulator.name}` : undefined}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Dosage (optional)"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
value={association.dosage || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedRegulators = [...(formData.regulators || [])]
|
||||||
|
updatedRegulators[index] = {
|
||||||
|
...updatedRegulators[index],
|
||||||
|
dosage: e.target.value ? parseFloat(e.target.value) : undefined,
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, regulators: updatedRegulators })
|
||||||
|
}}
|
||||||
|
helpText="Specific dosage (kg/t, L/t, or %)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => {
|
||||||
|
const updatedRegulators = (formData.regulators || []).filter((_, i) => i !== index)
|
||||||
|
setFormData({ ...formData, regulators: updatedRegulators })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const newAssociation: ModuleComponentRegulatorAssociation = {
|
||||||
|
regulatorId: '',
|
||||||
|
percentage: 0,
|
||||||
|
}
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
regulators: [...(formData.regulators || []), newAssociation],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Regulator
|
||||||
|
</Button>
|
||||||
|
{(formData.regulators || []).length > 0 && (
|
||||||
|
<div className="regulators-summary">
|
||||||
|
<strong>
|
||||||
|
Total: {(formData.regulators || []).reduce((sum, a) => sum + (a.percentage || 0), 0).toFixed(1)}%
|
||||||
|
</strong>
|
||||||
|
{errors.regulators && <span className="error-text">{errors.regulators}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Associated Services Section */}
|
||||||
|
<div className="form-section">
|
||||||
|
<h3 className="form-section-title">Associated Services</h3>
|
||||||
|
<p className="form-help-text">
|
||||||
|
Add services to this module component. Services can be used for business plan calculations.
|
||||||
|
</p>
|
||||||
|
<div className="services-list">
|
||||||
|
{(formData.services || []).map((association, index) => {
|
||||||
|
const service = services.find((s) => s.id === association.serviceId)
|
||||||
|
return (
|
||||||
|
<div key={index} className="service-association-item">
|
||||||
|
<Select
|
||||||
|
label="Service"
|
||||||
|
value={association.serviceId || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedServices = [...(formData.services || [])]
|
||||||
|
updatedServices[index] = {
|
||||||
|
...updatedServices[index],
|
||||||
|
serviceId: e.target.value,
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, services: updatedServices })
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'Select a service' },
|
||||||
|
...services.map((s) => ({
|
||||||
|
value: s.id,
|
||||||
|
label: `${s.name} (${s.type})`,
|
||||||
|
})).filter(
|
||||||
|
(opt) =>
|
||||||
|
opt.value === association.serviceId ||
|
||||||
|
!(formData.services || []).some((a) => a.serviceId === opt.value)
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Notes (optional)"
|
||||||
|
value={association.notes || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedServices = [...(formData.services || [])]
|
||||||
|
updatedServices[index] = {
|
||||||
|
...updatedServices[index],
|
||||||
|
notes: e.target.value || undefined,
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, services: updatedServices })
|
||||||
|
}}
|
||||||
|
helpText={service ? `Type: ${service.type}` : undefined}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => {
|
||||||
|
const updatedServices = (formData.services || []).filter((_, i) => i !== index)
|
||||||
|
setFormData({ ...formData, services: updatedServices })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const newAssociation: ModuleComponentServiceAssociation = {
|
||||||
|
serviceId: '',
|
||||||
|
}
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
services: [...(formData.services || []), newAssociation],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Service
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Costs Section */}
|
{/* Costs Section */}
|
||||||
<div className="form-section">
|
<div className="form-section">
|
||||||
<h3 className="form-section-title">Costs (Annual)</h3>
|
<h3 className="form-section-title">Costs (Annual)</h3>
|
||||||
|
|||||||
@ -54,3 +54,59 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help-text {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transporters-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transporter-association-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr auto 2fr auto;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: var(--primary-green);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useStorage } from '@/hooks/useStorage'
|
import { useStorage } from '@/hooks/useStorage'
|
||||||
import { TreatmentSite, SiteStatus } from '@/types'
|
import { TreatmentSite, SiteStatus, SiteTransporterAssociation } from '@/types'
|
||||||
import Card from '@/components/base/Card'
|
import Card from '@/components/base/Card'
|
||||||
import Button from '@/components/base/Button'
|
import Button from '@/components/base/Button'
|
||||||
import Input from '@/components/base/Input'
|
import Input from '@/components/base/Input'
|
||||||
@ -20,11 +20,13 @@ export default function TreatmentSitesPage() {
|
|||||||
availableGroundSurface: 0,
|
availableGroundSurface: 0,
|
||||||
monthlyTemperatures: Array(12).fill(0),
|
monthlyTemperatures: Array(12).fill(0),
|
||||||
subscribedServices: [],
|
subscribedServices: [],
|
||||||
|
transporters: [],
|
||||||
})
|
})
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
const sites = data?.treatmentSites || []
|
const sites = data?.treatmentSites || []
|
||||||
const services = data?.services || []
|
const services = data?.services || []
|
||||||
|
const transporters = data?.transporters || []
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ value: 'toBeApproached', label: 'To be approached' },
|
{ value: 'toBeApproached', label: 'To be approached' },
|
||||||
@ -61,6 +63,7 @@ export default function TreatmentSitesPage() {
|
|||||||
availableGroundSurface: formData.availableGroundSurface!,
|
availableGroundSurface: formData.availableGroundSurface!,
|
||||||
monthlyTemperatures: formData.monthlyTemperatures!,
|
monthlyTemperatures: formData.monthlyTemperatures!,
|
||||||
subscribedServices: formData.subscribedServices || [],
|
subscribedServices: formData.subscribedServices || [],
|
||||||
|
transporters: formData.transporters || [],
|
||||||
createdAt: editingId ? sites.find((s) => s.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
|
createdAt: editingId ? sites.find((s) => s.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
@ -82,6 +85,7 @@ export default function TreatmentSitesPage() {
|
|||||||
availableGroundSurface: 0,
|
availableGroundSurface: 0,
|
||||||
monthlyTemperatures: Array(12).fill(0),
|
monthlyTemperatures: Array(12).fill(0),
|
||||||
subscribedServices: [],
|
subscribedServices: [],
|
||||||
|
transporters: [],
|
||||||
})
|
})
|
||||||
setEditingId(null)
|
setEditingId(null)
|
||||||
setErrors({})
|
setErrors({})
|
||||||
@ -130,6 +134,27 @@ export default function TreatmentSitesPage() {
|
|||||||
header: 'Services',
|
header: 'Services',
|
||||||
render: (site: TreatmentSite) => site.subscribedServices.length,
|
render: (site: TreatmentSite) => site.subscribedServices.length,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'transporters',
|
||||||
|
header: 'Transporters',
|
||||||
|
render: (site: TreatmentSite) => {
|
||||||
|
if (!site.transporters || site.transporters.length === 0) {
|
||||||
|
return <span style={{ color: 'var(--text-secondary)' }}>None</span>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{site.transporters.map((assoc, idx) => {
|
||||||
|
const transporter = transporters.find((t) => t.id === assoc.transporterId)
|
||||||
|
return (
|
||||||
|
<Badge key={idx} variant={assoc.isPrimary ? 'success' : 'info'} style={{ marginRight: '4px' }}>
|
||||||
|
{transporter?.name || 'Unknown'} {assoc.isPrimary && '(Primary)'}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
@ -211,6 +236,110 @@ export default function TreatmentSitesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Associated Transporters Section */}
|
||||||
|
<div className="form-section">
|
||||||
|
<h3 className="form-section-title">Associated Transporters</h3>
|
||||||
|
<p className="form-help-text">
|
||||||
|
Add transporters for this treatment site. Mark one as primary if needed.
|
||||||
|
</p>
|
||||||
|
<div className="transporters-list">
|
||||||
|
{(formData.transporters || []).map((association, index) => {
|
||||||
|
const transporter = transporters.find((t) => t.id === association.transporterId)
|
||||||
|
return (
|
||||||
|
<div key={index} className="transporter-association-item">
|
||||||
|
<Select
|
||||||
|
label="Transporter"
|
||||||
|
value={association.transporterId || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedTransporters = [...(formData.transporters || [])]
|
||||||
|
updatedTransporters[index] = {
|
||||||
|
...updatedTransporters[index],
|
||||||
|
transporterId: e.target.value,
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, transporters: updatedTransporters })
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'Select a transporter' },
|
||||||
|
...transporters.map((t) => ({
|
||||||
|
value: t.id,
|
||||||
|
label: `${t.name} (${t.type})`,
|
||||||
|
})).filter(
|
||||||
|
(opt) =>
|
||||||
|
opt.value === association.transporterId ||
|
||||||
|
!(formData.transporters || []).some((a) => a.transporterId === opt.value)
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="form-checkbox">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={association.isPrimary || false}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedTransporters = [...(formData.transporters || [])]
|
||||||
|
updatedTransporters[index] = {
|
||||||
|
...updatedTransporters[index],
|
||||||
|
isPrimary: e.target.checked,
|
||||||
|
}
|
||||||
|
// If this is set as primary, unset others
|
||||||
|
if (e.target.checked) {
|
||||||
|
updatedTransporters.forEach((t, i) => {
|
||||||
|
if (i !== index) {
|
||||||
|
t.isPrimary = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, transporters: updatedTransporters })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>Primary</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="Notes (optional)"
|
||||||
|
value={association.notes || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedTransporters = [...(formData.transporters || [])]
|
||||||
|
updatedTransporters[index] = {
|
||||||
|
...updatedTransporters[index],
|
||||||
|
notes: e.target.value || undefined,
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, transporters: updatedTransporters })
|
||||||
|
}}
|
||||||
|
helpText={transporter ? `Type: ${transporter.type}, Capacity: ${transporter.capacity}` : undefined}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => {
|
||||||
|
const updatedTransporters = (formData.transporters || []).filter((_, i) => i !== index)
|
||||||
|
setFormData({ ...formData, transporters: updatedTransporters })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const newAssociation: SiteTransporterAssociation = {
|
||||||
|
transporterId: '',
|
||||||
|
isPrimary: false,
|
||||||
|
}
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
transporters: [...(formData.transporters || []), newAssociation],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Transporter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
<Button type="submit" variant="primary">
|
<Button type="submit" variant="primary">
|
||||||
{editingId ? 'Update' : 'Add'} Treatment Site
|
{editingId ? 'Update' : 'Add'} Treatment Site
|
||||||
|
|||||||
@ -33,3 +33,59 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help-text {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transporters-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transporter-association-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr auto 2fr auto;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: var(--primary-green);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useStorage } from '@/hooks/useStorage'
|
import { useStorage } from '@/hooks/useStorage'
|
||||||
import { WasteSite, SiteStatus } from '@/types'
|
import { WasteSite, SiteStatus, SiteTransporterAssociation } from '@/types'
|
||||||
import Card from '@/components/base/Card'
|
import Card from '@/components/base/Card'
|
||||||
import Button from '@/components/base/Button'
|
import Button from '@/components/base/Button'
|
||||||
import Input from '@/components/base/Input'
|
import Input from '@/components/base/Input'
|
||||||
@ -27,11 +27,13 @@ export default function WasteSitesPage() {
|
|||||||
},
|
},
|
||||||
collectionType: '',
|
collectionType: '',
|
||||||
distance: 0,
|
distance: 0,
|
||||||
|
transporters: [],
|
||||||
})
|
})
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
const sites = data?.wasteSites || []
|
const sites = data?.wasteSites || []
|
||||||
const wastes = data?.wastes || []
|
const wastes = data?.wastes || []
|
||||||
|
const transporters = data?.transporters || []
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ value: 'toBeApproached', label: 'To be approached' },
|
{ value: 'toBeApproached', label: 'To be approached' },
|
||||||
@ -74,6 +76,7 @@ export default function WasteSitesPage() {
|
|||||||
contact: formData.contact!,
|
contact: formData.contact!,
|
||||||
collectionType: formData.collectionType!,
|
collectionType: formData.collectionType!,
|
||||||
distance: formData.distance!,
|
distance: formData.distance!,
|
||||||
|
transporters: formData.transporters || [],
|
||||||
createdAt: editingId ? sites.find((s) => s.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
|
createdAt: editingId ? sites.find((s) => s.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
@ -102,6 +105,7 @@ export default function WasteSitesPage() {
|
|||||||
},
|
},
|
||||||
collectionType: '',
|
collectionType: '',
|
||||||
distance: 0,
|
distance: 0,
|
||||||
|
transporters: [],
|
||||||
})
|
})
|
||||||
setEditingId(null)
|
setEditingId(null)
|
||||||
setErrors({})
|
setErrors({})
|
||||||
@ -162,6 +166,27 @@ export default function WasteSitesPage() {
|
|||||||
header: 'Distance (km)',
|
header: 'Distance (km)',
|
||||||
render: (site: WasteSite) => site.distance,
|
render: (site: WasteSite) => site.distance,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'transporters',
|
||||||
|
header: 'Transporters',
|
||||||
|
render: (site: WasteSite) => {
|
||||||
|
if (!site.transporters || site.transporters.length === 0) {
|
||||||
|
return <span style={{ color: 'var(--text-secondary)' }}>None</span>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{site.transporters.map((assoc, idx) => {
|
||||||
|
const transporter = transporters.find((t) => t.id === assoc.transporterId)
|
||||||
|
return (
|
||||||
|
<Badge key={idx} variant={assoc.isPrimary ? 'success' : 'info'} style={{ marginRight: '4px' }}>
|
||||||
|
{transporter?.name || 'Unknown'} {assoc.isPrimary && '(Primary)'}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
@ -283,6 +308,110 @@ export default function WasteSitesPage() {
|
|||||||
error={errors.distance}
|
error={errors.distance}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Associated Transporters Section */}
|
||||||
|
<div className="form-section">
|
||||||
|
<h3 className="form-section-title">Associated Transporters</h3>
|
||||||
|
<p className="form-help-text">
|
||||||
|
Add transporters for this waste site. Mark one as primary if needed.
|
||||||
|
</p>
|
||||||
|
<div className="transporters-list">
|
||||||
|
{(formData.transporters || []).map((association, index) => {
|
||||||
|
const transporter = transporters.find((t) => t.id === association.transporterId)
|
||||||
|
return (
|
||||||
|
<div key={index} className="transporter-association-item">
|
||||||
|
<Select
|
||||||
|
label="Transporter"
|
||||||
|
value={association.transporterId || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedTransporters = [...(formData.transporters || [])]
|
||||||
|
updatedTransporters[index] = {
|
||||||
|
...updatedTransporters[index],
|
||||||
|
transporterId: e.target.value,
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, transporters: updatedTransporters })
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'Select a transporter' },
|
||||||
|
...transporters.map((t) => ({
|
||||||
|
value: t.id,
|
||||||
|
label: `${t.name} (${t.type})`,
|
||||||
|
})).filter(
|
||||||
|
(opt) =>
|
||||||
|
opt.value === association.transporterId ||
|
||||||
|
!(formData.transporters || []).some((a) => a.transporterId === opt.value)
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="form-checkbox">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={association.isPrimary || false}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedTransporters = [...(formData.transporters || [])]
|
||||||
|
updatedTransporters[index] = {
|
||||||
|
...updatedTransporters[index],
|
||||||
|
isPrimary: e.target.checked,
|
||||||
|
}
|
||||||
|
// If this is set as primary, unset others
|
||||||
|
if (e.target.checked) {
|
||||||
|
updatedTransporters.forEach((t, i) => {
|
||||||
|
if (i !== index) {
|
||||||
|
t.isPrimary = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, transporters: updatedTransporters })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>Primary</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="Notes (optional)"
|
||||||
|
value={association.notes || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updatedTransporters = [...(formData.transporters || [])]
|
||||||
|
updatedTransporters[index] = {
|
||||||
|
...updatedTransporters[index],
|
||||||
|
notes: e.target.value || undefined,
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, transporters: updatedTransporters })
|
||||||
|
}}
|
||||||
|
helpText={transporter ? `Type: ${transporter.type}, Capacity: ${transporter.capacity}` : undefined}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => {
|
||||||
|
const updatedTransporters = (formData.transporters || []).filter((_, i) => i !== index)
|
||||||
|
setFormData({ ...formData, transporters: updatedTransporters })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const newAssociation: SiteTransporterAssociation = {
|
||||||
|
transporterId: '',
|
||||||
|
isPrimary: false,
|
||||||
|
}
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
transporters: [...(formData.transporters || []), newAssociation],
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Transporter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
<Button type="submit" variant="primary">
|
<Button type="submit" variant="primary">
|
||||||
{editingId ? 'Update' : 'Add'} Waste Site
|
{editingId ? 'Update' : 'Add'} Waste Site
|
||||||
|
|||||||
@ -39,6 +39,26 @@ export interface WasteRegulatorAssociation {
|
|||||||
percentage: number // Percentage of total volume (0-100)
|
percentage: number // Percentage of total volume (0-100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module Component Regulator Association
|
||||||
|
export interface ModuleComponentRegulatorAssociation {
|
||||||
|
regulatorId: string // Reference to NaturalRegulator
|
||||||
|
percentage: number // Percentage of total volume (0-100)
|
||||||
|
dosage?: number // Optional specific dosage (kg/t, L/t, or %)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Site Transporter Association
|
||||||
|
export interface SiteTransporterAssociation {
|
||||||
|
transporterId: string // Reference to Transporter
|
||||||
|
isPrimary?: boolean // Whether this is the primary transporter for the site
|
||||||
|
notes?: string // Optional notes about this transporter association
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module Component Service Association
|
||||||
|
export interface ModuleComponentServiceAssociation {
|
||||||
|
serviceId: string // Reference to Service
|
||||||
|
notes?: string // Optional notes about this service association
|
||||||
|
}
|
||||||
|
|
||||||
// Waste
|
// Waste
|
||||||
export interface Waste extends BaseEntity {
|
export interface Waste extends BaseEntity {
|
||||||
name: string
|
name: string
|
||||||
@ -196,6 +216,7 @@ export interface TreatmentSite extends BaseEntity {
|
|||||||
availableGroundSurface: number // m²
|
availableGroundSurface: number // m²
|
||||||
monthlyTemperatures: number[] // 12 values, °C
|
monthlyTemperatures: number[] // 12 values, °C
|
||||||
subscribedServices: string[] // service IDs
|
subscribedServices: string[] // service IDs
|
||||||
|
transporters?: SiteTransporterAssociation[] // Associated transporters
|
||||||
}
|
}
|
||||||
|
|
||||||
// Waste Site
|
// Waste Site
|
||||||
@ -216,6 +237,7 @@ export interface WasteSite extends BaseEntity {
|
|||||||
}
|
}
|
||||||
collectionType: string
|
collectionType: string
|
||||||
distance: number // km
|
distance: number // km
|
||||||
|
transporters?: SiteTransporterAssociation[] // Associated transporters
|
||||||
}
|
}
|
||||||
|
|
||||||
// Investor
|
// Investor
|
||||||
@ -445,6 +467,12 @@ export interface ModuleComponent extends BaseEntity {
|
|||||||
|
|
||||||
// Lifetime
|
// Lifetime
|
||||||
lifetime?: number // years (Durée de vie)
|
lifetime?: number // years (Durée de vie)
|
||||||
|
|
||||||
|
// Associated regulators
|
||||||
|
regulators?: ModuleComponentRegulatorAssociation[] // Associated regulators with volume percentages
|
||||||
|
|
||||||
|
// Associated services
|
||||||
|
services?: ModuleComponentServiceAssociation[] // Associated services
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ecosystem Regulator Association
|
// Ecosystem Regulator Association
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user