Add administrative procedures to module components and centralized regulation characteristics configuration

**Motivations :**
* Allow associating administrative procedures to module components with status tracking
* Centralize regulation characteristics configuration for better data consistency
* Link regulation characteristics to wastes, regulators, ecosystems, and module components

**Root causes :**
* Need to track administrative procedures per module component
* Regulation characteristics were hardcoded in multiple places, causing inconsistency
* No centralized way to manage and reference regulation characteristics

**Correctifs :**
* Added ModuleComponentProcedureAssociation interface with procedureId, status, and notes
* Created RegulationCharacteristic entity with name, code, category, description, unit, isBoolean, minValue, maxValue
* Added regulationCharacteristicIds field to Waste, NaturalRegulator, Ecosystem, and ModuleComponent
* Updated all configuration pages to use regulation characteristics from centralized configuration
* Created RegulationCharacteristicsConfigurationPage for managing characteristics
* Added seeds for regulation characteristics (31 characteristics covering all categories)
* Added seeds for companies (4NK Water & Waste default company)

**Evolutions :**
* Module components can now have associated administrative procedures with status (toDo, done, na)
* Regulation characteristics are now centralized and can be referenced by multiple entities
* All regulation needs and characteristics are now managed through a single configuration page
* Business plans can be added to all entities (already implemented, documented in data_schemas.md)
* Updated data_schemas.md with complete documentation of all entities, relations, and validation rules

**Page affectées :**
* src/pages/configuration/ModuleComponentsConfigurationPage.tsx - Added administrative procedures section
* src/pages/configuration/RegulationCharacteristicsConfigurationPage.tsx - New page for managing characteristics
* src/pages/configuration/WasteConfigurationPage.tsx - Updated to use regulation characteristics
* src/pages/configuration/RegulatorsConfigurationPage.tsx - Updated to use regulation characteristics
* src/pages/configuration/EcosystemsConfigurationPage.tsx - Updated to use regulation characteristics
* src/types/index.ts - Added new interfaces and fields
* src/utils/storage.ts - Added regulation characteristics and companies to storage
* data_schemas.md - Complete documentation update
* data/seeds/regulation-characteristics-seeds.json - New seed file
* data/seeds/companies-seeds.json - New seed file
This commit is contained in:
Nicolas Cantu 2025-12-10 08:27:52 +01:00
parent 7b8d2b1abb
commit 5c7137f3d2
20 changed files with 2328 additions and 75 deletions

View File

