Files
zentral/frontend/src/modules/production/pages/ProductionOrderFormPage.tsx
2025-12-01 10:00:40 +01:00

440 lines
15 KiB
TypeScript

import { useState, useEffect } from "react";
import { useForm, Controller } from "react-hook-form";
import { useNavigate, useParams } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Box,
Button,
Paper,
Typography,
Grid,
TextField,
Autocomplete,
Alert,
Chip,
FormControlLabel,
Checkbox,
Tabs,
Tab,
IconButton,
} from "@mui/material";
import { DataGrid } from "@mui/x-data-grid";
import {
Save as SaveIcon,
ArrowBack as BackIcon,
Check as CheckIcon,
PlayArrow as StartIcon,
Done as CompleteIcon,
Visibility as ViewIcon,
} from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { productionService } from "../services/productionService";
import { articleService } from "../../warehouse/services/warehouseService";
import { CreateProductionOrderDto, UpdateProductionOrderDto, ProductionOrderStatus } from "../types";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import dayjs from "dayjs";
import ProductionOrderPhases from "../components/ProductionOrderPhases";
export default function ProductionOrderFormPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const { id } = useParams();
const isEdit = Boolean(id);
const queryClient = useQueryClient();
const [tabValue, setTabValue] = useState(0);
const { control, handleSubmit, reset, watch, setValue, formState: { errors } } = useForm<CreateProductionOrderDto>({
defaultValues: {
articleId: 0,
quantity: 1,
startDate: new Date().toISOString(),
dueDate: new Date().toISOString(),
notes: "",
billOfMaterialsId: undefined,
},
});
const { data: order, isLoading } = useQuery({
queryKey: ["production-order", id],
queryFn: () => productionService.getProductionOrderById(Number(id)),
enabled: isEdit,
});
const { data: articles = [] } = useQuery({
queryKey: ["articles"],
queryFn: () => articleService.getAll(),
});
const { data: boms = [] } = useQuery({
queryKey: ["boms"],
queryFn: () => productionService.getBillOfMaterials(),
});
useEffect(() => {
if (order) {
reset({
articleId: order.articleId,
quantity: order.quantity,
startDate: order.startDate,
dueDate: order.dueDate,
notes: order.notes,
});
}
}, [order, reset]);
const createMutation = useMutation({
mutationFn: (data: CreateProductionOrderDto) => productionService.createProductionOrder(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["production-orders"] });
navigate("/production/orders");
},
});
const updateMutation = useMutation({
mutationFn: (data: UpdateProductionOrderDto) => productionService.updateProductionOrder(Number(id), data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["production-orders"] });
queryClient.invalidateQueries({ queryKey: ["production-order", id] });
navigate("/production/orders");
},
});
const changeStatusMutation = useMutation({
mutationFn: (status: ProductionOrderStatus) => productionService.changeProductionOrderStatus(Number(id), status),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["production-orders"] });
queryClient.invalidateQueries({ queryKey: ["production-order", id] });
},
});
const onSubmit = (data: CreateProductionOrderDto) => {
if (isEdit) {
// For update we need to pass status as well, but form doesn't have it.
// We assume status doesn't change via form save, only via actions.
const updateData: UpdateProductionOrderDto = {
quantity: data.quantity,
startDate: data.startDate,
dueDate: data.dueDate,
notes: data.notes,
status: order!.status // Keep existing status
};
updateMutation.mutate(updateData);
} else {
createMutation.mutate(data);
}
};
const handleArticleChange = (articleId: number | null) => {
if (!articleId) return;
setValue('articleId', articleId);
// Try to find a BOM for this article to auto-select
const bom = boms.find(b => b.articleId === articleId);
if (bom) {
setValue('billOfMaterialsId', bom.id);
}
};
const isReadOnly = isEdit && order?.status === ProductionOrderStatus.Completed;
if (isEdit && isLoading) return <Typography>Loading...</Typography>;
return (
<Box sx={{ p: 3, maxWidth: 800, mx: "auto" }}>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mb: 3 }}>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Button
startIcon={<BackIcon />}
onClick={() => navigate("/production/orders")}
sx={{ mr: 2 }}
>
{t("common.back")}
</Button>
<Box>
<Typography variant="h4">
{isEdit ? `${t("production.order.editTitle")} ${order?.code}` : t("production.order.createTitle")}
</Typography>
{isEdit && order && (
<Chip
label={t(`production.order.status.${ProductionOrderStatus[order.status]}`)}
color={order.status === ProductionOrderStatus.Completed ? "success" : "default"}
size="small"
sx={{ mt: 1 }}
/>
)}
</Box>
</Box>
<Box sx={{ display: "flex", gap: 2 }}>
{isEdit && order?.status === ProductionOrderStatus.Draft && (
<Button
variant="contained"
color="info"
startIcon={<CheckIcon />}
onClick={() => changeStatusMutation.mutate(ProductionOrderStatus.Planned)}
>
{t("production.order.actions.plan")}
</Button>
)}
{isEdit && order?.status === ProductionOrderStatus.Planned && (
<Button
variant="contained"
color="primary"
startIcon={<StartIcon />}
onClick={() => changeStatusMutation.mutate(ProductionOrderStatus.Released)}
>
{t("production.order.actions.release")}
</Button>
)}
{isEdit && order?.status === ProductionOrderStatus.Released && (
<Button
variant="contained"
color="warning"
startIcon={<StartIcon />}
onClick={() => changeStatusMutation.mutate(ProductionOrderStatus.InProgress)}
>
{t("production.order.actions.start")}
</Button>
)}
{isEdit && order?.status === ProductionOrderStatus.InProgress && (
<Button
variant="contained"
color="success"
startIcon={<CompleteIcon />}
onClick={() => changeStatusMutation.mutate(ProductionOrderStatus.Completed)}
>
{t("production.order.actions.complete")}
</Button>
)}
{!isReadOnly && (
<Button
variant="contained"
startIcon={<SaveIcon />}
onClick={handleSubmit(onSubmit)}
disabled={createMutation.isPending || updateMutation.isPending}
>
{t("common.save")}
</Button>
)}
</Box>
</Box>
{(createMutation.isError || updateMutation.isError || changeStatusMutation.isError) && (
<Alert severity="error" sx={{ mb: 3 }}>
{t("common.error")}
</Alert>
)}
<Paper sx={{ p: 3, mb: 3 }}>
<Grid container spacing={3}>
<Grid size={{ xs: 12 }}>
<Controller
name="articleId"
control={control}
rules={{ required: t("common.required") }}
render={({ field }: { field: any }) => (
<Autocomplete
options={articles}
getOptionLabel={(option) => `${option.code} - ${option.description}`}
value={articles.find(a => a.id === field.value) || null}
onChange={(_, newValue) => handleArticleChange(newValue?.id || null)}
disabled={isEdit || isReadOnly} // Cannot change article after creation
renderInput={(params) => (
<TextField
{...params}
label={t("production.order.fields.article")}
error={!!errors.articleId}
helperText={errors.articleId?.message}
/>
)}
/>
)}
/>
</Grid>
{!isEdit && (
<Grid size={{ xs: 12 }}>
<Controller
name="billOfMaterialsId"
control={control}
render={({ field }: { field: any }) => (
<Autocomplete
options={boms.filter(b => b.articleId === watch('articleId'))}
getOptionLabel={(option) => option.name}
value={boms.find(b => b.id === field.value) || null}
onChange={(_, newValue) => field.onChange(newValue?.id)}
disabled={isReadOnly}
renderInput={(params) => (
<TextField
{...params}
label={t("production.order.fields.bom")}
helperText={t("production.order.fields.bomHelp")}
/>
)}
/>
)}
/>
</Grid>
)}
{!isEdit && (
<Grid size={{ xs: 12 }}>
<Controller
name="createChildOrders"
control={control}
render={({ field }: { field: any }) => (
<FormControlLabel
control={
<Checkbox
checked={field.value}
onChange={(e) => field.onChange(e.target.checked)}
/>
}
label={t("production.order.fields.createChildOrders")}
/>
)}
/>
</Grid>
)}
<Grid size={{ xs: 12, md: 4 }}>
<Controller
name="quantity"
control={control}
rules={{ required: t("common.required"), min: 0.0001 }}
render={({ field }: { field: any }) => (
<TextField
{...field}
label={t("production.order.fields.quantity")}
type="number"
fullWidth
disabled={isReadOnly}
onChange={(e) => field.onChange(Number(e.target.value))}
error={!!errors.quantity}
helperText={errors.quantity?.message}
/>
)}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<Controller
name="startDate"
control={control}
render={({ field }: { field: any }) => (
<DatePicker
label={t("production.order.fields.startDate")}
value={field.value ? dayjs(field.value) : null}
onChange={(date) => field.onChange(date?.toISOString())}
disabled={isReadOnly}
slotProps={{ textField: { fullWidth: true } }}
/>
)}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<Controller
name="dueDate"
control={control}
render={({ field }: { field: any }) => (
<DatePicker
label={t("production.order.fields.dueDate")}
value={field.value ? dayjs(field.value) : null}
onChange={(date) => field.onChange(date?.toISOString())}
disabled={isReadOnly}
slotProps={{ textField: { fullWidth: true } }}
/>
)}
/>
</Grid>
<Grid size={{ xs: 12 }}>
<Controller
name="notes"
control={control}
render={({ field }: { field: any }) => (
<TextField
{...field}
label={t("production.order.fields.notes")}
fullWidth
multiline
rows={3}
disabled={isReadOnly}
/>
)}
/>
</Grid>
</Grid>
</Paper>
{isEdit && order && (
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={(_, val) => setTabValue(val)}>
<Tab label={t("production.order.phases.title")} />
<Tab label={t("production.order.subOrders")} />
</Tabs>
</Box>
<TabPanel value={tabValue} index={0}>
<ProductionOrderPhases order={order} isReadOnly={isReadOnly} />
</TabPanel>
<TabPanel value={tabValue} index={1}>
{order.childOrders && order.childOrders.length > 0 ? (
<Paper sx={{ width: '100%' }}>
<DataGrid
rows={order.childOrders}
columns={[
{ field: "code", headerName: t("production.order.columns.code"), width: 150 },
{ field: "articleName", headerName: t("production.order.columns.article"), flex: 1 },
{ field: "quantity", headerName: t("production.order.columns.quantity"), width: 100 },
{ field: "status", headerName: t("production.order.columns.status"), width: 150, valueGetter: (_value, row: any) => t(`production.order.status.${ProductionOrderStatus[row.status]}`) },
{
field: "actions",
headerName: t("common.actions"),
width: 100,
renderCell: (params: any) => (
<IconButton size="small" onClick={() => navigate(`/production/orders/${params.row.id}`)}>
<ViewIcon />
</IconButton>
)
}
]}
hideFooter
autoHeight
/>
</Paper>
) : (
<Typography sx={{ p: 2 }}>{t("production.order.noSubOrders")}</Typography>
)}
</TabPanel>
</Box>
)}
</Box>
);
}
function TabPanel(props: { children?: React.ReactNode; index: number; value: number }) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
{children}
</Box>
)}
</div>
);
}