turash/bugulma/frontend/components/resource-flow/ResourceFlowWizard.tsx
2025-12-15 10:06:41 +01:00

444 lines
16 KiB
TypeScript

import Button from '@/components/ui/Button.tsx';
import ErrorMessage from '@/components/ui/ErrorMessage.tsx';
import { Flex, Stack } from '@/components/ui/layout';
import Wizard from '@/components/wizard/Wizard.tsx';
import { useCreateResourceFlow } from '@/hooks/api/useResourcesAPI.ts';
import { useSitesByOrganization } from '@/hooks/api/useSitesAPI.ts';
import { useTranslation } from '@/hooks/useI18n.tsx';
import type { ResourceDirection } from '@/schemas/backend/resource-flow';
import React, { useCallback, useState } from 'react';
interface ResourceFlowWizardProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
organizationId: string;
organizationName: string;
direction: ResourceDirection;
}
interface WizardData {
siteId: string;
type: string;
// Step 2: Quality
temperature_celsius?: number;
pressure_bar?: number;
purity_pct?: number;
physical_state?: string;
// Step 3: Quantity
amount: number;
unit: string;
temporal_unit?: string;
// Step 4: Economic
cost_in?: number;
cost_out?: number;
waste_disposal_cost?: number;
transportation_cost?: number;
// Metadata
precision_level: string;
source_type: string;
}
const ResourceFlowWizard: React.FC<ResourceFlowWizardProps> = ({
isOpen,
onClose,
onSuccess,
organizationId,
// organizationName is intentionally accepted by callers but unused in the wizard UI
direction,
}) => {
const { t } = useTranslation();
const createResourceFlowMutation = useCreateResourceFlow();
const { data: sites, isLoading: isLoadingSites } = useSitesByOrganization(organizationId);
const [currentStep, setCurrentStep] = useState(1);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<WizardData>({
siteId: '',
type: 'heat',
amount: 0,
unit: 'MWh',
precision_level: 'estimated',
source_type: 'declared',
});
const totalSteps = 4;
const updateData = useCallback((updates: Partial<WizardData>) => {
setData((prev) => ({ ...prev, ...updates }));
}, []);
const handleNext = useCallback(() => {
if (currentStep < totalSteps) {
setCurrentStep((prev) => prev + 1);
setError(null);
}
}, [currentStep, totalSteps]);
const handlePrevious = useCallback(() => {
if (currentStep > 1) {
setCurrentStep((prev) => prev - 1);
setError(null);
}
}, [currentStep]);
const handleSubmit = useCallback(async () => {
try {
setError(null);
const request = {
organization_id: organizationId,
site_id: data.siteId,
direction,
type: data.type,
quality: {
temperature_celsius: data.temperature_celsius,
pressure_bar: data.pressure_bar,
purity_pct: data.purity_pct,
physical_state: data.physical_state,
},
quantity: {
amount: data.amount,
unit: data.unit,
temporal_unit: data.temporal_unit,
},
economic_data: {
cost_in: data.cost_in,
cost_out: data.cost_out,
waste_disposal_cost: data.waste_disposal_cost,
transportation_cost: data.transportation_cost,
},
precision_level: data.precision_level,
source_type: data.source_type,
};
await createResourceFlowMutation.mutateAsync(request);
onSuccess();
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
}
}, [data, direction, organizationId, createResourceFlowMutation, onSuccess]);
const canProceed = useCallback(() => {
switch (currentStep) {
case 1:
return data.siteId && data.type;
case 2:
return true; // Quality is optional
case 3:
return data.amount > 0 && data.unit;
case 4:
return true; // Economic data is optional
default:
return false;
}
}, [currentStep, data]);
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<Stack spacing="md">
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step1.site')}
</label>
<select
value={data.siteId}
onChange={(e) => updateData({ siteId: e.target.value })}
className="w-full p-2 border rounded-md"
disabled={isLoadingSites}
>
<option value="">{t('resourceFlowWizard.step1.selectSite')}</option>
{sites?.map((site) => (
<option key={site.ID} value={site.ID}>
{site.Name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step1.resourceType')}
</label>
<select
value={data.type}
onChange={(e) => updateData({ type: e.target.value })}
className="w-full p-2 border rounded-md"
>
<option value="heat">{t('resourceTypes.heat')}</option>
<option value="water">{t('resourceTypes.water')}</option>
<option value="steam">{t('resourceTypes.steam')}</option>
<option value="CO2">{t('resourceTypes.CO2')}</option>
<option value="biowaste">{t('resourceTypes.biowaste')}</option>
<option value="cooling">{t('resourceTypes.cooling')}</option>
<option value="logistics">{t('resourceTypes.logistics')}</option>
<option value="materials">{t('resourceTypes.materials')}</option>
<option value="service">{t('resourceTypes.service')}</option>
</select>
</div>
</Stack>
);
case 2:
return (
<Stack spacing="md">
<h3 className="text-lg font-medium">{t('resourceFlowWizard.step2.title')}</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step2.temperature')}
</label>
<input
type="number"
value={data.temperature_celsius || ''}
onChange={(e) =>
updateData({ temperature_celsius: parseFloat(e.target.value) || undefined })
}
className="w-full p-2 border rounded-md"
placeholder="°C"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step2.pressure')}
</label>
<input
type="number"
value={data.pressure_bar || ''}
onChange={(e) =>
updateData({ pressure_bar: parseFloat(e.target.value) || undefined })
}
className="w-full p-2 border rounded-md"
placeholder="bar"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step2.purity')}
</label>
<input
type="number"
value={data.purity_pct || ''}
onChange={(e) =>
updateData({ purity_pct: parseFloat(e.target.value) || undefined })
}
className="w-full p-2 border rounded-md"
placeholder="%"
min="0"
max="100"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step2.physicalState')}
</label>
<select
value={data.physical_state || ''}
onChange={(e) => updateData({ physical_state: e.target.value })}
className="w-full p-2 border rounded-md"
>
<option value="">{t('resourceFlowWizard.step2.selectState')}</option>
<option value="solid">{t('physicalStates.solid')}</option>
<option value="liquid">{t('physicalStates.liquid')}</option>
<option value="gas">{t('physicalStates.gas')}</option>
</select>
</div>
</div>
</Stack>
);
case 3:
return (
<Stack spacing="md">
<h3 className="text-lg font-medium">{t('resourceFlowWizard.step3.title')}</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step3.amount')} *
</label>
<input
type="number"
value={data.amount}
onChange={(e) => updateData({ amount: parseFloat(e.target.value) || 0 })}
className="w-full p-2 border rounded-md"
min="0"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step3.unit')} *
</label>
<input
type="text"
value={data.unit}
onChange={(e) => updateData({ unit: e.target.value })}
className="w-full p-2 border rounded-md"
placeholder="MWh, kg, m³..."
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step3.temporalUnit')}
</label>
<select
value={data.temporal_unit || ''}
onChange={(e) => updateData({ temporal_unit: e.target.value })}
className="w-full p-2 border rounded-md"
>
<option value="">{t('resourceFlowWizard.step3.selectTemporal')}</option>
<option value="per_hour">{t('temporalUnits.perHour')}</option>
<option value="per_day">{t('temporalUnits.perDay')}</option>
<option value="per_month">{t('temporalUnits.perMonth')}</option>
<option value="per_year">{t('temporalUnits.perYear')}</option>
</select>
</div>
</Stack>
);
case 4:
return (
<Stack spacing="md">
<h3 className="text-lg font-medium">{t('resourceFlowWizard.step4.title')}</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step4.costIn')}
</label>
<input
type="number"
value={data.cost_in || ''}
onChange={(e) => updateData({ cost_in: parseFloat(e.target.value) || undefined })}
className="w-full p-2 border rounded-md"
placeholder="€/unit"
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step4.costOut')}
</label>
<input
type="number"
value={data.cost_out || ''}
onChange={(e) =>
updateData({ cost_out: parseFloat(e.target.value) || undefined })
}
className="w-full p-2 border rounded-md"
placeholder="€/unit"
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step4.wasteCost')}
</label>
<input
type="number"
value={data.waste_disposal_cost || ''}
onChange={(e) =>
updateData({ waste_disposal_cost: parseFloat(e.target.value) || undefined })
}
className="w-full p-2 border rounded-md"
placeholder="€/unit"
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step4.transportCost')}
</label>
<input
type="number"
value={data.transportation_cost || ''}
onChange={(e) =>
updateData({ transportation_cost: parseFloat(e.target.value) || undefined })
}
className="w-full p-2 border rounded-md"
placeholder="€/km"
min="0"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step4.precision')}
</label>
<select
value={data.precision_level}
onChange={(e) => updateData({ precision_level: e.target.value })}
className="w-full p-2 border rounded-md"
>
<option value="measured">{t('precisionLevels.measured')}</option>
<option value="estimated">{t('precisionLevels.estimated')}</option>
<option value="rough">{t('precisionLevels.rough')}</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('resourceFlowWizard.step4.source')}
</label>
<select
value={data.source_type}
onChange={(e) => updateData({ source_type: e.target.value })}
className="w-full p-2 border rounded-md"
>
<option value="device">{t('sourceTypes.device')}</option>
<option value="declared">{t('sourceTypes.declared')}</option>
<option value="calculated">{t('sourceTypes.calculated')}</option>
</select>
</div>
</div>
</Stack>
);
default:
return null;
}
};
const wizardTitle = `${direction === 'input' ? t('resourceFlowWizard.titleInput') : t('resourceFlowWizard.titleOutput')} - ${t('resourceFlowWizard.step', { current: currentStep, total: totalSteps })}`;
return (
<Wizard isOpen={isOpen} onClose={onClose} title={wizardTitle}>
<Stack spacing="lg">
{error && <ErrorMessage message={error} />}
{renderStepContent()}
<Flex justify="between" className="pt-4 border-t">
<Button variant="outline" onClick={currentStep === 1 ? onClose : handlePrevious}>
{currentStep === 1 ? t('common.cancel') : t('wizard.previous')}
</Button>
<Flex gap="xs">
<span className="text-sm text-muted-foreground self-center">
{currentStep} / {totalSteps}
</span>
{currentStep < totalSteps ? (
<Button onClick={handleNext} disabled={!canProceed()}>
{t('wizard.next')}
</Button>
) : (
<Button
onClick={handleSubmit}
disabled={!canProceed() || createResourceFlowMutation.isPending}
>
{createResourceFlowMutation.isPending
? t('common.creating')
: t('resourceFlowWizard.create')}
</Button>
)}
</Flex>
</Flex>
</Stack>
</Wizard>
);
};
export default ResourceFlowWizard;