From 5c7137f3d2761c60e581cdfe6eb3dd420d0ad99f Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Wed, 10 Dec 2025 08:27:52 +0100 Subject: [PATCH] Add administrative procedures to module components and centralized regulation characteristics configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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 --- data/seeds/companies-seeds.json | 63 ++ .../regulation-characteristics-seeds.json | 296 +++++++++ data_schemas.md | 560 ++++++++++++++++-- src/App.tsx | 2 + src/components/layout/Sidebar.tsx | 4 + src/pages/YieldsPage.css | 15 + src/pages/YieldsPage.tsx | 122 +++- .../EcosystemsConfigurationPage.tsx | 2 + .../ModuleComponentsConfigurationPage.css | 17 + .../ModuleComponentsConfigurationPage.tsx | 155 ++++- ...lationCharacteristicsConfigurationPage.css | 53 ++ ...lationCharacteristicsConfigurationPage.tsx | 258 ++++++++ .../RegulatorsConfigurationPage.tsx | 12 +- .../configuration/WasteConfigurationPage.tsx | 12 +- src/pages/projects/CompaniesPage.tsx | 273 +++++++++ src/pages/projects/TreatmentSitesPage.css | 19 + src/pages/projects/TreatmentSitesPage.tsx | 257 +++++++- src/types/index.ts | 67 ++- .../calculations/businessPlanAggregation.ts | 196 ++++++ src/utils/storage.ts | 20 +- 20 files changed, 2328 insertions(+), 75 deletions(-) create mode 100644 data/seeds/companies-seeds.json create mode 100644 data/seeds/regulation-characteristics-seeds.json create mode 100644 src/pages/configuration/RegulationCharacteristicsConfigurationPage.css create mode 100644 src/pages/configuration/RegulationCharacteristicsConfigurationPage.tsx create mode 100644 src/pages/projects/CompaniesPage.tsx create mode 100644 src/utils/calculations/businessPlanAggregation.ts diff --git a/data/seeds/companies-seeds.json b/data/seeds/companies-seeds.json new file mode 100644 index 0000000..919c89d --- /dev/null +++ b/data/seeds/companies-seeds.json @@ -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" + } + ] +} + diff --git a/data/seeds/regulation-characteristics-seeds.json b/data/seeds/regulation-characteristics-seeds.json new file mode 100644 index 0000000..6c03288 --- /dev/null +++ b/data/seeds/regulation-characteristics-seeds.json @@ -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" + } + ] +} + diff --git a/data_schemas.md b/data_schemas.md index 1d956df..7095bfe 100644 --- a/data_schemas.md +++ b/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) diff --git a/src/App.tsx b/src/App.tsx index 7b9e758..3750457 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 8cf04eb..431052a 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -40,6 +40,10 @@ export default function Sidebar() { 🌱 Ecosystems + + 📋 + Regulation Characteristics +
diff --git a/src/pages/YieldsPage.css b/src/pages/YieldsPage.css index 87f2f3d..8814d17 100644 --- a/src/pages/YieldsPage.css +++ b/src/pages/YieldsPage.css @@ -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; +} \ No newline at end of file diff --git a/src/pages/YieldsPage.tsx b/src/pages/YieldsPage.tsx index e0bfa14..36d06e7 100644 --- a/src/pages/YieldsPage.tsx +++ b/src/pages/YieldsPage.tsx @@ -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() { />
+ + {aggregatedBusinessPlan && ( + +
+

Revenues (€/year)

+ `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 {formatCurrency(total)} + }, + }, + ]} + data={Array(10).fill(null)} + /> + +

Variable Costs (€/year)

+
`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 {formatCurrency(total)} + }, + }, + ]} + data={Array(10).fill(null)} + /> + +

Fixed Costs (€/year)

