mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Remove nested git repository from bugulma/frontend/.git - Add all frontend files to main repository tracking - Convert from separate frontend/backend repos to unified monorepo - Preserve all frontend code and development history as tracked files - Eliminate nested repository complexity for simpler development workflow This creates a proper monorepo structure with frontend and backend coexisting in the same repository for easier development and deployment.
440 lines
16 KiB
TypeScript
440 lines
16 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
|
import { useCreateResourceFlow } from '@/hooks/api/useResourcesAPI.ts';
|
|
import { useSitesByOrganization } from '@/hooks/api/useSitesAPI.ts';
|
|
import Wizard from '@/components/wizard/Wizard.tsx';
|
|
import Button from '@/components/ui/Button.tsx';
|
|
import { Flex, Stack } from '@/components/ui/layout';
|
|
import ErrorMessage from '@/components/ui/ErrorMessage.tsx';
|
|
import type { ResourceDirection } from '@/schemas/backend/resource-flow';
|
|
|
|
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,
|
|
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;
|