From 5b2c2f83d5dc6d82271d5cc95a841ea441fb4384 Mon Sep 17 00:00:00 2001 From: Maryem Hadj Wannes Date: Fri, 6 Feb 2026 17:00:54 +0100 Subject: [PATCH] Plant_Builder: implement high level rules (#76) --- src/app/plant-builder/App.css | 42 - src/app/plant-builder/ComplianceCheck.tsx | 479 ---------- src/app/plant-builder/ComplianceResults.tsx | 178 ---- .../plant-builder/DownstreamOperations.tsx | 161 ---- .../plant-builder/FilterCertifications.tsx | 322 ------- src/app/plant-builder/Index.tsx | 162 ---- src/app/plant-builder/NotFound.tsx | 32 - src/app/plant-builder/OfftakeLocation.tsx | 189 ---- src/app/plant-builder/PlantBuilder.tsx | 825 ------------------ src/app/plant-builder/VerifyProducts.tsx | 114 --- src/app/plant-builder/compliance-results.json | 54 -- src/app/plant-builder/complianceCheck.ts | 242 ----- src/app/plant-builder/connections.json | 19 - src/app/plant-builder/data.ts | 140 --- src/app/plant-builder/page.tsx | 14 - src/app/plant-builder/plant-builder-vite.css | 78 -- .../plant-flow-twin.code-workspace | 11 - src/app/plant-builder/plant-info.json | 24 - src/app/plant-builder/product-info.json | 38 - .../RecommendationInterface.tsx | 477 ---------- .../RecommendationPageContent.tsx | 19 - .../plant-builder/recommendations/page.tsx | 15 - src/app/plant-builder/types.ts | 203 ----- src/app/plant-builder/user-details.json | 5 - .../plant-builder/PlantBuilder.tsx | 272 +++++- src/components/plant-builder/Canvas.tsx | 42 +- .../plant-builder/ConnectionArrow.tsx | 23 +- .../plant-builder/PlantComponent.tsx | 92 +- src/components/ui/tooltip.tsx | 20 +- src/services/plant-builder/digitalTwins.ts | 26 + 30 files changed, 446 insertions(+), 3872 deletions(-) delete mode 100644 src/app/plant-builder/App.css delete mode 100644 src/app/plant-builder/ComplianceCheck.tsx delete mode 100644 src/app/plant-builder/ComplianceResults.tsx delete mode 100644 src/app/plant-builder/DownstreamOperations.tsx delete mode 100644 src/app/plant-builder/FilterCertifications.tsx delete mode 100644 src/app/plant-builder/Index.tsx delete mode 100644 src/app/plant-builder/NotFound.tsx delete mode 100644 src/app/plant-builder/OfftakeLocation.tsx delete mode 100644 src/app/plant-builder/PlantBuilder.tsx delete mode 100644 src/app/plant-builder/VerifyProducts.tsx delete mode 100644 src/app/plant-builder/compliance-results.json delete mode 100644 src/app/plant-builder/complianceCheck.ts delete mode 100644 src/app/plant-builder/connections.json delete mode 100644 src/app/plant-builder/data.ts delete mode 100644 src/app/plant-builder/page.tsx delete mode 100644 src/app/plant-builder/plant-builder-vite.css delete mode 100644 src/app/plant-builder/plant-flow-twin.code-workspace delete mode 100644 src/app/plant-builder/plant-info.json delete mode 100644 src/app/plant-builder/product-info.json delete mode 100644 src/app/plant-builder/recommendations/RecommendationInterface.tsx delete mode 100644 src/app/plant-builder/recommendations/RecommendationPageContent.tsx delete mode 100644 src/app/plant-builder/recommendations/page.tsx delete mode 100644 src/app/plant-builder/types.ts delete mode 100644 src/app/plant-builder/user-details.json diff --git a/src/app/plant-builder/App.css b/src/app/plant-builder/App.css deleted file mode 100644 index b9d355d..0000000 --- a/src/app/plant-builder/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/app/plant-builder/ComplianceCheck.tsx b/src/app/plant-builder/ComplianceCheck.tsx deleted file mode 100644 index dac8906..0000000 --- a/src/app/plant-builder/ComplianceCheck.tsx +++ /dev/null @@ -1,479 +0,0 @@ -'use client'; - -import { useState, useEffect } from "react"; -import { VerifyProducts } from "./VerifyProducts"; -import { OfftakeLocation as OfftakeLocationComponent } from "./OfftakeLocation"; -import { DownstreamOperations } from "./DownstreamOperations"; -import { FilterCertifications } from "./FilterCertifications"; -import { ComplianceResults } from "./ComplianceResults"; -import { toast } from "sonner"; -import { - ProductInfo, - ComplianceResult, - PlacedComponent, - PlantInfo, - CertificationScheme, - UserDetails, - Connection, - OfftakeLocation, -} from "./types"; - -const certificationSchemes: CertificationScheme[] = [ - { - id: "rfnbo", - name: "RFNBO", - criteria: { - origin: ["wind", "solar", "hydro"], - carbonFootprint: { max: 3.4, unit: "gCO2e/MJ" }, - chainOfCustody: ["mass-balance", "book-and-claim"], - markets: ["EU", "UK"], - }, - weight: 0.4, - }, - { - id: "advanced", - name: "Advanced Biofuels", - criteria: { - origin: ["biomass", "waste"], - carbonFootprint: { max: 5.0, unit: "gCO2e/MJ" }, - chainOfCustody: ["mass-balance"], - markets: ["EU", "US"], - }, - weight: 0.3, - }, - { - id: "annexIXA", - name: "Annex IX Part A", - criteria: { - origin: ["waste", "residues"], - carbonFootprint: { max: 4.0, unit: "gCO2e/MJ" }, - chainOfCustody: ["mass-balance"], - markets: ["EU"], - }, - weight: 0.2, - }, - { - id: "annexIXB", - name: "Annex IX Part B", - criteria: { - origin: ["used cooking oil", "animal fats"], - carbonFootprint: { max: 4.5, unit: "gCO2e/MJ" }, - chainOfCustody: ["mass-balance"], - markets: ["EU"], - }, - weight: 0.1, - }, -]; - -const hardcodedProducts: ProductInfo[] = [ - { - productId: "P001", - productName: "H2", - productType: "Fuel", - productionCapacity: "1000", - unit: "kg/day", - fuelType: "Hydrogen", - fuelClass: "RFNBO", - feedstock: "RES, WATER", - carbonFootprint: { value: 2.5, unit: "gCO2e/MJ" }, - offtakeLocations: [], - downstreamOperations: "", - verified: true, - }, - { - productId: "P002", - productName: "MeOH", - productType: "Fuel", - productionCapacity: "5000", - unit: "liters/day", - fuelType: "Methanol", - fuelClass: "RFNBO", - feedstock: "Hydrogen H2, RES, Water, CO2", - carbonFootprint: { value: 3.0, unit: "gCO2e/MJ" }, - offtakeLocations: [], - downstreamOperations: "", - verified: true, - }, - { - productId: "P003", - productName: "H2", - productType: "Fuel", - productionCapacity: "1000", - unit: "kg/day", - fuelType: "Hydrogen", - fuelClass: "RFNBO", - feedstock: "Biomass, Water (via gasification and electrolysis)", - carbonFootprint: { value: 3.2, unit: "gCO2e/MJ" }, - offtakeLocations: [], - downstreamOperations: "", - verified: false, - }, - { - productId: "P004", - productName: "MeOH", - productType: "Fuel", - productionCapacity: "5000", - unit: "liters/day", - fuelType: "Methanol", - fuelClass: "RFNBO", - feedstock: "Biomass, CO2 (via syngas from biomass gasification)", - carbonFootprint: { value: 3.5, unit: "gCO2e/MJ" }, - offtakeLocations: [], - downstreamOperations: "", - verified: false, - }, -]; - -// FIXED: calculateCompliance now returns full ComplianceResult -const calculateCompliance = ( - products: ProductInfo[], - components: PlacedComponent[], - plantInfo: PlantInfo | null, - selectedCertifications: string[], - verifiedProducts: string[] -): ComplianceResult[] => { - const results: ComplianceResult[] = []; - - products.forEach((product) => { - // Only check compliance for products that have been verified in the first step - if (!verifiedProducts.includes(product.productName)) return; - - // Filter components related to this specific product type - const outputGates = components.filter( - (c) => - c.type === "gate" && - c.data?.gateType === "output" && - c.data?.product === product.productType - ); - const relatedCarriers = components.filter( - (c) => c.type === "carrier" && c.data?.product === product.productType - ); - - certificationSchemes - .filter((scheme) => selectedCertifications.includes(scheme.id)) - .forEach((scheme) => { - let originStatus: "match" | "mismatch" | "risk" = "risk"; - let carbonFootprintStatus: "match" | "mismatch" | "risk" = "risk"; - let chainOfCustodyStatus: "match" | "mismatch" | "risk" = "risk"; - let confidenceScore = 0; - let originDetails: string | undefined; - let carbonFootprintDetails: string | undefined; - let chainOfCustodyDetails: string | undefined; - - // === ORIGIN === - const origins = outputGates - .map((gate) => gate.data?.sourceOrigin) - .filter((o): o is string => !!o); - - if (origins.length > 0 && scheme.criteria.origin) { - originStatus = origins.every((o) => scheme.criteria.origin!.includes(o)) - ? "match" - : "mismatch"; - originDetails = originStatus === "match" - ? origins.join(", ") - : `Required: ${scheme.criteria.origin!.join(", ")}`; - } else { - originDetails = "Missing origin data"; - } - confidenceScore += (originStatus === "match" ? 1 : originStatus === "risk" ? 0.5 : 0) * scheme.weight * 0.4; - - // === CARBON FOOTPRINT === - // Simple average of related carrier carbon footprints - const carbonFootprint = relatedCarriers.reduce((sum, c) => { - const cf = c.data?.carbonFootprint || 0; - return sum + (typeof cf === "number" ? cf : 0); - }, 0) / (relatedCarriers.length || 1); - - if (scheme.criteria.carbonFootprint && carbonFootprint > 0) { - carbonFootprintStatus = - carbonFootprint <= scheme.criteria.carbonFootprint.max ? "match" : "mismatch"; - carbonFootprintDetails = carbonFootprintStatus === "match" - ? `${carbonFootprint.toFixed(2)} ${scheme.criteria.carbonFootprint.unit}` - : `Exceeds max (${scheme.criteria.carbonFootprint.max})`; - } else { - carbonFootprintDetails = "Missing carbon footprint data"; - } - confidenceScore += (carbonFootprintStatus === "match" ? 1 : carbonFootprintStatus === "risk" ? 0.5 : 0) * scheme.weight * 0.4; - - // === CHAIN OF CUSTODY === - const custodyMethods = outputGates - .map((gate) => gate.data?.chainOfCustody) - .filter((c): c is string => !!c); - - if (custodyMethods.length > 0 && scheme.criteria.chainOfCustody) { - chainOfCustodyStatus = custodyMethods.every((c) => - scheme.criteria.chainOfCustody!.includes(c) - ) - ? "match" - : "mismatch"; - chainOfCustodyDetails = chainOfCustodyStatus === "match" - ? custodyMethods.join(", ") - : `Required: ${scheme.criteria.chainOfCustody!.join(", ")}`; - } else { - chainOfCustodyDetails = "Missing chain of custody data"; - } - confidenceScore += (chainOfCustodyStatus === "match" ? 1 : chainOfCustodyStatus === "risk" ? 0.5 : 0) * scheme.weight * 0.2; - - // === MARKET ELIGIBILITY (MULTIPLE OFFTAKE LOCATIONS) === - let eligibleMarkets: string[] = scheme.criteria.markets; - let marketMatchFactor = 1; - - if (product.offtakeLocations && product.offtakeLocations.length > 0) { - const countries = product.offtakeLocations.map(loc => loc.country).filter(Boolean); - if (countries.length > 0) { - const matches = countries.filter(country => scheme.criteria.markets.includes(country)).length; - const matchRatio = matches / countries.length; - // Market match adds confidence: 50% if any match, 100% if all match. - marketMatchFactor = matchRatio > 0 ? (0.5 + (0.5 * matchRatio)) : 0.5; - eligibleMarkets = [...new Set([...countries, ...scheme.criteria.markets])]; - } - } else if (plantInfo?.country) { - const marketMatch = scheme.criteria.markets.includes(plantInfo.country); - // If plant country matches, full confidence, otherwise slight penalty. - marketMatchFactor = marketMatch ? 1 : 0.8; - eligibleMarkets = marketMatch ? [plantInfo.country, ...scheme.criteria.markets] : scheme.criteria.markets; - } - - confidenceScore *= marketMatchFactor; - - // === DOWNSTREAM PENALTY === - // Apply a small penalty if downstream operations are required but not defined, implying risk - if (product.downstreamOperations) { - confidenceScore *= 0.9; - } - - // === FUEL CLASS MAPPING === - const fuelClass = - scheme.id === "rfnbo" - ? "RFNBO" - : scheme.id === "advanced" - ? "Advanced Biofuel" - : `Annex IX ${scheme.id === "annexIXA" ? "Part A" : "Part B"}`; - - // Create the final result object - results.push({ - productId: product.productId, // REQUIRED - productName: product.productName, - scheme: scheme.name, - confidence: Math.round(confidenceScore * 100), - fuelClass, - origin: originDetails, - carbonFootprint: carbonFootprintDetails, - chainOfCustody: chainOfCustodyDetails, - eligibleMarkets, - complianceStatus: confidenceScore > 0.8 ? "fully-compliant" : confidenceScore > 0.5 ? "partial-compliance" : "non-compliant", - enhancedConfidence: Math.round(confidenceScore * 100), - }); - }); - }); - - return results; -}; - -const sortComplianceResults = ( - results: ComplianceResult[], - sortBy: "product" | "scheme" | "confidence" | "fuelClass" = "confidence", - sortOrder: "asc" | "desc" = "desc" -): ComplianceResult[] => { - return [...results].sort((a, b) => { - const factor = sortOrder === "asc" ? 1 : -1; - switch (sortBy) { - case "product": - return a.productName.localeCompare(b.productName) * factor; - case "scheme": - return a.scheme.localeCompare(b.scheme) * factor; - case "fuelClass": - return a.fuelClass.localeCompare(b.fuelClass) * factor; - default: - // Default to sorting by confidence (number) - return (a.confidence - b.confidence) * factor; - } - }); -}; - -interface ComplianceCheckProps { - productInfo: ProductInfo[]; - setProductInfo: React.Dispatch>; - components: PlacedComponent[]; - plantInfo: PlantInfo | null; - verifiedProducts: string[]; - setVerifiedProducts: React.Dispatch>; - selectedCertifications: string[]; - setSelectedCertifications: React.Dispatch>; - complianceResults: ComplianceResult[]; - setComplianceResults: React.Dispatch>; - sortBy: "product" | "scheme" | "confidence" | "fuelClass"; - setSortBy: React.Dispatch>; - sortOrder: "asc" | "desc"; - setSortOrder: React.Dispatch>; - error: string | null; - setError: React.Dispatch>; - onBack: () => void; - userDetails: UserDetails | null; - connections: Connection[]; -} - -export const ComplianceCheck = ({ - productInfo: propProductInfo, // Renamed to propProductInfo to avoid shadowing state variable - setProductInfo: propSetProductInfo, // Renamed to avoid shadowing - components, - plantInfo: propPlantInfo, // Renamed to propPlantInfo to avoid shadowing state variable - verifiedProducts, - setVerifiedProducts, - selectedCertifications, - setSelectedCertifications, - complianceResults, - setComplianceResults, - sortBy, - setSortBy, - sortOrder, - setSortOrder, - error, - setError, - onBack, - userDetails, - connections, -}: ComplianceCheckProps) => { - const [step, setStep] = useState<"verify" | "offtake" | "downstream" | "certifications" | "results">("verify"); - // Use internal state for product info and plant info if needed, or stick to prop values if they are the source of truth - const [productInfo, setProductInfo] = useState(hardcodedProducts); - const [plantInfo, setPlantInfo] = useState(propPlantInfo); - - // Initialize state based on hardcoded products and default selections - useEffect(() => { - // Ensure offtakeLocations array exists for existing products - const migratedProducts = hardcodedProducts.map(p => ({ - ...p, - offtakeLocations: p.offtakeLocations || [], - })); - setProductInfo(migratedProducts); - setVerifiedProducts(migratedProducts.filter(p => p.verified).map(p => p.productName)); - setSelectedCertifications(certificationSchemes.map(s => s.id)); - setPlantInfo(propPlantInfo); // Ensure plantInfo is initialized from props - }, [propPlantInfo, setVerifiedProducts, setSelectedCertifications]); // Dependencies added - - const handleNextStep = () => { - // Step validation checks - if (step === "verify" && verifiedProducts.length === 0) { - setError("Please verify at least one product to proceed."); - toast.error("Please verify at least one product."); - return; - } - if (step === "certifications") { - if (selectedCertifications.length === 0) { - setError("Please select at least one certification scheme."); - toast.error("Please select at least one certification scheme."); - return; - } - try { - // Calculate compliance results - const results = calculateCompliance(productInfo, components, plantInfo, selectedCertifications, verifiedProducts); - const sortedResults = sortComplianceResults(results, sortBy, sortOrder); - setComplianceResults(sortedResults); - setStep("results"); - toast.success("Compliance check completed! Review the results below."); - } catch (err) { - console.error("Compliance Calculation Error:", err); - setError("Failed to calculate compliance results. Please check your model and selections."); - toast.error("Error during compliance check."); - } - return; - } - - // Move to next step - setStep(prev => { - switch (prev) { - case "verify": return "offtake"; - case "offtake": return "downstream"; - case "downstream": return "certifications"; - default: return prev; // Should not happen - } - }); - setError(null); - }; - - const handleBackStep = () => { - if (step === "verify") { - // If on the first step, call the main back handler - onBack(); - } else { - // Move to previous step - setStep(prev => { - switch (prev) { - case "offtake": return "verify"; - case "downstream": return "offtake"; - case "certifications": return "downstream"; - case "results": return "certifications"; - default: return prev; // Should not happen - } - }); - setError(null); - } - }; - - return ( -
- {step === "verify" && ( - - )} - {step === "offtake" && ( - - )} - {step === "downstream" && ( - - )} - {step === "certifications" && ( - - )} - {step === "results" && ( - - )} -
- ); -}; - -export default ComplianceCheck; diff --git a/src/app/plant-builder/ComplianceResults.tsx b/src/app/plant-builder/ComplianceResults.tsx deleted file mode 100644 index 06ad436..0000000 --- a/src/app/plant-builder/ComplianceResults.tsx +++ /dev/null @@ -1,178 +0,0 @@ -'use client'; - -import React, { useState } from "react"; -import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { Download } from "lucide-react"; -import { toast } from "sonner"; -import { - Table, TableBody, TableCell, TableHead, TableHeader, TableRow, -} from "@/components/ui/table"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import jsPDF from "jspdf"; -import html2canvas from "html2canvas"; -import { ComplianceResult, ProductInfo } from "../../app/plant-builder/types"; -import { - UserDetails, // interface - PlantInfo, // interface - PlacedComponent, // interface - Connection, // interface -} from "../../app/plant-builder/types"; // EXACT PATH - -interface ComplianceResultsProps { - productInfo: ProductInfo[]; - complianceResults: ComplianceResult[]; - sortBy: "product" | "scheme" | "confidence" | "fuelClass"; - setSortBy: React.Dispatch>; - sortOrder: "asc" | "desc"; - setSortOrder: React.Dispatch>; - setComplianceResults: React.Dispatch>; - error: string | null; - handleBackStep: () => void; - sortComplianceResults: (results: ComplianceResult[], sortBy: any, sortOrder: any) => ComplianceResult[]; - userDetails: UserDetails | null; - plantInfo: PlantInfo | null; - components: PlacedComponent[]; - connections: Connection[]; -} - -export const ComplianceResults = ({ - productInfo, - complianceResults, - sortBy, - setSortBy, - sortOrder, - setSortOrder, - setComplianceResults, - error, - handleBackStep, - sortComplianceResults, - userDetails, // NEW - plantInfo, // NEW - components, // NEW - connections, // NEW -}: ComplianceResultsProps) => { - const router = useRouter(); - const [activeTab, setActiveTab] = useState("products"); - - const handleExport = (data: ComplianceResult[], filename: string) => { - const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); - toast.success(`${filename} exported!`); - }; - - const handleExportPDF = async () => { - const pdf = new jsPDF("p", "mm", "a4"); - const pageWidth = pdf.internal.pageSize.getWidth(); - const margin = 10; - let y = 20; - - const addTable = async (id: string, title: string) => { - const el = document.getElementById(id); - if (!el) return; - pdf.setFontSize(14); - pdf.text(title, margin, y); - y += 10; - const canvas = await html2canvas(el, { scale: 2 }); - const imgData = canvas.toDataURL("image/png"); - const imgProps = pdf.getImageProperties(imgData); - const imgWidth = pageWidth - 2 * margin; - const imgHeight = (imgProps.height * imgWidth) / imgProps.width; - if (y + imgHeight > 280) { pdf.addPage(); y = 20; } - pdf.addImage(imgData, "PNG", margin, y, imgWidth, imgHeight); - y += imgHeight + 15; - }; - - await addTable("products-table", "Products"); - await addTable("eligibility-table", "Eligibility"); - await addTable("product-scheme-table", "Product-Scheme"); - - pdf.save("compliance-results.pdf"); - toast.success("PDF exported!"); - }; - - const handleSortChange = (value: any) => { - setSortBy(value); - setComplianceResults(sortComplianceResults(complianceResults, value, sortOrder)); - }; - - const handleOrderChange = (value: any) => { - setSortOrder(value); - setComplianceResults(sortComplianceResults(complianceResults, sortBy, value)); - }; - - return ( -
-

Compliance Check Results

- {error &&
{error}
} - -
-
- - -
- - - - Products - Eligibility - Product-Scheme - - - -
{/* ... same table ... */}
-
- - -
{/* ... same ... */}
-
- - -
{/* ... same ... */}
-
-
- -
- -
- - - -
-
-
-
- ); -}; \ No newline at end of file diff --git a/src/app/plant-builder/DownstreamOperations.tsx b/src/app/plant-builder/DownstreamOperations.tsx deleted file mode 100644 index 2d7edfe..0000000 --- a/src/app/plant-builder/DownstreamOperations.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Plus, Trash2 } from "lucide-react"; -import { ProductInfo } from "./types"; - -interface DownstreamOperationsProps { - productInfo: ProductInfo[]; - setProductInfo: React.Dispatch>; - verifiedProducts: string[]; - error: string | null; - handleNextStep: () => void; - handleBackStep: () => void; -} - -export const DownstreamOperations = ({ - productInfo, - setProductInfo, - verifiedProducts, - error, - handleNextStep, - handleBackStep, -}: DownstreamOperationsProps) => { - // Support multiple downstream operations via array (migrate in parent if needed) - // Assume ProductInfo has downstreamOperationsArray?: string[] - - const addDownstreamOperation = (productName: string) => { - const updatedProducts = [...productInfo]; - const productIndex = updatedProducts.findIndex((p) => p.productName === productName); - if (productIndex !== -1) { - const currentOps = (updatedProducts[productIndex] as any).downstreamOperationsArray || []; - (updatedProducts[productIndex] as any).downstreamOperationsArray = [...currentOps, ""]; - setProductInfo(updatedProducts); - } - }; - - const removeDownstreamOperation = (productName: string, opIndex: number) => { - const updatedProducts = [...productInfo]; - const productIndex = updatedProducts.findIndex((p) => p.productName === productName); - if (productIndex !== -1) { - const currentOps = (updatedProducts[productIndex] as any).downstreamOperationsArray || []; - currentOps.splice(opIndex, 1); - (updatedProducts[productIndex] as any).downstreamOperationsArray = currentOps; - setProductInfo(updatedProducts); - } - }; - - const updateDownstreamOperation = ( - productName: string, - opIndex: number, - value: string - ) => { - const updatedProducts = [...productInfo]; - const productIndex = updatedProducts.findIndex((p) => p.productName === productName); - if (productIndex !== -1) { - const currentOps = (updatedProducts[productIndex] as any).downstreamOperationsArray || []; - if (currentOps[opIndex] !== undefined) { - currentOps[opIndex] = value; - (updatedProducts[productIndex] as any).downstreamOperationsArray = currentOps; - setProductInfo(updatedProducts); - } - } - }; - - const verifiedProductInfo = productInfo.filter((p) => verifiedProducts.includes(p.productName)); - - return ( -
-

Downstream Operations

- {error && ( -
{error}
- )} -
-
-

- Provide details about downstream operations for each verified product (optional). You can add multiple operations (e.g., refining, blending, transport). This may affect compliance confidence (e.g., additional processing reduces score by 10% per operation). -

-
-
- {/* Header Row */} -
-
Product ID
-
Product Name
-
Fuel Type
-
Feedstock
-
Downstream Operations
-
- {/* Body */} - {verifiedProductInfo.length === 0 ? ( -
-
- No verified products -
-
- ) : ( - verifiedProductInfo.map((product, productIndex) => ( -
- {/* Product Header Row */} -
-
{product.productId}
-
{product.productName}
-
{product.fuelType}
-
{product.feedstock || "N/A"}
-
{/* Alignment spacer */} -
- {/* Operations List */} -
- {((product as any).downstreamOperationsArray || []).map((operation: string, opIndex: number) => ( -
-
-