+
`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 {formatCurrency(total)} + }, + }, + ]} + data={Array(10).fill(null)} + /> + + + )} )} diff --git a/src/pages/configuration/EcosystemsConfigurationPage.tsx b/src/pages/configuration/EcosystemsConfigurationPage.tsx index 12f240c..deda381 100644 --- a/src/pages/configuration/EcosystemsConfigurationPage.tsx +++ b/src/pages/configuration/EcosystemsConfigurationPage.tsx @@ -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 || {}, diff --git a/src/pages/configuration/ModuleComponentsConfigurationPage.css b/src/pages/configuration/ModuleComponentsConfigurationPage.css index cc1171b..ced12bb 100644 --- a/src/pages/configuration/ModuleComponentsConfigurationPage.css +++ b/src/pages/configuration/ModuleComponentsConfigurationPage.css @@ -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; +} diff --git a/src/pages/configuration/ModuleComponentsConfigurationPage.tsx b/src/pages/configuration/ModuleComponentsConfigurationPage.tsx index 8dcddfb..ad25c4e 100644 --- a/src/pages/configuration/ModuleComponentsConfigurationPage.tsx +++ b/src/pages/configuration/ModuleComponentsConfigurationPage.tsx @@ -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>({}) @@ -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 None + } + return ( +
+ {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 ( + + {procedure?.name || 'Unknown'} + + ) + })} +
+ ) + }, + }, { key: 'actions', header: 'Actions', @@ -875,6 +901,133 @@ export default function ModuleComponentsConfigurationPage() { + {/* Regulation Characteristics Section */} +
+

Regulation Characteristics

+

+ Select regulation characteristics that this module component addresses. Configure characteristics in Regulation Characteristics page. +

+
+ {regulationCharacteristics.map((char) => ( + + ))} + {regulationCharacteristics.length === 0 && ( +

No regulation characteristics configured. Add them in the Regulation Characteristics page.

+ )} +
+
+ + {/* Associated Administrative Procedures Section */} +
+

Associated Administrative Procedures

+

+ Add administrative procedures for this module component. Track the status of each procedure. +

+
+ {(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 ( +
+ { + const updatedProcedures = [...(formData.administrativeProcedures || [])] + updatedProcedures[index] = { + ...updatedProcedures[index], + status: e.target.value as ProcedureStatus, + } + setFormData({ ...formData, administrativeProcedures: updatedProcedures }) + }} + options={procedureStatusOptions} + /> + { + 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} + /> + +
+ ) + })} + +
+
+ {/* Costs Section */}

Costs (Annual)

diff --git a/src/pages/configuration/RegulationCharacteristicsConfigurationPage.css b/src/pages/configuration/RegulationCharacteristicsConfigurationPage.css new file mode 100644 index 0000000..f7289c6 --- /dev/null +++ b/src/pages/configuration/RegulationCharacteristicsConfigurationPage.css @@ -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; +} diff --git a/src/pages/configuration/RegulationCharacteristicsConfigurationPage.tsx b/src/pages/configuration/RegulationCharacteristicsConfigurationPage.tsx new file mode 100644 index 0000000..4b70683 --- /dev/null +++ b/src/pages/configuration/RegulationCharacteristicsConfigurationPage.tsx @@ -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(null) + const [formData, setFormData] = useState>({ + name: '', + code: '', + category: 'other', + description: '', + unit: '', + isBoolean: true, + minValue: undefined, + maxValue: undefined, + }) + const [errors, setErrors] = useState>({}) + + 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 = {} + 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) => {char.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) => ( +
+ + +
+ ), + }, + ] + + return ( +
+

Regulation Characteristics Configuration

+ +
+ +
+
+ setFormData({ ...formData, name: e.target.value })} + error={errors.name} + /> + setFormData({ ...formData, code: e.target.value })} + error={errors.code} + helpText="Unique identifier (e.g., 'pathogenElimination', 'heavyMetalsElimination')" + /> +
+ +
+ setFormData({ ...formData, isBoolean: e.target.value === 'boolean' })} + options={[ + { value: 'boolean', label: 'Boolean (Capability/Need)' }, + { value: 'numeric', label: 'Numeric (Value)' }, + ]} + /> +
+ + setFormData({ ...formData, description: e.target.value })} + multiline + rows={3} + /> + + {!formData.isBoolean && ( +
+ setFormData({ ...formData, unit: e.target.value })} + helpText="Unit of measurement (e.g., 'kg/t', '%', 'pH')" + /> + setFormData({ ...formData, minValue: e.target.value ? parseFloat(e.target.value) : undefined })} + /> + setFormData({ ...formData, maxValue: e.target.value ? parseFloat(e.target.value) : undefined })} + /> +
+ )} + +
+ + {editingId && ( + + )} +
+ +
+ + +
+ + + + ) +} diff --git a/src/pages/configuration/RegulatorsConfigurationPage.tsx b/src/pages/configuration/RegulatorsConfigurationPage.tsx index 6f77544..4567b09 100644 --- a/src/pages/configuration/RegulatorsConfigurationPage.tsx +++ b/src/pages/configuration/RegulatorsConfigurationPage.tsx @@ -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(null) const [formData, setFormData] = useState>({ 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(), } diff --git a/src/pages/configuration/WasteConfigurationPage.tsx b/src/pages/configuration/WasteConfigurationPage.tsx index dec2284..53cbdeb 100644 --- a/src/pages/configuration/WasteConfigurationPage.tsx +++ b/src/pages/configuration/WasteConfigurationPage.tsx @@ -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(), } diff --git a/src/pages/projects/CompaniesPage.tsx b/src/pages/projects/CompaniesPage.tsx new file mode 100644 index 0000000..f4e5b8c --- /dev/null +++ b/src/pages/projects/CompaniesPage.tsx @@ -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(null) + const [formData, setFormData] = useState>({ + name: '', + legalName: '', + registrationNumber: '', + address: '', + contact: { + email: '', + phone: '', + website: '', + }, + }) + const [errors, setErrors] = useState>({}) + 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 = {} + 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) => ( +
+ + +
+ ), + }, + ] + + return ( +
+

Companies

+ +
+ +
+
+ setFormData({ ...formData, name: e.target.value })} + error={errors.name} + /> + setFormData({ ...formData, legalName: e.target.value })} + /> +
+ +
+ setFormData({ ...formData, registrationNumber: e.target.value })} + /> + setFormData({ ...formData, address: e.target.value })} + /> +
+ +
+

Contact Information

+
+ + setFormData({ + ...formData, + contact: { + ...formData.contact!, + email: e.target.value, + }, + }) + } + /> + + setFormData({ + ...formData, + contact: { + ...formData.contact!, + phone: e.target.value, + }, + }) + } + /> +
+ + setFormData({ + ...formData, + contact: { + ...formData.contact!, + website: e.target.value, + }, + }) + } + /> +
+ +
+
+

Business Plan

+ +
+ {showBusinessPlan && formData.businessPlan && ( + + )} +
+ +
+ + {editingId && ( + + )} +
+ +
+ + +
+ + + + ) +} + diff --git a/src/pages/projects/TreatmentSitesPage.css b/src/pages/projects/TreatmentSitesPage.css index 7088e6a..33044a8 100644 --- a/src/pages/projects/TreatmentSitesPage.css +++ b/src/pages/projects/TreatmentSitesPage.css @@ -109,4 +109,23 @@ height: 20px; 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; } \ No newline at end of file diff --git a/src/pages/projects/TreatmentSitesPage.tsx b/src/pages/projects/TreatmentSitesPage.tsx index 6f96321..11b012b 100644 --- a/src/pages/projects/TreatmentSitesPage.tsx +++ b/src/pages/projects/TreatmentSitesPage.tsx @@ -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>({}) 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 None + } + return ( +
+ {site.investors.map((assoc, idx) => { + const investor = investors.find((i) => i.id === assoc.investorId) + return ( + + {investor?.name || 'Unknown'} + + ) + })} +
+ ) + }, + }, + { + key: 'procedures', + header: 'Procedures', + render: (site: TreatmentSite) => { + if (!site.administrativeProcedures || site.administrativeProcedures.length === 0) { + return None + } + return ( +
+ {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 ( + + {procedure?.name || 'Unknown'} + + ) + })} +
+ ) + }, + }, { key: 'actions', header: 'Actions', @@ -340,6 +391,210 @@ export default function TreatmentSitesPage() { + {/* Associated Investors Section */} +
+

Associated Investors

+

+ Add investors for this treatment site. Specify the investment status and amount if known. +

+
+ {(formData.investors || []).map((association, index) => { + const investor = investors.find((i) => i.id === association.investorId) + return ( +
+ { + const updatedInvestors = [...(formData.investors || [])] + updatedInvestors[index] = { + ...updatedInvestors[index], + status: e.target.value as 'toBeApproached' | 'loiOk' | 'inProgress' | 'completed', + } + setFormData({ ...formData, investors: updatedInvestors }) + }} + options={statusOptions} + /> + { + 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} + /> + { + const updatedInvestors = [...(formData.investors || [])] + updatedInvestors[index] = { + ...updatedInvestors[index], + notes: e.target.value || undefined, + } + setFormData({ ...formData, investors: updatedInvestors }) + }} + /> + +
+ ) + })} + +
+
+ + {/* Associated Administrative Procedures Section */} +
+

Associated Administrative Procedures

+

+ Add administrative procedures for this treatment site. Track the status of each procedure. +

+
+ {(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 ( +
+ { + const updatedProcedures = [...(formData.administrativeProcedures || [])] + updatedProcedures[index] = { + ...updatedProcedures[index], + status: e.target.value as ProcedureStatus, + } + setFormData({ ...formData, administrativeProcedures: updatedProcedures }) + }} + options={procedureStatusOptions} + /> + { + 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} + /> + +
+ ) + })} + +
+
+