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;
|
||||
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 { 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 Button from '@/components/base/Button'
|
||||
import Input from '@/components/base/Input'
|
||||
@ -54,11 +54,15 @@ export default function ModuleComponentsConfigurationPage() {
|
||||
isOptional: false,
|
||||
defaultDuration: 21,
|
||||
durationUnit: 'days',
|
||||
regulators: [],
|
||||
services: [],
|
||||
})
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
|
||||
const components = data?.moduleComponents || []
|
||||
const wastes = data?.wastes || []
|
||||
const regulators = data?.regulators || []
|
||||
const services = data?.services || []
|
||||
|
||||
const selectedComponentType = COMPONENT_TYPES.find(t => t.value === formData.type)
|
||||
|
||||
@ -84,6 +88,12 @@ export default function ModuleComponentsConfigurationPage() {
|
||||
newErrors.type = validateRequired(formData.type)
|
||||
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)
|
||||
|
||||
if (Object.values(newErrors).some((error) => error !== undefined)) {
|
||||
@ -122,6 +132,8 @@ export default function ModuleComponentsConfigurationPage() {
|
||||
annualExploitationConsumablesCost: formData.annualExploitationConsumablesCost,
|
||||
annualMaintenanceConsumablesCost: formData.annualMaintenanceConsumablesCost,
|
||||
lifetime: formData.lifetime,
|
||||
regulators: formData.regulators || [],
|
||||
services: formData.services || [],
|
||||
createdAt: editingId ? components.find((c) => c.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
@ -142,6 +154,7 @@ export default function ModuleComponentsConfigurationPage() {
|
||||
isOptional: false,
|
||||
defaultDuration: 21,
|
||||
durationUnit: 'days',
|
||||
regulators: [],
|
||||
})
|
||||
setEditingId(null)
|
||||
setErrors({})
|
||||
@ -209,6 +222,48 @@ export default function ModuleComponentsConfigurationPage() {
|
||||
header: 'Duration',
|
||||
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',
|
||||
header: 'Actions',
|
||||
@ -635,6 +690,191 @@ export default function ModuleComponentsConfigurationPage() {
|
||||
</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 */}
|
||||
<div className="form-section">
|
||||
<h3 className="form-section-title">Costs (Annual)</h3>
|
||||
|
||||
@ -54,3 +54,59 @@
|
||||
display: flex;
|
||||
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 { useStorage } from '@/hooks/useStorage'
|
||||
import { TreatmentSite, SiteStatus } from '@/types'
|
||||
import { TreatmentSite, SiteStatus, SiteTransporterAssociation } from '@/types'
|
||||
import Card from '@/components/base/Card'
|
||||
import Button from '@/components/base/Button'
|
||||
import Input from '@/components/base/Input'
|
||||
@ -20,11 +20,13 @@ export default function TreatmentSitesPage() {
|
||||
availableGroundSurface: 0,
|
||||
monthlyTemperatures: Array(12).fill(0),
|
||||
subscribedServices: [],
|
||||
transporters: [],
|
||||
})
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
|
||||
const sites = data?.treatmentSites || []
|
||||
const services = data?.services || []
|
||||
const transporters = data?.transporters || []
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'toBeApproached', label: 'To be approached' },
|
||||
@ -61,6 +63,7 @@ export default function TreatmentSitesPage() {
|
||||
availableGroundSurface: formData.availableGroundSurface!,
|
||||
monthlyTemperatures: formData.monthlyTemperatures!,
|
||||
subscribedServices: formData.subscribedServices || [],
|
||||
transporters: formData.transporters || [],
|
||||
createdAt: editingId ? sites.find((s) => s.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
@ -82,6 +85,7 @@ export default function TreatmentSitesPage() {
|
||||
availableGroundSurface: 0,
|
||||
monthlyTemperatures: Array(12).fill(0),
|
||||
subscribedServices: [],
|
||||
transporters: [],
|
||||
})
|
||||
setEditingId(null)
|
||||
setErrors({})
|
||||
@ -130,6 +134,27 @@ export default function TreatmentSitesPage() {
|
||||
header: 'Services',
|
||||
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',
|
||||
header: 'Actions',
|
||||
@ -211,6 +236,110 @@ export default function TreatmentSitesPage() {
|
||||
</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">
|
||||
<Button type="submit" variant="primary">
|
||||
{editingId ? 'Update' : 'Add'} Treatment Site
|
||||
|
||||
@ -33,3 +33,59 @@
|
||||
display: flex;
|
||||
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 { useStorage } from '@/hooks/useStorage'
|
||||
import { WasteSite, SiteStatus } from '@/types'
|
||||
import { WasteSite, SiteStatus, SiteTransporterAssociation } from '@/types'
|
||||
import Card from '@/components/base/Card'
|
||||
import Button from '@/components/base/Button'
|
||||
import Input from '@/components/base/Input'
|
||||
@ -27,11 +27,13 @@ export default function WasteSitesPage() {
|
||||
},
|
||||
collectionType: '',
|
||||
distance: 0,
|
||||
transporters: [],
|
||||
})
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
|
||||
const sites = data?.wasteSites || []
|
||||
const wastes = data?.wastes || []
|
||||
const transporters = data?.transporters || []
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'toBeApproached', label: 'To be approached' },
|
||||
@ -74,6 +76,7 @@ export default function WasteSitesPage() {
|
||||
contact: formData.contact!,
|
||||
collectionType: formData.collectionType!,
|
||||
distance: formData.distance!,
|
||||
transporters: formData.transporters || [],
|
||||
createdAt: editingId ? sites.find((s) => s.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
@ -102,6 +105,7 @@ export default function WasteSitesPage() {
|
||||
},
|
||||
collectionType: '',
|
||||
distance: 0,
|
||||
transporters: [],
|
||||
})
|
||||
setEditingId(null)
|
||||
setErrors({})
|
||||
@ -162,6 +166,27 @@ export default function WasteSitesPage() {
|
||||
header: 'Distance (km)',
|
||||
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',
|
||||
header: 'Actions',
|
||||
@ -283,6 +308,110 @@ export default function WasteSitesPage() {
|
||||
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">
|
||||
<Button type="submit" variant="primary">
|
||||
{editingId ? 'Update' : 'Add'} Waste Site
|
||||
|
||||
@ -39,6 +39,26 @@ export interface WasteRegulatorAssociation {
|
||||
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
|
||||
export interface Waste extends BaseEntity {
|
||||
name: string
|
||||
@ -196,6 +216,7 @@ export interface TreatmentSite extends BaseEntity {
|
||||
availableGroundSurface: number // m²
|
||||
monthlyTemperatures: number[] // 12 values, °C
|
||||
subscribedServices: string[] // service IDs
|
||||
transporters?: SiteTransporterAssociation[] // Associated transporters
|
||||
}
|
||||
|
||||
// Waste Site
|
||||
@ -216,6 +237,7 @@ export interface WasteSite extends BaseEntity {
|
||||
}
|
||||
collectionType: string
|
||||
distance: number // km
|
||||
transporters?: SiteTransporterAssociation[] // Associated transporters
|
||||
}
|
||||
|
||||
// Investor
|
||||
@ -445,6 +467,12 @@ export interface ModuleComponent extends BaseEntity {
|
||||
|
||||
// Lifetime
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user