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:
parent
7b8d2b1abb
commit
5c7137f3d2
63
data/seeds/companies-seeds.json
Normal file
63
data/seeds/companies-seeds.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
296
data/seeds/regulation-characteristics-seeds.json
Normal file
296
data/seeds/regulation-characteristics-seeds.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
560
data_schemas.md
560
data_schemas.md
@ -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)
|
||||
|
||||
@ -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 />} />
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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 || {},
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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(),
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
|
||||
273
src/pages/projects/CompaniesPage.tsx
Normal file
273
src/pages/projects/CompaniesPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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[]
|
||||
}
|
||||
|
||||
196
src/utils/calculations/businessPlanAggregation.ts
Normal file
196
src/utils/calculations/businessPlanAggregation.ts
Normal 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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user