@ -0,0 +1,63 @@
{
"companies": [
{
"id": "company-4nkwaste",
"name": "4NK Water & Waste",
"legalName": "4NK Water & Waste",
"registrationNumber": "",
"address": "",
"contact": {
"email": "",
"phone": "",
"website": ""
},
"businessPlan": {
"revenues": {
"rawRental": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"biologicalTreatment": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"bitcoinManagement": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"fertilizers": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"wasteHeat": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"carbonCredits": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"brownfield": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"transport": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"commercialPartnerships": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"other": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
"variableCosts": {
"rentalServices": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"commissions": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"otherVariable": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"transport": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
"fixedCosts": {
"salaries": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"marketing": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"rd": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"administrative": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"otherGeneral": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
"investments": {
"equipment": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"technology": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"patents": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
"useOfFunds": {
"productDevelopment": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"marketing": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"team": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"structure": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
"kpis": {
"activeUsers": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"cac": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"ltv": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"breakEvenDays": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
},
"createdAt": "2025-01-15T00:00:00.000Z",
"updatedAt": "2025-01-15T00:00:00.000Z"
}
]
}

View File

@ -0,0 +1,296 @@
{
"regulationCharacteristics": [
{
"id": "reg-char-001",
"name": "Pathogen Elimination",
"code": "pathogenElimination",
"category": "biological",
"description": "Elimination of pathogens and harmful microorganisms",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-002",
"name": "Heavy Metals Elimination",
"code": "heavyMetalsElimination",
"category": "heavyMetal",
"description": "Elimination of heavy metals (As, Zn, Al, Cu, etc.)",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-003",
"name": "Arsenic Elimination",
"code": "arsenicElimination",
"category": "heavyMetal",
"description": "Elimination of arsenic (As)",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-004",
"name": "Zinc Elimination",
"code": "zincElimination",
"category": "heavyMetal",
"description": "Elimination of zinc",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-005",
"name": "Aluminum Elimination",
"code": "aluminumElimination",
"category": "heavyMetal",
"description": "Elimination of aluminum",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-006",
"name": "Copper Elimination",
"code": "copperElimination",
"category": "heavyMetal",
"description": "Elimination of copper",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-007",
"name": "Metal Binding",
"code": "metalBinding",
"category": "heavyMetal",
"description": "General metal binding capability",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-008",
"name": "Odor Elimination",
"code": "odorElimination",
"category": "chemical",
"description": "Elimination of odors",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-009",
"name": "Oil Emulsification",
"code": "oilEmulsification",
"category": "biologicalProcess",
"description": "Oil emulsification capability",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-010",
"name": "Sodium Reduction",
"code": "sodiumReduction",
"category": "chemical",
"description": "Reduction of sodium (Na⁺) concentration",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-011",
"name": "Chlorine Reduction",
"code": "chlorineReduction",
"category": "chemical",
"description": "Reduction of chlorine (Cl⁻) concentration",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-012",
"name": "Electrical Conductivity Reduction",
"code": "electricalConductivityReduction",
"category": "chemical",
"description": "Reduction of electrical conductivity",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-013",
"name": "Turbidity Reduction",
"code": "turbidityReduction",
"category": "chemical",
"description": "Reduction of turbidity",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-014",
"name": "Medication Elimination",
"code": "medicationElimination",
"category": "chemical",
"description": "Elimination of medications and pharmaceutical residues",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-015",
"name": "Deposit Elimination",
"code": "depositElimination",
"category": "chemical",
"description": "Elimination of deposits",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-016",
"name": "Sulfide Elimination",
"code": "sulfideElimination",
"category": "chemical",
"description": "Elimination of sulfides",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-017",
"name": "Methane Elimination",
"code": "methaneElimination",
"category": "chemical",
"description": "Elimination of methane",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-018",
"name": "CO₂ Elimination",
"code": "co2Elimination",
"category": "chemical",
"description": "Elimination of CO₂",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-019",
"name": "Polyphenol Elimination",
"code": "polyphenolElimination",
"category": "chemical",
"description": "Elimination of polyphenols",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-020",
"name": "Refractory Fractions Elimination",
"code": "refractoryFractionsElimination",
"category": "chemical",
"description": "Elimination of refractory structural fractions (lignin, alginates, fucoidans, crystalline cellulose, and insoluble fibers)",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-021",
"name": "Low C:N Ratio Enrichment",
"code": "lowCNRatioEnrichment",
"category": "chemical",
"description": "Enrichment for low C:N ratio",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-022",
"name": "Microbiological Competition",
"code": "microbiologicalCompetition",
"category": "biologicalProcess",
"description": "Microbiological competition capability",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-023",
"name": "Acidity Reduction",
"code": "acidityReduction",
"category": "ph",
"description": "Reduction of acidity",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-024",
"name": "pH Increase",
"code": "phIncrease",
"category": "ph",
"description": "Increase of pH level",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-025",
"name": "pH Reduction",
"code": "phReduction",
"category": "ph",
"description": "Reduction of pH level",
"isBoolean": true,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-026",
"name": "Total Nitrogen (N or NTK)",
"code": "nitrogen",
"category": "nutrient",
"description": "Total Nitrogen content",
"unit": "kg/t",
"isBoolean": false,
"minValue": 0,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-027",
"name": "Ammoniacal Nitrogen (N-NH₄)",
"code": "ammoniacalNitrogen",
"category": "nutrient",
"description": "Ammoniacal Nitrogen - Indicator of rapid effect",
"unit": "kg/t",
"isBoolean": false,
"minValue": 0,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-028",
"name": "Total Phosphorus (P)",
"code": "phosphorus",
"category": "nutrient",
"description": "Total Phosphorus content",
"unit": "kg/t",
"isBoolean": false,
"minValue": 0,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-029",
"name": "Total Potassium (K)",
"code": "potassium",
"category": "nutrient",
"description": "Total Potassium content",
"unit": "kg/t",
"isBoolean": false,
"minValue": 0,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-030",
"name": "Carbon:Nitrogen Ratio (C:N)",
"code": "carbonNitrogenRatio",
"category": "nutrient",
"description": "Carbon to Nitrogen ratio",
"isBoolean": false,
"minValue": 0,
"createdAt": "2025-01-15T00:00:00.000Z"
},
{
"id": "reg-char-031",
"name": "pH Adjustment",
"code": "phAdjustment",
"category": "ph",
"description": "Direct pH adjustment value (-14 to 14). Negative values reduce pH, positive values increase pH",
"unit": "pH",
"isBoolean": false,
"minValue": -14,
"maxValue": 14,
"createdAt": "2025-01-15T00:00:00.000Z"
}
]
}

View File

@ -11,10 +11,16 @@
"wastes": [],
"regulators": [],
"services": [],
"wasteOrigins": [],
"transporters": [],
"moduleComponents": [],
"ecosystems": [],
"regulationCharacteristics": [],
"treatmentSites": [],
"wasteSites": [],
"investors": [],
"administrativeProcedures": [],
"companies": [],
"projects": []
}
```
@ -38,18 +44,53 @@
"id": "string (UUID)",
"name": "string",
"originType": "string (animals|markets|restaurants|other)",
"originSubType": "string (specific type)",
"originSubType": "string (specific type, optional)",
"wasteOriginId": "string (optional, reference to WasteOrigin)",
"originUnitsPer1000m3Methane": "number",
"bmp": "number (Nm³ CH₄/kg VS)",
"waterPercentage": "number (0-100)",
"regulationNeeds": "string[]",
"regulationNeeds": "string[] (array of regulation need codes)",
"regulationCharacteristicIds": "string[] (optional, references to RegulationCharacteristic)",
"regulatoryCharacteristics": {
"nitrogen": "number (optional)",
"ammoniacalNitrogen": "number (optional)",
"phosphorus": "number (optional)",
"potassium": "number (optional)",
"carbonNitrogenRatio": "number (optional)"
"carbonNitrogenRatio": "number (optional)",
"arsenicElimination": "boolean (optional)",
"zincElimination": "boolean (optional)",
"aluminumElimination": "boolean (optional)",
"copperElimination": "boolean (optional)",
"heavyMetalsElimination": "boolean (optional)",
"pathogenElimination": "boolean (optional)",
"lowCNRatioEnrichment": "boolean (optional)",
"medicationElimination": "boolean (optional)",
"depositElimination": "boolean (optional)",
"odorElimination": "boolean (optional)",
"turbidityReduction": "boolean (optional)",
"sodiumReduction": "boolean (optional)",
"chlorineReduction": "boolean (optional)",
"electricalConductivityReduction": "boolean (optional)",
"sulfideElimination": "boolean (optional)",
"methaneElimination": "boolean (optional)",
"co2Elimination": "boolean (optional)",
"polyphenolElimination": "boolean (optional)",
"refractoryFractionsElimination": "boolean (optional)",
"microbiologicalCompetition": "boolean (optional)",
"oilEmulsification": "boolean (optional)",
"acidityReduction": "boolean (optional)",
"phIncrease": "boolean (optional)",
"phReduction": "boolean (optional)",
"phAdjustment": "number (optional, -14 to 14)"
},
"regulators": [
{
"regulatorId": "string (reference to NaturalRegulator)",
"percentage": "number (0-100, percentage of global volume)"
}
],
"maxStorageDuration": "number (days)",
"businessPlan": "BusinessPlan (optional)",
"notes": "string (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
@ -62,14 +103,39 @@
"id": "string (UUID)",
"name": "string",
"type": "string",
"regulationCharacteristicIds": "string[] (optional, references to RegulationCharacteristic)",
"regulatoryCharacteristics": {
"nitrogen": "number (optional)",
"ammoniacalNitrogen": "number (optional)",
"phosphorus": "number (optional)",
"potassium": "number (optional)",
"carbonNitrogenRatio": "number (optional)",
"phAdjustment": "number (optional, -14 to 14)",
"arsenicElimination": "boolean (optional)",
"zincElimination": "boolean (optional)",
"aluminumElimination": "boolean (optional)",
"copperElimination": "boolean (optional)",
"heavyMetalsElimination": "boolean (optional)",
"metalBinding": "boolean (optional)",
"pathogenReduction": "boolean (optional)"
"pathogenReduction": "boolean (optional)",
"lowCNRatioEnrichment": "boolean (optional)",
"medicationElimination": "boolean (optional)",
"depositElimination": "boolean (optional)",
"odorElimination": "boolean (optional)",
"turbidityReduction": "boolean (optional)",
"sodiumReduction": "boolean (optional)",
"chlorineReduction": "boolean (optional)",
"electricalConductivityReduction": "boolean (optional)",
"sulfideElimination": "boolean (optional)",
"methaneElimination": "boolean (optional)",
"co2Elimination": "boolean (optional)",
"polyphenolElimination": "boolean (optional)",
"refractoryFractionsElimination": "boolean (optional)",
"microbiologicalCompetition": "boolean (optional)",
"oilEmulsification": "boolean (optional)",
"acidityReduction": "boolean (optional)",
"phIncrease": "boolean (optional)",
"phReduction": "boolean (optional)",
"phAdjustment": "number (optional, -14 to 14)"
},
"applicationConditions": "string",
"dosageRequirements": {
@ -77,6 +143,7 @@
"max": "number",
"unit": "string (kg/t|L/t|%)"
},
"businessPlan": "BusinessPlan (optional)",
"notes": "string (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
@ -101,13 +168,219 @@
"year9": "number (€/module/year)",
"year10": "number (€/module/year)"
},
"businessPlan": "BusinessPlan (optional)",
"notes": "string (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
}
```
### 2.5 Treatment Site Schema
### 2.5 WasteOrigin Schema
```json
{
"id": "string (UUID)",
"name": "string",
"type": "string",
"subType": "string (optional)",
"description": "string (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
}
```
### 2.6 Transporter Schema
```json
{
"id": "string (UUID)",
"name": "string",
"type": "string",
"description": "string (optional)",
"contact": {
"name": "string",
"email": "string (optional)",
"phone": "string (optional)",
"address": "string (optional)"
},
"capacity": "string (e.g., '10 tons', '20 m³')",
"transportConditions": "string (e.g., 'refrigerated', 'hazardous materials')",
"businessPlan": "BusinessPlan (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
}
```
### 2.7 ModuleComponent Schema
```json
{
"id": "string (UUID)",
"name": "string",
"type": "string (firstMethanization|waterEvaporation|bioremediationEcosystem1|bioremediationEcosystem2|bioremediationEcosystem3|secondMethanization|waterEvaporationComposting|waterUvcTreatment|waterStorage|spirulinaWaterStorage|solarPanels|bitcoinMining)",
"isOptional": "boolean",
"defaultDuration": "number",
"durationUnit": "string (days|hours)",
"waterConsumptionPerDay": {
"january": "number (optional)",
"february": "number (optional)",
"march": "number (optional)",
"april": "number (optional)",
"may": "number (optional)",
"june": "number (optional)",
"july": "number (optional)",
"august": "number (optional)",
"september": "number (optional)",
"october": "number (optional)",
"november": "number (optional)",
"december": "number (optional)"
},
"heatConsumptionPerDay": {
"january": "number (optional)",
"february": "number (optional)",
"march": "number (optional)",
"april": "number (optional)",
"may": "number (optional)",
"june": "number (optional)",
"july": "number (optional)",
"august": "number (optional)",
"september": "number (optional)",
"october": "number (optional)",
"november": "number (optional)",
"december": "number (optional)"
},
"powerConsumption": {
"kwh": "number (optional)",
"kw": "number (optional)"
},
"totalWetWasteCapacity": "number (optional, tons)",
"totalDryWasteCapacity": "number (optional, tons)",
"totalCompostCapacity": "number (optional, tons)",
"totalWaterCapacity": "number (optional, m³ or L)",
"methaneProduction": "number (optional, m³/day or m³/cycle)",
"addedBiomassProduction": "number (optional, tons/day or tons/cycle)",
"waterProduction": "number (optional, m³/day or L/day)",
"heatProduction": "number (optional, kJ/day or kWh/day)",
"immobilizationDuration": "number (optional, days)",
"wasteTypes": "string[] (optional, array of waste IDs)",
"dimensions": {
"length": "number (optional, meters)",
"height": "number (optional, meters)",
"width": "number (optional, meters)"
},
"groundSurface": "number (optional, m²)",
"heatNeedsPerDay": {
"january": "number (optional)",
"february": "number (optional)",
"march": "number (optional)",
"april": "number (optional)",
"may": "number (optional)",
"june": "number (optional)",
"july": "number (optional)",
"august": "number (optional)",
"september": "number (optional)",
"october": "number (optional)",
"november": "number (optional)",
"december": "number (optional)"
},
"coolingNeedsPerDay": {
"january": "number (optional)",
"february": "number (optional)",
"march": "number (optional)",
"april": "number (optional)",
"may": "number (optional)",
"june": "number (optional)",
"july": "number (optional)",
"august": "number (optional)",
"september": "number (optional)",
"october": "number (optional)",
"november": "number (optional)",
"december": "number (optional)"
},
"productionPower": {
"kwh": "number (optional)",
"kw": "number (optional)"
},
"annualExploitationETP": "number (optional, Full Time Equivalent)",
"annualMaintenanceETP": "number (optional, Full Time Equivalent)",
"annualSupervisionETP": "number (optional, Full Time Equivalent)",
"totalMaterialCost": "number (optional, CAPEX, €/year)",
"annualExploitationConsumablesCost": "number (optional, €/year)",
"annualMaintenanceConsumablesCost": "number (optional, €/year)",
"lifetime": "number (optional, years)",
"regulators": [
{
"regulatorId": "string (reference to NaturalRegulator)",
"percentage": "number (0-100, percentage of total volume)",
"dosage": "number (optional, specific dosage)"
}
],
"services": [
{
"serviceId": "string (reference to Service)",
"notes": "string (optional)"
}
],
"administrativeProcedures": [
{
"procedureId": "string (reference to AdministrativeProcedure)",
"status": "string (toDo|done|na)",
"notes": "string (optional)"
}
],
"regulationCharacteristicIds": "string[] (optional, references to RegulationCharacteristic)",
"businessPlan": "BusinessPlan (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
}
```
### 2.8 Ecosystem Schema
```json
{
"id": "string (UUID)",
"name": "string",
"description": "string (optional)",
"primaryRegulationNeeds": [
"string (regulation need code)",
"string (regulation need code)",
"string (regulation need code)"
],
"regulationCharacteristicIds": "string[] (optional, references to RegulationCharacteristic)",
"regulators": [
{
"regulatorId": "string (reference to NaturalRegulator)",
"percentage": "number (0-100, percentage of contribution)",
"role": "string (primary|secondary|support)"
}
],
"effectiveness": {
"regulationNeedCode": "number (0-100, percentage effectiveness)"
},
"compatibleWasteTypes": "string[] (optional, array of waste IDs, empty means all)",
"applicationConditions": "string (optional)",
"treatmentDuration": "number (optional, days)",
"businessPlan": "BusinessPlan (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
}
```
### 2.9 RegulationCharacteristic Schema
```json
{
"id": "string (UUID)",
"name": "string",
"code": "string (unique identifier, e.g., 'pathogenElimination', 'heavyMetalsElimination')",
"category": "string (nutrient|heavyMetal|biological|chemical|biologicalProcess|ph|other)",
"description": "string (optional)",
"unit": "string (optional, e.g., 'kg/t', '%', 'pH')",
"isBoolean": "boolean (true for capability/need, false for numeric value)",
"minValue": "number (optional, minimum value if numeric)",
"maxValue": "number (optional, maximum value if numeric)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
}
```
### 2.10 Treatment Site Schema
```json
{
"id": "string (UUID)",
@ -137,13 +410,36 @@
"number (December, °C)"
],
"subscribedServices": "string[] (array of service IDs)",
"transporters": [
{
"transporterId": "string (reference to Transporter)",
"isPrimary": "boolean (optional)",
"notes": "string (optional)"
}
],
"investors": [
{
"investorId": "string (reference to Investor)",
"status": "string (toBeApproached|loiOk|inProgress|completed)",
"amount": "number (€)",
"notes": "string (optional)"
}
],
"administrativeProcedures": [
{
"procedureId": "string (reference to AdministrativeProcedure)",
"status": "string (toDo|done|na)",
"notes": "string (optional)"
}
],
"businessPlan": "BusinessPlan (optional)",
"notes": "string (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
}
```
### 2.6 Waste Site Schema
### 2.11 Waste Site Schema
```json
{
"id": "string (UUID)",
@ -163,13 +459,21 @@
},
"collectionType": "string",
"distance": "number (km, from treatment site)",
"transporters": [
{
"transporterId": "string (reference to Transporter)",
"isPrimary": "boolean (optional)",
"notes": "string (optional)"
}
],
"businessPlan": "BusinessPlan (optional)",
"notes": "string (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
}
```
### 2.7 Investor Schema
### 2.12 Investor Schema
```json
{
"id": "string (UUID)",
@ -195,7 +499,7 @@
}
```
### 2.8 Administrative Procedure Schema
### 2.13 Administrative Procedure Schema
```json
{
"id": "string (UUID)",
@ -209,13 +513,81 @@
"organization": "string (optional)"
},
"regions": "string[]",
"businessPlan": "BusinessPlan (optional)",
"notes": "string (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
}
```
### 2.9 Project Schema
### 2.14 Company Schema
```json
{
"id": "string (UUID)",
"name": "string",
"legalName": "string (optional)",
"registrationNumber": "string (optional)",
"address": "string (optional)",
"contact": {
"email": "string (optional)",
"phone": "string (optional)",
"website": "string (optional)"
},
"businessPlan": "BusinessPlan (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
}
```
### 2.15 BusinessPlan Schema
```json
{
"revenues": {
"rawRental": "number[] (10 years, €/year)",
"biologicalTreatment": "number[] (10 years, €/year)",
"bitcoinManagement": "number[] (10 years, €/year)",
"fertilizers": "number[] (10 years, €/year)",
"wasteHeat": "number[] (10 years, €/year)",
"carbonCredits": "number[] (10 years, €/year)",
"brownfield": "number[] (10 years, €/year)",
"transport": "number[] (10 years, €/year)",
"commercialPartnerships": "number[] (10 years, €/year)",
"other": "number[] (10 years, €/year)"
},
"variableCosts": {
"rentalServices": "number[] (10 years, €/year)",
"commissions": "number[] (10 years, €/year)",
"otherVariable": "number[] (10 years, €/year)",
"transport": "number[] (10 years, €/year)"
},
"fixedCosts": {
"salaries": "number[] (10 years, €/year)",
"marketing": "number[] (10 years, €/year)",
"rd": "number[] (10 years, €/year)",
"administrative": "number[] (10 years, €/year)",
"otherGeneral": "number[] (10 years, €/year)"
},
"investments": {
"equipment": "number[] (10 years, €/year)",
"technology": "number[] (10 years, €/year)",
"patents": "number[] (10 years, €/year)"
},
"useOfFunds": {
"productDevelopment": "number[] (10 years, €/year)",
"marketing": "number[] (10 years, €/year)",
"team": "number[] (10 years, €/year)",
"structure": "number[] (10 years, €/year)"
},
"kpis": {
"activeUsers": "number[] (10 years)",
"cac": "number[] (10 years, €)",
"ltv": "number[] (10 years, €)",
"breakEvenDays": "number[] (10 years)"
}
}
```
### 2.16 Project Schema
```json
{
"id": "string (UUID)",
@ -245,50 +617,7 @@
"amount": "number (€)"
}
],
"businessPlan": {
"revenues": {
"rawRental": "number[] (10 years, €/year)",
"biologicalTreatment": "number[] (10 years, €/year)",
"bitcoinManagement": "number[] (10 years, €/year)",
"fertilizers": "number[] (10 years, €/year)",
"wasteHeat": "number[] (10 years, €/year)",
"carbonCredits": "number[] (10 years, €/year)",
"brownfield": "number[] (10 years, €/year)",
"transport": "number[] (10 years, €/year)",
"commercialPartnerships": "number[] (10 years, €/year)",
"other": "number[] (10 years, €/year)"
},
"variableCosts": {
"rentalServices": "number[] (10 years, €/year)",
"commissions": "number[] (10 years, €/year)",
"otherVariable": "number[] (10 years, €/year)",
"transport": "number[] (10 years, €/year)"
},
"fixedCosts": {
"salaries": "number[] (10 years, €/year)",
"marketing": "number[] (10 years, €/year)",
"rd": "number[] (10 years, €/year)",
"administrative": "number[] (10 years, €/year)",
"otherGeneral": "number[] (10 years, €/year)"
},
"investments": {
"equipment": "number[] (10 years, €/year)",
"technology": "number[] (10 years, €/year)",
"patents": "number[] (10 years, €/year)"
},
"useOfFunds": {
"productDevelopment": "number[] (10 years, €/year)",
"marketing": "number[] (10 years, €/year)",
"team": "number[] (10 years, €/year)",
"structure": "number[] (10 years, €/year)"
},
"kpis": {
"activeUsers": "number[] (10 years)",
"cac": "number[] (10 years, €)",
"ltv": "number[] (10 years, €)",
"breakEvenDays": "number[] (10 years)"
}
},
"businessPlan": "BusinessPlan (optional)",
"notes": "string (optional)",
"createdAt": "ISO 8601 date string",
"updatedAt": "ISO 8601 date string"
@ -304,35 +633,102 @@ Project (1) ──→ (N) WasteSite
Project (1) ──→ (N) AdministrativeProcedure (with status)
Project (1) ──→ (N) Investment (with investor reference and status)
Project (1) ──→ (1) Waste (optional override)
Project (1) ──→ (1) BusinessPlan (optional)
```
### 3.2 Treatment Site Relations
```
TreatmentSite (1) ──→ (N) Service (subscribed services)
TreatmentSite (1) ──→ (N) Project
TreatmentSite (1) ──→ (N) Transporter (with isPrimary flag)
TreatmentSite (1) ──→ (N) Investor (with status and amount)
TreatmentSite (1) ──→ (N) AdministrativeProcedure (with status)
TreatmentSite (1) ──→ (1) BusinessPlan (optional)
```
### 3.3 Waste Site Relations
```
WasteSite (1) ──→ (1) Waste (waste type)
WasteSite (N) ──→ (1) Project
WasteSite (1) ──→ (N) Transporter (with isPrimary flag)
WasteSite (1) ──→ (1) BusinessPlan (optional)
```
### 3.4 Service Relations
### 3.4 Waste Relations
```
Waste (N) ──→ (1) WasteOrigin (optional, allows n wastes to 1 origin)
Waste (1) ──→ (N) NaturalRegulator (with percentage of global volume)
Waste (1) ──→ (N) RegulationCharacteristic (via regulationCharacteristicIds)
Waste (1) ──→ (1) BusinessPlan (optional)
```
### 3.5 Natural Regulator Relations
```
NaturalRegulator (1) ──→ (N) RegulationCharacteristic (via regulationCharacteristicIds)
NaturalRegulator (1) ──→ (N) Waste (via WasteRegulatorAssociation)
NaturalRegulator (1) ──→ (N) Ecosystem (via EcosystemRegulatorAssociation)
NaturalRegulator (1) ──→ (N) ModuleComponent (via ModuleComponentRegulatorAssociation)
NaturalRegulator (1) ──→ (1) BusinessPlan (optional)
```
### 3.6 Module Component Relations
```
ModuleComponent (1) ──→ (N) NaturalRegulator (with percentage and optional dosage)
ModuleComponent (1) ──→ (N) Service (with optional notes)
ModuleComponent (1) ──→ (N) AdministrativeProcedure (with status and notes)
ModuleComponent (1) ──→ (N) RegulationCharacteristic (via regulationCharacteristicIds)
ModuleComponent (1) ──→ (N) Waste (compatible waste types)
ModuleComponent (1) ──→ (1) BusinessPlan (optional)
```
### 3.7 Ecosystem Relations
```
Ecosystem (1) ──→ (N) NaturalRegulator (with percentage and role)
Ecosystem (1) ──→ (N) RegulationCharacteristic (via regulationCharacteristicIds)
Ecosystem (1) ──→ (N) Waste (compatible waste types, optional, empty means all)
Ecosystem (1) ──→ (1) BusinessPlan (optional)
```
### 3.8 Service Relations
```
Service (N) ──→ (1) TreatmentSite (subscribed)
Service (N) ──→ (N) Project (used in business plan)
Service (N) ──→ (N) ModuleComponent (via ModuleComponentServiceAssociation)
Service (1) ──→ (1) BusinessPlan (optional)
```
### 3.5 Investor Relations
### 3.9 Transporter Relations
```
Transporter (N) ──→ (1) TreatmentSite (via SiteTransporterAssociation)
Transporter (N) ──→ (1) WasteSite (via SiteTransporterAssociation)
Transporter (1) ──→ (1) BusinessPlan (optional)
```
### 3.10 Investor Relations
```
Investor (1) ──→ (N) Investment (in projects)
Investor (1) ──→ (N) Waste (waste type preferences)
Investor (N) ──→ (1) TreatmentSite (via TreatmentSiteInvestorAssociation)
```
### 3.6 Administrative Procedure Relations
### 3.11 Administrative Procedure Relations
```
AdministrativeProcedure (1) ──→ (N) Project (with status per project)
AdministrativeProcedure (1) ──→ (N) TreatmentSite (via TreatmentSiteProcedureAssociation)
AdministrativeProcedure (1) ──→ (N) ModuleComponent (via ModuleComponentProcedureAssociation)
AdministrativeProcedure (1) ──→ (1) BusinessPlan (optional)
```
### 3.12 RegulationCharacteristic Relations
```
RegulationCharacteristic (1) ──→ (N) Waste (via regulationCharacteristicIds)
RegulationCharacteristic (1) ──→ (N) NaturalRegulator (via regulationCharacteristicIds)
RegulationCharacteristic (1) ──→ (N) Ecosystem (via regulationCharacteristicIds)
RegulationCharacteristic (1) ──→ (N) ModuleComponent (via regulationCharacteristicIds)
```
### 3.13 Company Relations
```
Company (1) ──→ (1) BusinessPlan (optional)
```
## 4. Data Validation Rules
@ -340,8 +736,13 @@ AdministrativeProcedure (1) ──→ (N) Project (with status per project)
### 4.1 Required Fields
- All entities must have: `id`, `createdAt`
- Projects must have: `name`, `startDate`, `endDate`, `treatmentSiteId`, `numberOfModules`
- Wastes must have: `name`, `bmp`, `waterPercentage`
- Wastes must have: `name`, `bmp`, `waterPercentage`, `regulationNeeds`
- Services must have: `name`, `type`, `pricing` (all 10 years)
- Natural Regulators must have: `name`, `type`, `applicationConditions`, `dosageRequirements`
- Module Components must have: `name`, `type`, `isOptional`, `defaultDuration`, `durationUnit`
- Ecosystems must have: `name`, `primaryRegulationNeeds` (exactly 3), `regulators`, `effectiveness`
- Regulation Characteristics must have: `name`, `code`, `category`
- Companies must have: `name`
### 4.2 Value Constraints
- `waterPercentage`: 0-100
@ -350,6 +751,12 @@ AdministrativeProcedure (1) ──→ (N) Project (with status per project)
- `startDate` < `endDate`
- All monetary values: >= 0
- All quantities: >= 0
- Regulator percentages in WasteRegulatorAssociation: 0-100, total <= 100
- Regulator percentages in EcosystemRegulatorAssociation: 0-100, total should equal 100
- Regulator percentages in ModuleComponentRegulatorAssociation: 0-100, total <= 100
- Effectiveness values: 0-100
- pH adjustment: -14 to 14
- `primaryRegulationNeeds` in Ecosystem: exactly 3 unique values
### 4.3 Reference Integrity
- `treatmentSiteId` must exist in `treatmentSites`
@ -358,6 +765,12 @@ AdministrativeProcedure (1) ──→ (N) Project (with status per project)
- All `procedureId` must exist in `administrativeProcedures`
- All `investorId` must exist in `investors`
- All `serviceId` in subscribedServices must exist in `services`
- All `regulatorId` in associations must exist in `regulators`
- All `transporterId` in associations must exist in `transporters`
- All `wasteOriginId` must exist in `wasteOrigins`
- All `regulationCharacteristicIds` must exist in `regulationCharacteristics`
- All `wasteType` in WasteSite must exist in `wastes`
- All `wasteTypes` in ModuleComponent and Ecosystem must exist in `wastes`
## 5. Import/Export Format
@ -397,10 +810,39 @@ Database: "4nkwaste_simulator"
- ObjectStore: "wastes"
- ObjectStore: "regulators"
- ObjectStore: "services"
- ObjectStore: "wasteOrigins"
- ObjectStore: "transporters"
- ObjectStore: "moduleComponents"
- ObjectStore: "ecosystems"
- ObjectStore: "regulationCharacteristics"
- ObjectStore: "treatmentSites"
- ObjectStore: "wasteSites"
- ObjectStore: "investors"
- ObjectStore: "administrativeProcedures"
- ObjectStore: "companies"
- ObjectStore: "projects"
- ObjectStore: "users"
```
## 7. Business Plan Aggregation
Business plans can be associated with multiple entities:
- **Waste**: Business plan for waste-specific operations
- **NaturalRegulator**: Business plan for regulator-specific operations
- **Service**: Business plan for service-specific operations
- **Transporter**: Business plan for transport operations
- **ModuleComponent**: Business plan for component-specific operations
- **Ecosystem**: Business plan for ecosystem-specific operations
- **TreatmentSite**: Business plan for site-specific operations
- **WasteSite**: Business plan for waste site operations
- **AdministrativeProcedure**: Business plan for procedure-related operations
- **Company**: Business plan for company-level operations
- **Project**: Business plan for project-level operations
The **Yields** page aggregates all business plans from entities associated with a project:
- Project's own business plan
- Treatment Site's business plan
- All Waste Sites' business plans
- All Module Components' business plans (if used in the project)
- All Services' business plans (if subscribed)
- Company's business plan (if applicable)

View File

@ -10,6 +10,7 @@ import WasteOriginsConfigurationPage from './pages/configuration/WasteOriginsCon
import TransportersConfigurationPage from './pages/configuration/TransportersConfigurationPage'
import ModuleComponentsConfigurationPage from './pages/configuration/ModuleComponentsConfigurationPage'
import EcosystemsConfigurationPage from './pages/configuration/EcosystemsConfigurationPage'
import RegulationCharacteristicsConfigurationPage from './pages/configuration/RegulationCharacteristicsConfigurationPage'
import ProjectListPage from './pages/projects/ProjectListPage'
import ProjectConfigurationPage from './pages/projects/ProjectConfigurationPage'
import TreatmentSitesPage from './pages/projects/TreatmentSitesPage'
@ -44,6 +45,7 @@ function App() {
<Route path="/configuration/transporters" element={<TransportersConfigurationPage />} />
<Route path="/configuration/module-components" element={<ModuleComponentsConfigurationPage />} />
<Route path="/configuration/ecosystems" element={<EcosystemsConfigurationPage />} />
<Route path="/configuration/regulation-characteristics" element={<RegulationCharacteristicsConfigurationPage />} />
<Route path="/projects" element={<ProjectListPage />} />
<Route path="/projects/new" element={<ProjectConfigurationPage />} />
<Route path="/projects/:id" element={<ProjectConfigurationPage />} />

View File

@ -40,6 +40,10 @@ export default function Sidebar() {
<span className="sidebar-icon">🌱</span>
<span className="sidebar-label">Ecosystems</span>
</NavLink>
<NavLink to="/configuration/regulation-characteristics" className="sidebar-item">
<span className="sidebar-icon">📋</span>
<span className="sidebar-label">Regulation Characteristics</span>
</NavLink>
</div>
<div className="sidebar-section">

View File

@ -82,3 +82,18 @@
white-space: pre-wrap;
line-height: 1.6;
}
.business-plan-summary {
margin-top: var(--spacing-lg);
}
.business-plan-summary h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin: var(--spacing-xl) 0 var(--spacing-md) 0;
}
.business-plan-summary h3:first-child {
margin-top: 0;
}

View File

@ -1,10 +1,12 @@
import { useState } from 'react'
import { useStorage } from '@/hooks/useStorage'
import { Project } from '@/types'
import { Project, BusinessPlan } from '@/types'
import { calculateYields, YieldsResult } from '@/utils/calculations/yields'
import { aggregateProjectBusinessPlans } from '@/utils/calculations/businessPlanAggregation'
import Card from '@/components/base/Card'
import Select from '@/components/base/Select'
import { formatNumber } from '@/utils/formatters'
import Table from '@/components/base/Table'
import { formatNumber, formatCurrency } from '@/utils/formatters'
import './YieldsPage.css'
export default function YieldsPage() {
@ -14,6 +16,14 @@ export default function YieldsPage() {
const projects = data?.projects || []
const wastes = data?.wastes || []
const treatmentSites = data?.treatmentSites || []
const services = data?.services || []
const transporters = data?.transporters || []
const moduleComponents = data?.moduleComponents || []
const ecosystems = data?.ecosystems || []
const regulators = data?.regulators || []
const wasteSites = data?.wasteSites || []
const administrativeProcedures = data?.administrativeProcedures || []
const companies = data?.companies || []
const selectedProject = projects.find((p) => p.id === selectedProjectId)
const selectedWaste = selectedProject?.wasteCharacteristicsOverride?.wasteId
@ -27,6 +37,22 @@ export default function YieldsPage() {
? calculateYields(selectedProject, selectedWaste, selectedTreatmentSite)
: null
// Aggregate all business plans for the selected project
const aggregatedBusinessPlan: BusinessPlan | null = selectedProject
? aggregateProjectBusinessPlans(
selectedProject,
services,
transporters,
moduleComponents,
ecosystems,
regulators,
treatmentSites,
wasteSites,
administrativeProcedures,
companies
)
: null
const projectOptions = projects.map((project) => ({
value: project.id,
label: project.name,
@ -181,6 +207,98 @@ export default function YieldsPage() {
/>
</div>
</Card>
{aggregatedBusinessPlan && (
<Card title="Aggregated Business Plan (10 Years)" className="yields-card">
<div className="business-plan-summary">
<h3>Revenues (/year)</h3>
<Table
columns={[
{ key: 'year', header: 'Year', render: (_: any, idx: number) => `Year ${idx + 1}` },
{ key: 'rawRental', header: 'Raw Rental', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.revenues.rawRental[idx]) },
{ key: 'biologicalTreatment', header: 'Biological Treatment', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.revenues.biologicalTreatment[idx]) },
{ key: 'bitcoinManagement', header: 'Bitcoin Management', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.revenues.bitcoinManagement[idx]) },
{ key: 'fertilizers', header: 'Fertilizers', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.revenues.fertilizers[idx]) },
{ key: 'wasteHeat', header: 'Waste Heat', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.revenues.wasteHeat[idx]) },
{ key: 'carbonCredits', header: 'Carbon Credits', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.revenues.carbonCredits[idx]) },
{ key: 'brownfield', header: 'Brownfield', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.revenues.brownfield[idx]) },
{ key: 'transport', header: 'Transport', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.revenues.transport[idx]) },
{ key: 'commercialPartnerships', header: 'Commercial Partnerships', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.revenues.commercialPartnerships[idx]) },
{ key: 'other', header: 'Other', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.revenues.other[idx]) },
{
key: 'total',
header: 'Total Revenues',
render: (_: any, idx: number) => {
const total =
aggregatedBusinessPlan.revenues.rawRental[idx] +
aggregatedBusinessPlan.revenues.biologicalTreatment[idx] +
aggregatedBusinessPlan.revenues.bitcoinManagement[idx] +
aggregatedBusinessPlan.revenues.fertilizers[idx] +
aggregatedBusinessPlan.revenues.wasteHeat[idx] +
aggregatedBusinessPlan.revenues.carbonCredits[idx] +
aggregatedBusinessPlan.revenues.brownfield[idx] +
aggregatedBusinessPlan.revenues.transport[idx] +
aggregatedBusinessPlan.revenues.commercialPartnerships[idx] +
aggregatedBusinessPlan.revenues.other[idx]
return <strong>{formatCurrency(total)}</strong>
},
},
]}
data={Array(10).fill(null)}
/>
<h3>Variable Costs (/year)</h3>
<Table
columns={[
{ key: 'year', header: 'Year', render: (_: any, idx: number) => `Year ${idx + 1}` },
{ key: 'rentalServices', header: 'Rental Services', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.variableCosts.rentalServices[idx]) },
{ key: 'commissions', header: 'Commissions', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.variableCosts.commissions[idx]) },
{ key: 'otherVariable', header: 'Other Variable', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.variableCosts.otherVariable[idx]) },
{ key: 'transport', header: 'Transport', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.variableCosts.transport[idx]) },
{
key: 'total',
header: 'Total Variable Costs',
render: (_: any, idx: number) => {
const total =
aggregatedBusinessPlan.variableCosts.rentalServices[idx] +
aggregatedBusinessPlan.variableCosts.commissions[idx] +
aggregatedBusinessPlan.variableCosts.otherVariable[idx] +
aggregatedBusinessPlan.variableCosts.transport[idx]
return <strong>{formatCurrency(total)}</strong>
},
},
]}
data={Array(10).fill(null)}
/>
<h3>Fixed Costs (/year)</h3>
<Table
columns={[
{ key: 'year', header: 'Year', render: (_: any, idx: number) => `Year ${idx + 1}` },
{ key: 'salaries', header: 'Salaries', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.fixedCosts.salaries[idx]) },
{ key: 'marketing', header: 'Marketing', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.fixedCosts.marketing[idx]) },
{ key: 'rd', header: 'R&D', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.fixedCosts.rd[idx]) },
{ key: 'administrative', header: 'Administrative', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.fixedCosts.administrative[idx]) },
{ key: 'otherGeneral', header: 'Other General', render: (_: any, idx: number) => formatCurrency(aggregatedBusinessPlan.fixedCosts.otherGeneral[idx]) },
{
key: 'total',
header: 'Total Fixed Costs',
render: (_: any, idx: number) => {
const total =
aggregatedBusinessPlan.fixedCosts.salaries[idx] +
aggregatedBusinessPlan.fixedCosts.marketing[idx] +
aggregatedBusinessPlan.fixedCosts.rd[idx] +
aggregatedBusinessPlan.fixedCosts.administrative[idx] +
aggregatedBusinessPlan.fixedCosts.otherGeneral[idx]
return <strong>{formatCurrency(total)}</strong>
},
},
]}
data={Array(10).fill(null)}
/>
</div>
</Card>
)}
</div>
)}
</div>

View File

@ -51,6 +51,7 @@ export default function EcosystemsConfigurationPage() {
const ecosystems = data?.ecosystems || []
const regulators = data?.regulators || []
const wastes = data?.wastes || []
const regulationCharacteristics = data?.regulationCharacteristics || []
const regulatorOptions = regulators.map((reg) => ({
value: reg.id,
@ -117,6 +118,7 @@ export default function EcosystemsConfigurationPage() {
name: formData.name!,
description: formData.description,
primaryRegulationNeeds: formData.primaryRegulationNeeds as [string, string, string],
regulationCharacteristicIds: formData.regulationCharacteristicIds || [],
regulators: formData.regulators || [],
compatibleWasteTypes: formData.compatibleWasteTypes || [],
effectiveness: formData.effectiveness || {},

View File

@ -189,3 +189,20 @@
border: 1px solid var(--border);
border-radius: 8px;
}
.procedures-list {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
margin-top: var(--spacing-md);
}
.procedure-association-item {
display: grid;
grid-template-columns: 2fr 1fr 2fr auto;
gap: var(--spacing-md);
align-items: flex-end;
padding: var(--spacing-sm);
border: 1px solid var(--border);
border-radius: 8px;
}

View File

@ -1,6 +1,6 @@
import { useState } from 'react'
import { useStorage } from '@/hooks/useStorage'
import { ModuleComponent, ModuleComponentType, MonthlyValue, Dimensions, PowerSpecs, ProductionPowerSpecs, ModuleComponentRegulatorAssociation, ModuleComponentServiceAssociation } from '@/types'
import { ModuleComponent, ModuleComponentType, MonthlyValue, Dimensions, PowerSpecs, ProductionPowerSpecs, ModuleComponentRegulatorAssociation, ModuleComponentServiceAssociation, ModuleComponentProcedureAssociation, ProcedureStatus } from '@/types'
import Card from '@/components/base/Card'
import Button from '@/components/base/Button'
import Input from '@/components/base/Input'
@ -56,6 +56,7 @@ export default function ModuleComponentsConfigurationPage() {
durationUnit: 'days',
regulators: [],
services: [],
administrativeProcedures: [],
})
const [errors, setErrors] = useState<Record<string, string>>({})
@ -63,6 +64,7 @@ export default function ModuleComponentsConfigurationPage() {
const wastes = data?.wastes || []
const regulators = data?.regulators || []
const services = data?.services || []
const administrativeProcedures = data?.administrativeProcedures || []
const selectedComponentType = COMPONENT_TYPES.find(t => t.value === formData.type)
@ -134,6 +136,8 @@ export default function ModuleComponentsConfigurationPage() {
lifetime: formData.lifetime,
regulators: formData.regulators || [],
services: formData.services || [],
administrativeProcedures: formData.administrativeProcedures || [],
regulationCharacteristicIds: formData.regulationCharacteristicIds || [],
createdAt: editingId ? components.find((c) => c.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
@ -264,6 +268,28 @@ export default function ModuleComponentsConfigurationPage() {
)
},
},
{
key: 'procedures',
header: 'Procedures',
render: (component: ModuleComponent) => {
if (!component.administrativeProcedures || component.administrativeProcedures.length === 0) {
return <span style={{ color: 'var(--text-secondary)' }}>None</span>
}
return (
<div>
{component.administrativeProcedures.map((assoc, idx) => {
const procedure = administrativeProcedures.find((p) => p.id === assoc.procedureId)
const procedureStatusVariant = assoc.status === 'done' ? 'success' : assoc.status === 'toDo' ? 'warning' : 'info'
return (
<Badge key={idx} variant={procedureStatusVariant} style={{ marginRight: '4px' }}>
{procedure?.name || 'Unknown'}
</Badge>
)
})}
</div>
)
},
},
{
key: 'actions',
header: 'Actions',
@ -875,6 +901,133 @@ export default function ModuleComponentsConfigurationPage() {
</div>
</div>
{/* Regulation Characteristics Section */}
<div className="form-section">
<h3 className="form-section-title">Regulation Characteristics</h3>
<p className="form-help-text">
Select regulation characteristics that this module component addresses. Configure characteristics in Regulation Characteristics page.
</p>
<div className="checkbox-grid">
{regulationCharacteristics.map((char) => (
<label key={char.id} className="checkbox-item">
<input
type="checkbox"
checked={formData.regulationCharacteristicIds?.includes(char.id) || false}
onChange={(e) => {
const currentIds = formData.regulationCharacteristicIds || []
if (e.target.checked) {
setFormData({ ...formData, regulationCharacteristicIds: [...currentIds, char.id] })
} else {
setFormData({ ...formData, regulationCharacteristicIds: currentIds.filter((id) => id !== char.id) })
}
}}
/>
<span>{char.name} ({char.category})</span>
</label>
))}
{regulationCharacteristics.length === 0 && (
<p className="form-help-text">No regulation characteristics configured. Add them in the Regulation Characteristics page.</p>
)}
</div>
</div>
{/* Associated Administrative Procedures Section */}
<div className="form-section">
<h3 className="form-section-title">Associated Administrative Procedures</h3>
<p className="form-help-text">
Add administrative procedures for this module component. Track the status of each procedure.
</p>
<div className="procedures-list">
{(formData.administrativeProcedures || []).map((association, index) => {
const procedure = administrativeProcedures.find((p) => p.id === association.procedureId)
const procedureStatusOptions = [
{ value: 'toDo', label: 'To Do' },
{ value: 'done', label: 'Done' },
{ value: 'na', label: 'N/A' },
]
return (
<div key={index} className="procedure-association-item">
<Select
label="Procedure"
value={association.procedureId || ''}
onChange={(e) => {
const updatedProcedures = [...(formData.administrativeProcedures || [])]
updatedProcedures[index] = {
...updatedProcedures[index],
procedureId: e.target.value,
}
setFormData({ ...formData, administrativeProcedures: updatedProcedures })
}}
options={[
{ value: '', label: 'Select a procedure' },
...administrativeProcedures.map((p) => ({
value: p.id,
label: `${p.name} (${p.type})`,
})).filter(
(opt) =>
opt.value === association.procedureId ||
!(formData.administrativeProcedures || []).some((a) => a.procedureId === opt.value)
),
]}
/>
<Select
label="Status"
value={association.status || 'toDo'}
onChange={(e) => {
const updatedProcedures = [...(formData.administrativeProcedures || [])]
updatedProcedures[index] = {
...updatedProcedures[index],
status: e.target.value as ProcedureStatus,
}
setFormData({ ...formData, administrativeProcedures: updatedProcedures })
}}
options={procedureStatusOptions}
/>
<Input
label="Notes (optional)"
value={association.notes || ''}
onChange={(e) => {
const updatedProcedures = [...(formData.administrativeProcedures || [])]
updatedProcedures[index] = {
...updatedProcedures[index],
notes: e.target.value || undefined,
}
setFormData({ ...formData, administrativeProcedures: updatedProcedures })
}}
helpText={procedure ? `Type: ${procedure.type}, Delays: ${procedure.delays} days` : undefined}
/>
<Button
type="button"
variant="danger"
onClick={() => {
const updatedProcedures = (formData.administrativeProcedures || []).filter((_, i) => i !== index)
setFormData({ ...formData, administrativeProcedures: updatedProcedures })
}}
>
Remove
</Button>
</div>
)
})}
<Button
type="button"
variant="secondary"
onClick={() => {
const newAssociation: ModuleComponentProcedureAssociation = {
procedureId: '',
status: 'toDo',
}
setFormData({
...formData,
administrativeProcedures: [...(formData.administrativeProcedures || []), newAssociation],
})
}}
>
Add Procedure
</Button>
</div>
</div>
{/* Costs Section */}
<div className="form-section">
<h3 className="form-section-title">Costs (Annual)</h3>

View File

@ -0,0 +1,53 @@
.regulation-characteristics-config-page {
max-width: 1400px;
margin: 0 auto;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--spacing-2xl);
}
.page-content {
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
}
.form-card {
max-width: 100%;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-md);
}
.form-actions {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-xl);
padding-top: var(--spacing-xl);
border-top: 1px solid var(--border);
}
.table-card {
margin-top: var(--spacing-xl);
}
.table-actions {
display: flex;
gap: var(--spacing-sm);
}
code {
background-color: var(--background-secondary);
padding: 2px 6px;
border-radius: 4px;
font-family: var(--font-mono);
font-size: 0.875rem;
}

View File

@ -0,0 +1,258 @@
import { useState } from 'react'
import { useStorage } from '@/hooks/useStorage'
import { RegulationCharacteristic } 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 { validateRequired } from '@/utils/validators'
import './RegulationCharacteristicsConfigurationPage.css'
export default function RegulationCharacteristicsConfigurationPage() {
const { data, addEntity, updateEntity, deleteEntity } = useStorage()
const [editingId, setEditingId] = useState<string | null>(null)
const [formData, setFormData] = useState<Partial<RegulationCharacteristic>>({
name: '',
code: '',
category: 'other',
description: '',
unit: '',
isBoolean: true,
minValue: undefined,
maxValue: undefined,
})
const [errors, setErrors] = useState<Record<string, string>>({})
const characteristics = data?.regulationCharacteristics || []
const categoryOptions = [
{ value: 'nutrient', label: 'Nutrient' },
{ value: 'heavyMetal', label: 'Heavy Metal' },
{ value: 'biological', label: 'Biological' },
{ value: 'chemical', label: 'Chemical' },
{ value: 'biologicalProcess', label: 'Biological Process' },
{ value: 'ph', label: 'pH' },
{ value: 'other', label: 'Other' },
]
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const newErrors: Record<string, string> = {}
newErrors.name = validateRequired(formData.name)
newErrors.code = validateRequired(formData.code)
// Check for duplicate code
const existingWithCode = characteristics.find((c) => c.code === formData.code && c.id !== editingId)
if (existingWithCode) {
newErrors.code = 'A characteristic with this code already exists'
}
setErrors(newErrors)
if (Object.values(newErrors).some((error) => error !== undefined)) {
return
}
const characteristic: RegulationCharacteristic = {
id: editingId || crypto.randomUUID(),
name: formData.name!,
code: formData.code!,
category: formData.category!,
description: formData.description,
unit: formData.unit,
isBoolean: formData.isBoolean ?? true,
minValue: formData.minValue,
maxValue: formData.maxValue,
createdAt: editingId ? characteristics.find((c) => c.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
if (editingId) {
updateEntity('regulationCharacteristics', editingId, characteristic)
} else {
addEntity('regulationCharacteristics', characteristic)
}
resetForm()
}
const resetForm = () => {
setFormData({
name: '',
code: '',
category: 'other',
description: '',
unit: '',
isBoolean: true,
minValue: undefined,
maxValue: undefined,
})
setEditingId(null)
setErrors({})
}
const handleEdit = (characteristic: RegulationCharacteristic) => {
setFormData(characteristic)
setEditingId(characteristic.id)
}
const handleDelete = (id: string) => {
if (confirm('Are you sure you want to delete this regulation characteristic?')) {
deleteEntity('regulationCharacteristics', id)
}
}
const tableColumns = [
{
key: 'name',
header: 'Name',
render: (char: RegulationCharacteristic) => char.name,
},
{
key: 'code',
header: 'Code',
render: (char: RegulationCharacteristic) => <code>{char.code}</code>,
},
{
key: 'category',
header: 'Category',
render: (char: RegulationCharacteristic) => {
const categoryLabel = categoryOptions.find((opt) => opt.value === char.category)?.label || char.category
return categoryLabel
},
},
{
key: 'type',
header: 'Type',
render: (char: RegulationCharacteristic) => (char.isBoolean ? 'Boolean' : 'Numeric'),
},
{
key: 'unit',
header: 'Unit',
render: (char: RegulationCharacteristic) => char.unit || '-',
},
{
key: 'range',
header: 'Range',
render: (char: RegulationCharacteristic) => {
if (char.isBoolean) return '-'
if (char.minValue !== undefined && char.maxValue !== undefined) {
return `${char.minValue} - ${char.maxValue}`
}
if (char.minValue !== undefined) return `${char.minValue}`
if (char.maxValue !== undefined) return `${char.maxValue}`
return '-'
},
},
{
key: 'actions',
header: 'Actions',
render: (char: RegulationCharacteristic) => (
<div className="table-actions">
<Button variant="secondary" onClick={() => handleEdit(char)}>
Edit
</Button>
<Button variant="danger" onClick={() => handleDelete(char.id)}>
Delete
</Button>
</div>
),
},
]
return (
<div className="regulation-characteristics-config-page">
<h1 className="page-title">Regulation Characteristics Configuration</h1>
<div className="page-content">
<Card title={editingId ? 'Edit Regulation Characteristic' : 'Add Regulation Characteristic'} 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}
/>
<Input
label="Code *"
value={formData.code || ''}
onChange={(e) => setFormData({ ...formData, code: e.target.value })}
error={errors.code}
helpText="Unique identifier (e.g., 'pathogenElimination', 'heavyMetalsElimination')"
/>
</div>
<div className="form-row">
<Select
label="Category *"
value={formData.category || 'other'}
onChange={(e) => setFormData({ ...formData, category: e.target.value as RegulationCharacteristic['category'] })}
options={categoryOptions}
/>
<Select
label="Type *"
value={formData.isBoolean ? 'boolean' : 'numeric'}
onChange={(e) => setFormData({ ...formData, isBoolean: e.target.value === 'boolean' })}
options={[
{ value: 'boolean', label: 'Boolean (Capability/Need)' },
{ value: 'numeric', label: 'Numeric (Value)' },
]}
/>
</div>
<Input
label="Description"
value={formData.description || ''}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
multiline
rows={3}
/>
{!formData.isBoolean && (
<div className="form-row">
<Input
label="Unit"
value={formData.unit || ''}
onChange={(e) => setFormData({ ...formData, unit: e.target.value })}
helpText="Unit of measurement (e.g., 'kg/t', '%', 'pH')"
/>
<Input
label="Min Value"
type="number"
step="0.01"
value={formData.minValue !== undefined ? formData.minValue : ''}
onChange={(e) => setFormData({ ...formData, minValue: e.target.value ? parseFloat(e.target.value) : undefined })}
/>
<Input
label="Max Value"
type="number"
step="0.01"
value={formData.maxValue !== undefined ? formData.maxValue : ''}
onChange={(e) => setFormData({ ...formData, maxValue: e.target.value ? parseFloat(e.target.value) : undefined })}
/>
</div>
)}
<div className="form-actions">
<Button type="submit" variant="primary">
{editingId ? 'Update' : 'Add'} Characteristic
</Button>
{editingId && (
<Button type="button" variant="secondary" onClick={resetForm}>
Cancel
</Button>
)}
</div>
</form>
</Card>
<Card title="Regulation Characteristics" className="table-card">
<Table columns={tableColumns} data={characteristics} emptyMessage="No regulation characteristics configured" />
</Card>
</div>
</div>
)
}

View File

@ -11,6 +11,7 @@ import './RegulatorsConfigurationPage.css'
export default function RegulatorsConfigurationPage() {
const { data, addEntity, updateEntity, deleteEntity, loading } = useStorage()
const regulationCharacteristics = data?.regulationCharacteristics || []
const [editingId, setEditingId] = useState<string | null>(null)
const [formData, setFormData] = useState<Partial<NaturalRegulator>>({
name: '',
@ -71,11 +72,12 @@ export default function RegulatorsConfigurationPage() {
const regulator: NaturalRegulator = {
id: editingId || crypto.randomUUID(),
name: formData.name!,
type: formData.type!,
regulatoryCharacteristics: formData.regulatoryCharacteristics || {},
applicationConditions: formData.applicationConditions!,
dosageRequirements: formData.dosageRequirements!,
createdAt: editingId ? regulators.find((r) => r.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
type: formData.type!,
regulationCharacteristicIds: formData.regulationCharacteristicIds || [],
regulatoryCharacteristics: formData.regulatoryCharacteristics || {},
applicationConditions: formData.applicationConditions!,
dosageRequirements: formData.dosageRequirements!,
createdAt: editingId ? regulators.find((r) => r.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
updatedAt: new Date().toISOString(),
}

View File

@ -29,6 +29,7 @@ export default function WasteConfigurationPage() {
const wastes = data?.wastes || []
const wasteOrigins = data?.wasteOrigins || []
const regulators = data?.regulators || []
const regulationCharacteristics = data?.regulationCharacteristics || []
const originTypeOptions = [
{ value: 'animals', label: 'Animals' },
@ -71,11 +72,12 @@ export default function WasteConfigurationPage() {
wasteOriginId: formData.wasteOriginId,
originUnitsPer1000m3Methane: formData.originUnitsPer1000m3Methane!,
bmp: formData.bmp!,
waterPercentage: formData.waterPercentage!,
regulationNeeds: formData.regulationNeeds || [],
regulatoryCharacteristics: formData.regulatoryCharacteristics,
regulators: formData.regulators || [],
maxStorageDuration: formData.maxStorageDuration!,
waterPercentage: formData.waterPercentage!,
regulationNeeds: formData.regulationNeeds || [],
regulationCharacteristicIds: formData.regulationCharacteristicIds || [],
regulatoryCharacteristics: formData.regulatoryCharacteristics,
regulators: formData.regulators || [],
maxStorageDuration: formData.maxStorageDuration!,
createdAt: editingId ? wastes.find((w) => w.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
updatedAt: new Date().toISOString(),
}

View File

@ -0,0 +1,273 @@
import { useState, useEffect } from 'react'
import { useStorage } from '@/hooks/useStorage'
import { Company, BusinessPlan } from '@/types'
import Card from '@/components/base/Card'
import Button from '@/components/base/Button'
import Input from '@/components/base/Input'
import Table from '@/components/base/Table'
import { validateRequired } from '@/utils/validators'
import { initializeBusinessPlan } from '@/utils/calculations/businessPlanAggregation'
import BusinessPlanEditor from '@/components/business-plan/BusinessPlanEditor'
import './CompaniesPage.css'
export default function CompaniesPage() {
const { data, addEntity, updateEntity, deleteEntity } = useStorage()
const [editingId, setEditingId] = useState<string | null>(null)
const [formData, setFormData] = useState<Partial<Company>>({
name: '',
legalName: '',
registrationNumber: '',
address: '',
contact: {
email: '',
phone: '',
website: '',
},
})
const [errors, setErrors] = useState<Record<string, string>>({})
const [showBusinessPlan, setShowBusinessPlan] = useState(false)
const companies = data?.companies || []
// Initialize default company on first load
useEffect(() => {
if (companies.length === 0) {
const defaultCompany: Company = {
id: crypto.randomUUID(),
name: '4NK Water & Waste',
legalName: '4NK Water & Waste',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
businessPlan: initializeBusinessPlan(),
}
addEntity('companies', defaultCompany)
}
}, [])
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const newErrors: Record<string, string> = {}
newErrors.name = validateRequired(formData.name)
setErrors(newErrors)
if (Object.values(newErrors).some((error) => error !== undefined)) {
return
}
const company: Company = {
id: editingId || crypto.randomUUID(),
name: formData.name!,
legalName: formData.legalName,
registrationNumber: formData.registrationNumber,
address: formData.address,
contact: formData.contact,
businessPlan: formData.businessPlan || initializeBusinessPlan(),
createdAt: editingId ? companies.find((c) => c.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
if (editingId) {
updateEntity('companies', editingId, company)
} else {
addEntity('companies', company)
}
resetForm()
}
const resetForm = () => {
setFormData({
name: '',
legalName: '',
registrationNumber: '',
address: '',
contact: {
email: '',
phone: '',
website: '',
},
})
setEditingId(null)
setErrors({})
setShowBusinessPlan(false)
}
const handleEdit = (company: Company) => {
setFormData(company)
setEditingId(company.id)
setShowBusinessPlan(false)
}
const handleDelete = (id: string) => {
if (confirm('Are you sure you want to delete this company?')) {
deleteEntity('companies', id)
}
}
const updateBusinessPlan = (businessPlan: BusinessPlan) => {
setFormData({
...formData,
businessPlan,
})
}
const tableColumns = [
{
key: 'name',
header: 'Name',
render: (company: Company) => company.name,
},
{
key: 'legalName',
header: 'Legal Name',
render: (company: Company) => company.legalName || '-',
},
{
key: 'registrationNumber',
header: 'Registration Number',
render: (company: Company) => company.registrationNumber || '-',
},
{
key: 'hasBusinessPlan',
header: 'Business Plan',
render: (company: Company) => (company.businessPlan ? 'Yes' : 'No'),
},
{
key: 'actions',
header: 'Actions',
render: (company: Company) => (
<div className="table-actions">
<Button variant="secondary" onClick={() => handleEdit(company)}>
Edit
</Button>
<Button variant="danger" onClick={() => handleDelete(company.id)}>
Delete
</Button>
</div>
),
},
]
return (
<div className="companies-page">
<h1 className="page-title">Companies</h1>
<div className="page-content">
<Card title={editingId ? 'Edit Company' : 'Add Company'} 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}
/>
<Input
label="Legal Name"
value={formData.legalName || ''}
onChange={(e) => setFormData({ ...formData, legalName: e.target.value })}
/>
</div>
<div className="form-row">
<Input
label="Registration Number"
value={formData.registrationNumber || ''}
onChange={(e) => setFormData({ ...formData, registrationNumber: e.target.value })}
/>
<Input
label="Address"
value={formData.address || ''}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
/>
</div>
<div className="form-section">
<h3 className="form-section-title">Contact Information</h3>
<div className="form-row">
<Input
label="Email"
type="email"
value={formData.contact?.email || ''}
onChange={(e) =>
setFormData({
...formData,
contact: {
...formData.contact!,
email: e.target.value,
},
})
}
/>
<Input
label="Phone"
value={formData.contact?.phone || ''}
onChange={(e) =>
setFormData({
...formData,
contact: {
...formData.contact!,
phone: e.target.value,
},
})
}
/>
</div>
<Input
label="Website"
type="url"
value={formData.contact?.website || ''}
onChange={(e) =>
setFormData({
...formData,
contact: {
...formData.contact!,
website: e.target.value,
},
})
}
/>
</div>
<div className="form-section">
<div className="form-section-header">
<h3 className="form-section-title">Business Plan</h3>
<Button
type="button"
variant="secondary"
onClick={() => setShowBusinessPlan(!showBusinessPlan)}
>
{showBusinessPlan ? 'Hide' : 'Show'} Business Plan
</Button>
</div>
{showBusinessPlan && formData.businessPlan && (
<BusinessPlanEditor
businessPlan={formData.businessPlan}
onChange={updateBusinessPlan}
/>
)}
</div>
<div className="form-actions">
<Button type="submit" variant="primary">
{editingId ? 'Update' : 'Add'} Company
</Button>
{editingId && (
<Button type="button" variant="secondary" onClick={resetForm}>
Cancel
</Button>
)}
</div>
</form>
</Card>
<Card title="Companies" className="table-card">
<Table columns={tableColumns} data={companies} emptyMessage="No companies configured" />
</Card>
</div>
</div>
)
}

View File

@ -110,3 +110,22 @@
cursor: pointer;
accent-color: var(--primary-green);
}
.investors-list,
.procedures-list {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
margin-top: var(--spacing-md);
}
.investor-association-item,
.procedure-association-item {
display: grid;
grid-template-columns: 2fr 1fr 1fr 2fr auto;
gap: var(--spacing-md);
align-items: flex-end;
padding: var(--spacing-sm);
border: 1px solid var(--border);
border-radius: 8px;
}

View File

@ -1,6 +1,6 @@
import { useState } from 'react'
import { useStorage } from '@/hooks/useStorage'
import { TreatmentSite, SiteStatus, SiteTransporterAssociation } from '@/types'
import { TreatmentSite, SiteStatus, SiteTransporterAssociation, TreatmentSiteInvestorAssociation, TreatmentSiteProcedureAssociation, ProcedureStatus } from '@/types'
import Card from '@/components/base/Card'
import Button from '@/components/base/Button'
import Input from '@/components/base/Input'
@ -21,12 +21,16 @@ export default function TreatmentSitesPage() {
monthlyTemperatures: Array(12).fill(0),
subscribedServices: [],
transporters: [],
investors: [],
administrativeProcedures: [],
})
const [errors, setErrors] = useState<Record<string, string>>({})
const sites = data?.treatmentSites || []
const services = data?.services || []
const transporters = data?.transporters || []
const investors = data?.investors || []
const administrativeProcedures = data?.administrativeProcedures || []
const statusOptions = [
{ value: 'toBeApproached', label: 'To be approached' },
@ -64,6 +68,8 @@ export default function TreatmentSitesPage() {
monthlyTemperatures: formData.monthlyTemperatures!,
subscribedServices: formData.subscribedServices || [],
transporters: formData.transporters || [],
investors: formData.investors || [],
administrativeProcedures: formData.administrativeProcedures || [],
createdAt: editingId ? sites.find((s) => s.id === editingId)?.createdAt || new Date().toISOString() : new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
@ -86,6 +92,8 @@ export default function TreatmentSitesPage() {
monthlyTemperatures: Array(12).fill(0),
subscribedServices: [],
transporters: [],
investors: [],
administrativeProcedures: [],
})
setEditingId(null)
setErrors({})
@ -155,6 +163,49 @@ export default function TreatmentSitesPage() {
)
},
},
{
key: 'investors',
header: 'Investors',
render: (site: TreatmentSite) => {
if (!site.investors || site.investors.length === 0) {
return <span style={{ color: 'var(--text-secondary)' }}>None</span>
}
return (
<div>
{site.investors.map((assoc, idx) => {
const investor = investors.find((i) => i.id === assoc.investorId)
return (
<Badge key={idx} variant={getStatusVariant(assoc.status)} style={{ marginRight: '4px' }}>
{investor?.name || 'Unknown'}
</Badge>
)
})}
</div>
)
},
},
{
key: 'procedures',
header: 'Procedures',
render: (site: TreatmentSite) => {
if (!site.administrativeProcedures || site.administrativeProcedures.length === 0) {
return <span style={{ color: 'var(--text-secondary)' }}>None</span>
}
return (
<div>
{site.administrativeProcedures.map((assoc, idx) => {
const procedure = administrativeProcedures.find((p) => p.id === assoc.procedureId)
const procedureStatusVariant = assoc.status === 'done' ? 'success' : assoc.status === 'toDo' ? 'warning' : 'info'
return (
<Badge key={idx} variant={procedureStatusVariant} style={{ marginRight: '4px' }}>
{procedure?.name || 'Unknown'}
</Badge>
)
})}
</div>
)
},
},
{
key: 'actions',
header: 'Actions',
@ -340,6 +391,210 @@ export default function TreatmentSitesPage() {
</div>
</div>
{/* Associated Investors Section */}
<div className="form-section">
<h3 className="form-section-title">Associated Investors</h3>
<p className="form-help-text">
Add investors for this treatment site. Specify the investment status and amount if known.
</p>
<div className="investors-list">
{(formData.investors || []).map((association, index) => {
const investor = investors.find((i) => i.id === association.investorId)
return (
<div key={index} className="investor-association-item">
<Select
label="Investor"
value={association.investorId || ''}
onChange={(e) => {
const updatedInvestors = [...(formData.investors || [])]
updatedInvestors[index] = {
...updatedInvestors[index],
investorId: e.target.value,
}
setFormData({ ...formData, investors: updatedInvestors })
}}
options={[
{ value: '', label: 'Select an investor' },
...investors.map((i) => ({
value: i.id,
label: `${i.name} (${i.type})`,
})).filter(
(opt) =>
opt.value === association.investorId ||
!(formData.investors || []).some((a) => a.investorId === opt.value)
),
]}
/>
<Select
label="Status"
value={association.status || 'toBeApproached'}
onChange={(e) => {
const updatedInvestors = [...(formData.investors || [])]
updatedInvestors[index] = {
...updatedInvestors[index],
status: e.target.value as 'toBeApproached' | 'loiOk' | 'inProgress' | 'completed',
}
setFormData({ ...formData, investors: updatedInvestors })
}}
options={statusOptions}
/>
<Input
label="Amount (€, optional)"
type="number"
min="0"
step="0.01"
value={association.amount || ''}
onChange={(e) => {
const updatedInvestors = [...(formData.investors || [])]
updatedInvestors[index] = {
...updatedInvestors[index],
amount: e.target.value ? parseFloat(e.target.value) : undefined,
}
setFormData({ ...formData, investors: updatedInvestors })
}}
helpText={investor ? `Range: ${investor.amountRange.min.toLocaleString()} - ${investor.amountRange.max.toLocaleString()}` : undefined}
/>
<Input
label="Notes (optional)"
value={association.notes || ''}
onChange={(e) => {
const updatedInvestors = [...(formData.investors || [])]
updatedInvestors[index] = {
...updatedInvestors[index],
notes: e.target.value || undefined,
}
setFormData({ ...formData, investors: updatedInvestors })
}}
/>
<Button
type="button"
variant="danger"
onClick={() => {
const updatedInvestors = (formData.investors || []).filter((_, i) => i !== index)
setFormData({ ...formData, investors: updatedInvestors })
}}
>
Remove
</Button>
</div>
)
})}
<Button
type="button"
variant="secondary"
onClick={() => {
const newAssociation: TreatmentSiteInvestorAssociation = {
investorId: '',
status: 'toBeApproached',
}
setFormData({
...formData,
investors: [...(formData.investors || []), newAssociation],
})
}}
>
Add Investor
</Button>
</div>
</div>
{/* Associated Administrative Procedures Section */}
<div className="form-section">
<h3 className="form-section-title">Associated Administrative Procedures</h3>
<p className="form-help-text">
Add administrative procedures for this treatment site. Track the status of each procedure.
</p>
<div className="procedures-list">
{(formData.administrativeProcedures || []).map((association, index) => {
const procedure = administrativeProcedures.find((p) => p.id === association.procedureId)
const procedureStatusOptions = [
{ value: 'toDo', label: 'To Do' },
{ value: 'done', label: 'Done' },
{ value: 'na', label: 'N/A' },
]
return (
<div key={index} className="procedure-association-item">
<Select
label="Procedure"
value={association.procedureId || ''}
onChange={(e) => {
const updatedProcedures = [...(formData.administrativeProcedures || [])]
updatedProcedures[index] = {
...updatedProcedures[index],
procedureId: e.target.value,
}
setFormData({ ...formData, administrativeProcedures: updatedProcedures })
}}
options={[
{ value: '', label: 'Select a procedure' },
...administrativeProcedures.map((p) => ({
value: p.id,
label: `${p.name} (${p.type})`,
})).filter(
(opt) =>
opt.value === association.procedureId ||
!(formData.administrativeProcedures || []).some((a) => a.procedureId === opt.value)
),
]}
/>
<Select
label="Status"
value={association.status || 'toDo'}
onChange={(e) => {
const updatedProcedures = [...(formData.administrativeProcedures || [])]
updatedProcedures[index] = {
...updatedProcedures[index],
status: e.target.value as ProcedureStatus,
}
setFormData({ ...formData, administrativeProcedures: updatedProcedures })
}}
options={procedureStatusOptions}
/>
<Input
label="Notes (optional)"
value={association.notes || ''}
onChange={(e) => {
const updatedProcedures = [...(formData.administrativeProcedures || [])]
updatedProcedures[index] = {
...updatedProcedures[index],
notes: e.target.value || undefined,
}
setFormData({ ...formData, administrativeProcedures: updatedProcedures })
}}
helpText={procedure ? `Type: ${procedure.type}, Delays: ${procedure.delays} days` : undefined}
/>
<Button
type="button"
variant="danger"
onClick={() => {
const updatedProcedures = (formData.administrativeProcedures || []).filter((_, i) => i !== index)
setFormData({ ...formData, administrativeProcedures: updatedProcedures })
}}
>
Remove
</Button>
</div>
)
})}
<Button
type="button"
variant="secondary"
onClick={() => {
const newAssociation: TreatmentSiteProcedureAssociation = {
procedureId: '',
status: 'toDo',
}
setFormData({
...formData,
administrativeProcedures: [...(formData.administrativeProcedures || []), newAssociation],
})
}}
>
Add Procedure
</Button>
</div>
</div>
<div className="form-actions">
<Button type="submit" variant="primary">
{editingId ? 'Update' : 'Add'} Treatment Site

View File

@ -31,6 +31,7 @@ export interface Transporter extends BaseEntity {
phone?: string
address?: string
}
businessPlan?: BusinessPlan
}
// Waste Regulator Association
@ -53,12 +54,34 @@ export interface SiteTransporterAssociation {
notes?: string // Optional notes about this transporter association
}
// Treatment Site Investor Association
export interface TreatmentSiteInvestorAssociation {
investorId: string // Reference to Investor
status: 'toBeApproached' | 'loiOk' | 'inProgress' | 'completed' // Investment status
amount?: number // Optional investment amount (€)
notes?: string // Optional notes about this investor association
}
// Treatment Site Procedure Association
export interface TreatmentSiteProcedureAssociation {
procedureId: string // Reference to AdministrativeProcedure
status: 'toDo' | 'done' | 'na' // Procedure status
notes?: string // Optional notes about this procedure association
}
// Module Component Service Association
export interface ModuleComponentServiceAssociation {
serviceId: string // Reference to Service
notes?: string // Optional notes about this service association
}
// Module Component Procedure Association
export interface ModuleComponentProcedureAssociation {
procedureId: string // Reference to AdministrativeProcedure
status: 'toDo' | 'done' | 'na' // Procedure status
notes?: string // Optional notes about this procedure association
}
// Waste
export interface Waste extends BaseEntity {
name: string
@ -119,6 +142,7 @@ export interface Waste extends BaseEntity {
export interface NaturalRegulator extends BaseEntity {
name: string
type: string
regulationCharacteristicIds?: string[] // References to RegulationCharacteristic entities
regulatoryCharacteristics?: {
// Nutrient Requirements
nitrogen?: number // Total Nitrogen (N or NTK)
@ -169,6 +193,7 @@ export interface NaturalRegulator extends BaseEntity {
max: number
unit: 'kg/t' | 'L/t' | '%'
}
businessPlan?: BusinessPlan
}
// Service
@ -197,6 +222,7 @@ export interface Service extends BaseEntity {
year9: number
year10: number
}
businessPlan?: BusinessPlan
}
// Treatment Site
@ -217,6 +243,9 @@ export interface TreatmentSite extends BaseEntity {
monthlyTemperatures: number[] // 12 values, °C
subscribedServices: string[] // service IDs
transporters?: SiteTransporterAssociation[] // Associated transporters
investors?: TreatmentSiteInvestorAssociation[] // Associated investors
administrativeProcedures?: TreatmentSiteProcedureAssociation[] // Associated administrative procedures
businessPlan?: BusinessPlan
}
// Waste Site
@ -238,6 +267,7 @@ export interface WasteSite extends BaseEntity {
collectionType: string
distance: number // km
transporters?: SiteTransporterAssociation[] // Associated transporters
businessPlan?: BusinessPlan
}
// Investor
@ -275,6 +305,7 @@ export interface AdministrativeProcedure extends BaseEntity {
organization?: string
}
regions: string[]
businessPlan?: BusinessPlan
}
// Project
@ -473,6 +504,10 @@ export interface ModuleComponent extends BaseEntity {
// Associated services
services?: ModuleComponentServiceAssociation[] // Associated services
// Associated administrative procedures
administrativeProcedures?: ModuleComponentProcedureAssociation[] // Associated administrative procedures
regulationCharacteristicIds?: string[] // References to RegulationCharacteristic entities
businessPlan?: BusinessPlan
}
// Ecosystem Regulator Association
@ -487,7 +522,8 @@ export interface Ecosystem extends BaseEntity {
name: string
description?: string
// Three primary regulation needs this ecosystem addresses
primaryRegulationNeeds: [string, string, string] // Exactly 3 regulation needs
primaryRegulationNeeds: [string, string, string] // Exactly 3 regulation needs (IDs from RegulationCharacteristic)
regulationCharacteristicIds?: string[] // References to RegulationCharacteristic entities
// Regulators that compose this ecosystem
regulators: EcosystemRegulatorAssociation[]
// Waste types that can benefit from this ecosystem (optional, empty means all)
@ -500,6 +536,33 @@ export interface Ecosystem extends BaseEntity {
applicationConditions?: string
// Estimated treatment duration (days)
treatmentDuration?: number
businessPlan?: BusinessPlan
}
// Regulation Characteristic
export interface RegulationCharacteristic extends BaseEntity {
name: string
code: string // Unique code identifier (e.g., 'pathogenElimination', 'heavyMetalsElimination')
category: 'nutrient' | 'heavyMetal' | 'biological' | 'chemical' | 'biologicalProcess' | 'ph' | 'other'
description?: string
unit?: string // Unit of measurement if applicable (e.g., 'kg/t', '%', 'pH')
isBoolean?: boolean // Whether this is a boolean characteristic (capability/need) or numeric
minValue?: number // Minimum value if numeric
maxValue?: number // Maximum value if numeric
}
// Company
export interface Company extends BaseEntity {
name: string
legalName?: string
registrationNumber?: string
address?: string
contact?: {
email?: string
phone?: string
website?: string
}
businessPlan?: BusinessPlan
}
// Storage structure
@ -514,9 +577,11 @@ export interface StorageData {
transporters: Transporter[]
moduleComponents: ModuleComponent[] // Added
ecosystems: Ecosystem[] // Added
regulationCharacteristics: RegulationCharacteristic[]
treatmentSites: TreatmentSite[]
wasteSites: WasteSite[]
investors: Investor[]
administrativeProcedures: AdministrativeProcedure[]
companies: Company[]
projects: Project[]
}

View File

@ -0,0 +1,196 @@
import { BusinessPlan, Project, Service, Transporter, ModuleComponent, Ecosystem, NaturalRegulator, TreatmentSite, WasteSite, AdministrativeProcedure, Company } from '@/types'
/**
* Initialize an empty business plan with all values set to 0
*/
export function initializeBusinessPlan(): BusinessPlan {
const years = Array(10).fill(0)
return {
revenues: {
rawRental: years,
biologicalTreatment: years,
bitcoinManagement: years,
fertilizers: years,
wasteHeat: years,
carbonCredits: years,
brownfield: years,
transport: years,
commercialPartnerships: years,
other: years,
},
variableCosts: {
rentalServices: years,
commissions: years,
otherVariable: years,
transport: years,
},
fixedCosts: {
salaries: years,
marketing: years,
rd: years,
administrative: years,
otherGeneral: years,
},
investments: {
equipment: years,
technology: years,
patents: years,
},
useOfFunds: {
productDevelopment: years,
marketing: years,
team: years,
structure: years,
},
kpis: {
activeUsers: years,
cac: years,
ltv: years,
breakEvenDays: years,
},
}
}
/**
* Aggregate two business plans by adding corresponding values
*/
function aggregateBusinessPlans(plan1: BusinessPlan, plan2: BusinessPlan): BusinessPlan {
const aggregateArray = (arr1: number[], arr2: number[]): number[] => {
return arr1.map((val, idx) => val + (arr2[idx] || 0))
}
return {
revenues: {
rawRental: aggregateArray(plan1.revenues.rawRental, plan2.revenues.rawRental),
biologicalTreatment: aggregateArray(plan1.revenues.biologicalTreatment, plan2.revenues.biologicalTreatment),
bitcoinManagement: aggregateArray(plan1.revenues.bitcoinManagement, plan2.revenues.bitcoinManagement),
fertilizers: aggregateArray(plan1.revenues.fertilizers, plan2.revenues.fertilizers),
wasteHeat: aggregateArray(plan1.revenues.wasteHeat, plan2.revenues.wasteHeat),
carbonCredits: aggregateArray(plan1.revenues.carbonCredits, plan2.revenues.carbonCredits),
brownfield: aggregateArray(plan1.revenues.brownfield, plan2.revenues.brownfield),
transport: aggregateArray(plan1.revenues.transport, plan2.revenues.transport),
commercialPartnerships: aggregateArray(plan1.revenues.commercialPartnerships, plan2.revenues.commercialPartnerships),
other: aggregateArray(plan1.revenues.other, plan2.revenues.other),
},
variableCosts: {
rentalServices: aggregateArray(plan1.variableCosts.rentalServices, plan2.variableCosts.rentalServices),
commissions: aggregateArray(plan1.variableCosts.commissions, plan2.variableCosts.commissions),
otherVariable: aggregateArray(plan1.variableCosts.otherVariable, plan2.variableCosts.otherVariable),
transport: aggregateArray(plan1.variableCosts.transport, plan2.variableCosts.transport),
},
fixedCosts: {
salaries: aggregateArray(plan1.fixedCosts.salaries, plan2.fixedCosts.salaries),
marketing: aggregateArray(plan1.fixedCosts.marketing, plan2.fixedCosts.marketing),
rd: aggregateArray(plan1.fixedCosts.rd, plan2.fixedCosts.rd),
administrative: aggregateArray(plan1.fixedCosts.administrative, plan2.fixedCosts.administrative),
otherGeneral: aggregateArray(plan1.fixedCosts.otherGeneral, plan2.fixedCosts.otherGeneral),
},
investments: {
equipment: aggregateArray(plan1.investments.equipment, plan2.investments.equipment),
technology: aggregateArray(plan1.investments.technology, plan2.investments.technology),
patents: aggregateArray(plan1.investments.patents, plan2.investments.patents),
},
useOfFunds: {
productDevelopment: aggregateArray(plan1.useOfFunds.productDevelopment, plan2.useOfFunds.productDevelopment),
marketing: aggregateArray(plan1.useOfFunds.marketing, plan2.useOfFunds.marketing),
team: aggregateArray(plan1.useOfFunds.team, plan2.useOfFunds.team),
structure: aggregateArray(plan1.useOfFunds.structure, plan2.useOfFunds.structure),
},
kpis: {
activeUsers: aggregateArray(plan1.kpis.activeUsers, plan2.kpis.activeUsers),
cac: aggregateArray(plan1.kpis.cac, plan2.kpis.cac),
ltv: aggregateArray(plan1.kpis.ltv, plan2.kpis.ltv),
breakEvenDays: aggregateArray(plan1.kpis.breakEvenDays, plan2.kpis.breakEvenDays),
},
}
}
/**
* Aggregate all business plans from a project and its associated entities
*/
export function aggregateProjectBusinessPlans(
project: Project,
services: Service[],
transporters: Transporter[],
moduleComponents: ModuleComponent[],
ecosystems: Ecosystem[],
regulators: NaturalRegulator[],
treatmentSites: TreatmentSite[],
wasteSites: WasteSite[],
administrativeProcedures: AdministrativeProcedure[],
companies: Company[]
): BusinessPlan {
let aggregated = initializeBusinessPlan()
// Add project's business plan
if (project.businessPlan) {
aggregated = aggregateBusinessPlans(aggregated, project.businessPlan)
}
// Add treatment site's business plan
const treatmentSite = treatmentSites.find((ts) => ts.id === project.treatmentSiteId)
if (treatmentSite?.businessPlan) {
aggregated = aggregateBusinessPlans(aggregated, treatmentSite.businessPlan)
}
// Add waste sites' business plans
project.collectionSiteIds.forEach((siteId) => {
const wasteSite = wasteSites.find((ws) => ws.id === siteId)
if (wasteSite?.businessPlan) {
aggregated = aggregateBusinessPlans(aggregated, wasteSite.businessPlan)
}
})
// Add subscribed services' business plans
if (treatmentSite) {
treatmentSite.subscribedServices.forEach((serviceId) => {
const service = services.find((s) => s.id === serviceId)
if (service?.businessPlan) {
aggregated = aggregateBusinessPlans(aggregated, service.businessPlan)
}
})
}
// Add transporters' business plans (from treatment site and waste sites)
if (treatmentSite?.transporters) {
treatmentSite.transporters.forEach((assoc) => {
const transporter = transporters.find((t) => t.id === assoc.transporterId)
if (transporter?.businessPlan) {
aggregated = aggregateBusinessPlans(aggregated, transporter.businessPlan)
}
})
}
project.collectionSiteIds.forEach((siteId) => {
const wasteSite = wasteSites.find((ws) => ws.id === siteId)
if (wasteSite?.transporters) {
wasteSite.transporters.forEach((assoc) => {
const transporter = transporters.find((t) => t.id === assoc.transporterId)
if (transporter?.businessPlan) {
aggregated = aggregateBusinessPlans(aggregated, transporter.businessPlan)
}
})
}
})
// Add administrative procedures' business plans
project.administrativeProcedures.forEach((proc) => {
const procedure = administrativeProcedures.find((p) => p.id === proc.procedureId)
if (procedure?.businessPlan) {
aggregated = aggregateBusinessPlans(aggregated, procedure.businessPlan)
}
})
// Add companies' business plans (all companies)
companies.forEach((company) => {
if (company.businessPlan) {
aggregated = aggregateBusinessPlans(aggregated, company.businessPlan)
}
})
// Note: Module components, ecosystems, and regulators are not directly linked to projects
// They would need to be associated through other means if needed
return aggregated
}

View File

@ -1,4 +1,4 @@
import { StorageData, Waste, NaturalRegulator, Service, WasteOrigin, Transporter, ModuleComponent, Ecosystem } from '@/types'
import { StorageData, Waste, NaturalRegulator, Service, WasteOrigin, Transporter, ModuleComponent, Ecosystem, Company, RegulationCharacteristic } from '@/types'
import wastesSeeds from '../../data/seeds/wastes-seeds.json'
import regulatorsSeeds from '../../data/seeds/regulators-seeds.json'
import servicesSeeds from '../../data/seeds/services-seeds.json'
@ -6,6 +6,8 @@ import wasteOriginsSeeds from '../../data/seeds/waste-origins-seeds.json'
import transportersSeeds from '../../data/seeds/transporters-seeds.json'
import moduleComponentsSeeds from '../../data/seeds/module-components-seeds.json'
import ecosystemsSeeds from '../../data/seeds/ecosystems-seeds.json'
import regulationCharacteristicsSeeds from '../../data/seeds/regulation-characteristics-seeds.json'
import companiesSeeds from '../../data/seeds/companies-seeds.json'
const STORAGE_KEY = '4nkwaste_simulator_data'
const USER_KEY = '4nkwaste_simulator_user'
@ -26,10 +28,12 @@ export function initializeStorage(): StorageData {
transporters: [],
moduleComponents: [],
ecosystems: [],
regulationCharacteristics: [],
treatmentSites: [],
wasteSites: [],
investors: [],
administrativeProcedures: [],
companies: [],
projects: [],
}
}
@ -73,6 +77,16 @@ function loadSeedData(): StorageData {
seedData.ecosystems = ecosystemsSeeds.ecosystems as Ecosystem[]
}
// Load regulation characteristics from seed file
if (regulationCharacteristicsSeeds && Array.isArray(regulationCharacteristicsSeeds.regulationCharacteristics)) {
seedData.regulationCharacteristics = regulationCharacteristicsSeeds.regulationCharacteristics as RegulationCharacteristic[]
}
// Load companies from seed file
if (companiesSeeds && Array.isArray(companiesSeeds.companies)) {
seedData.companies = companiesSeeds.companies as Company[]
}
return seedData
}
@ -89,12 +103,14 @@ function mergeData(seedData: StorageData, localData: StorageData): StorageData {
transporters: mergeArraysById(seedData.transporters, localData.transporters),
moduleComponents: mergeArraysById(seedData.moduleComponents, localData.moduleComponents),
ecosystems: mergeArraysById(seedData.ecosystems, localData.ecosystems),
regulationCharacteristics: mergeArraysById(seedData.regulationCharacteristics, localData.regulationCharacteristics),
// Keep local data for other arrays
users: localData.users || seedData.users,
treatmentSites: localData.treatmentSites || seedData.treatmentSites,
wasteSites: localData.wasteSites || seedData.wasteSites,
investors: localData.investors || seedData.investors,
administrativeProcedures: localData.administrativeProcedures || seedData.administrativeProcedures,
companies: localData.companies || seedData.companies,
projects: localData.projects || seedData.projects,
// Preserve version and lastModified from local data if exists
version: localData.version || seedData.version,
@ -176,10 +192,12 @@ export function importData(jsonString: string): { success: boolean; errors: stri
if (!Array.isArray(data.transporters)) errors.push('Invalid transporters array')
if (!Array.isArray(data.moduleComponents)) errors.push('Invalid moduleComponents array')
if (!Array.isArray(data.ecosystems)) errors.push('Invalid ecosystems array')
if (!Array.isArray(data.regulationCharacteristics)) errors.push('Invalid regulationCharacteristics array')
if (!Array.isArray(data.treatmentSites)) errors.push('Invalid treatmentSites array')
if (!Array.isArray(data.wasteSites)) errors.push('Invalid wasteSites array')
if (!Array.isArray(data.investors)) errors.push('Invalid investors array')
if (!Array.isArray(data.administrativeProcedures)) errors.push('Invalid administrativeProcedures array')
if (!Array.isArray(data.companies)) errors.push('Invalid companies array')
if (!Array.isArray(data.projects)) errors.push('Invalid projects array')
if (errors.length > 0) {