Files
Frontend/src/app/(dashboard)/(private)/fertilization-plan/page.tsx
T

408 lines
15 KiB
TypeScript
Raw Normal View History

2026-05-02 06:23:34 +03:30
"use client";
2026-05-02 16:31:23 +03:30
import { useCallback, useEffect, useMemo, useState } from "react";
2026-05-02 06:23:34 +03:30
import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Chip from "@mui/material/Chip";
import Divider from "@mui/material/Divider";
import IconButton from "@mui/material/IconButton";
2026-05-02 16:31:23 +03:30
import LinearProgress from "@mui/material/LinearProgress";
2026-05-02 06:23:34 +03:30
import Stack from "@mui/material/Stack";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
2026-04-30 04:00:19 +03:30
import FertilizationPlanParserPage from "@views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage";
2026-05-02 06:23:34 +03:30
import RelatedPlanSelector, {
type RelatedPlanItem,
} from "@views/dashboards/farm/planSelector/RelatedPlanSelector";
2026-05-02 16:31:23 +03:30
import { useFarmHub } from "@/hooks/useFarmHub";
import {
fertilizationPlanService,
type FertilizationPlanDetail,
type FertilizationPlanListItem,
} from "@/libs/api/services/fertilizationPlanService";
import { irrigationPlanService } from "@/libs/api/services/irrigationPlanService";
2026-05-02 06:23:34 +03:30
import CustomTextField from "@core/components/mui/TextField";
type FertilizationPlanRow = {
2026-05-02 16:31:23 +03:30
id: string;
2026-05-02 06:23:34 +03:30
planName: string;
finalProduct: string;
harvestTime: string;
outputTon: number;
fertilizerType: string;
status: "active" | "draft";
2026-05-02 16:31:23 +03:30
sourceLabel: string;
growthStage: string;
2026-05-02 06:23:34 +03:30
};
2026-05-02 16:31:23 +03:30
const PAGE_SIZE = 10;
const formatDate = (value?: string | null) => {
if (!value) return "—";
try {
return new Intl.DateTimeFormat("fa-IR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date(value));
} catch {
return value;
}
};
const mapFertilizationPlanRow = (
plan: FertilizationPlanListItem,
detail?: FertilizationPlanDetail | null,
): FertilizationPlanRow => ({
id: plan.plan_uuid,
planName: plan.title,
finalProduct: plan.plant_name || plan.crop_id || "—",
harvestTime: formatDate(plan.created_at),
outputTon: 0,
fertilizerType: detail?.plan_payload?.items?.[0]?.name || plan.source_label || "—",
status: plan.is_active ? "active" : "draft",
sourceLabel: plan.source_label || "—",
growthStage: plan.growth_stage || "—",
});
2026-04-30 04:00:19 +03:30
const FertilizationPlanPage = () => {
2026-05-02 16:31:23 +03:30
const { getFarmUuid } = useFarmHub();
const [plans, setPlans] = useState<FertilizationPlanRow[]>([]);
2026-05-02 06:23:34 +03:30
const [minOutputTon, setMinOutputTon] = useState("0");
2026-05-02 16:31:23 +03:30
const [selectedPlan, setSelectedPlan] = useState<FertilizationPlanRow | null>(null);
2026-05-02 06:23:34 +03:30
const [detailsOpen, setDetailsOpen] = useState(false);
2026-05-02 16:31:23 +03:30
const [loading, setLoading] = useState(false);
const [detailsLoading, setDetailsLoading] = useState(false);
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [relatedPlans, setRelatedPlans] = useState<RelatedPlanItem[]>([]);
const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState<number | string | null>(
null,
2026-05-02 06:23:34 +03:30
);
const filteredPlans = useMemo(() => {
const minTon = Number(minOutputTon);
return plans
.filter((plan) => plan.outputTon >= minTon)
.sort((firstPlan, secondPlan) => secondPlan.outputTon - firstPlan.outputTon);
}, [minOutputTon, plans]);
2026-05-02 16:31:23 +03:30
const loadPlans = useCallback(async () => {
const farmUuid = getFarmUuid();
if (!farmUuid) {
setError("ابتدا مزرعه فعال را انتخاب کنید.");
setPlans([]);
setSelectedPlan(null);
return;
}
setLoading(true);
setError(null);
try {
const response = await fertilizationPlanService.getPlans(farmUuid, 1, PAGE_SIZE);
const mappedPlans = response.data.map((plan) => mapFertilizationPlanRow(plan));
setPlans(mappedPlans);
setSelectedPlan((current) =>
mappedPlans.find((plan) => plan.id === current?.id) ?? mappedPlans[0] ?? null,
);
} catch (loadError) {
setError(
loadError instanceof Error
? loadError.message
: "دریافت لیست برنامه‌های کوددهی انجام نشد.",
);
setPlans([]);
setSelectedPlan(null);
} finally {
setLoading(false);
}
}, [getFarmUuid]);
useEffect(() => {
loadPlans();
}, [loadPlans]);
2026-05-02 06:23:34 +03:30
2026-05-02 16:31:23 +03:30
const loadRelatedPlans = useCallback(async () => {
const farmUuid = getFarmUuid();
2026-05-02 06:23:34 +03:30
2026-05-02 16:31:23 +03:30
if (!farmUuid) {
setRelatedPlans([]);
setSelectedRelatedPlanId(null);
return;
}
const response = await irrigationPlanService.getPlans(farmUuid, 1, PAGE_SIZE);
const items: RelatedPlanItem[] = response.data.map((plan) => ({
id: plan.plan_uuid,
title: plan.title,
finalProduct: plan.plant_name || plan.crop_id || "—",
harvestTime: formatDate(plan.created_at),
outputTon: 0,
methodLabel: `منبع: ${plan.source_label || "—"}`,
status: plan.is_active ? "active" : "draft",
}));
setRelatedPlans(items);
setSelectedRelatedPlanId(items[0]?.id ?? null);
}, [getFarmUuid]);
const handleActivate = async (planId: string) => {
setActionLoadingId(planId);
try {
await fertilizationPlanService.updatePlanStatus(planId, true);
setPlans((currentPlans) =>
currentPlans.map((plan) => ({
...plan,
status: plan.id === planId ? "active" : "draft",
})),
);
setSelectedPlan((currentPlan) =>
currentPlan && currentPlan.id === planId
? { ...currentPlan, status: "active" }
: currentPlan,
);
} catch (updateError) {
setError(
updateError instanceof Error
? updateError.message
: "تغییر وضعیت برنامه کوددهی انجام نشد.",
);
} finally {
setActionLoadingId(null);
2026-05-02 06:23:34 +03:30
}
};
2026-05-02 16:31:23 +03:30
const handleDelete = async (planId: string) => {
setActionLoadingId(planId);
2026-05-02 06:23:34 +03:30
2026-05-02 16:31:23 +03:30
try {
await fertilizationPlanService.deletePlan(planId);
setPlans((currentPlans) => currentPlans.filter((plan) => plan.id !== planId));
if (selectedPlan?.id === planId) {
setSelectedPlan(null);
}
} catch (deleteError) {
setError(
deleteError instanceof Error
? deleteError.message
: "حذف برنامه کوددهی انجام نشد.",
);
} finally {
setActionLoadingId(null);
2026-05-02 06:23:34 +03:30
}
};
2026-05-02 16:31:23 +03:30
const handleShowDetails = async (plan: FertilizationPlanRow) => {
2026-05-02 06:23:34 +03:30
setSelectedPlan(plan);
setDetailsOpen(true);
2026-05-02 16:31:23 +03:30
setDetailsLoading(true);
2026-05-02 06:23:34 +03:30
2026-05-02 16:31:23 +03:30
try {
const detail = await fertilizationPlanService.getPlanDetail(plan.id);
setSelectedPlan(mapFertilizationPlanRow(detail, detail));
await loadRelatedPlans();
} catch (detailError) {
setError(
detailError instanceof Error
? detailError.message
: "دریافت جزئیات برنامه کوددهی انجام نشد.",
);
setDetailsOpen(false);
} finally {
setDetailsLoading(false);
}
2026-05-02 06:23:34 +03:30
};
return (
<>
<Stack spacing={6}>
<FertilizationPlanParserPage />
<Card>
<CardContent>
<Stack spacing={4}>
2026-05-02 16:31:23 +03:30
{loading ? <LinearProgress sx={{ borderRadius: 999 }} /> : null}
{error ? <Alert severity="error">{error}</Alert> : null}
2026-05-02 06:23:34 +03:30
<Stack
direction={{ xs: "column", md: "row" }}
spacing={3}
alignItems={{ xs: "stretch", md: "center" }}
justifyContent="space-between"
>
<Box>
<Typography variant="h5">لیست برنامههای کوددهی</Typography>
<Typography color="text.secondary">
2026-05-02 16:31:23 +03:30
لیست برنامههای کوددهی ثبتشده برای مزرعه فعال
2026-05-02 06:23:34 +03:30
</Typography>
</Box>
<CustomTextField
select
value={minOutputTon}
onChange={(event) => setMinOutputTon(event.target.value)}
label="فیلتر تن خروجی محصول"
sx={{ minWidth: { xs: "100%", md: 260 } }}
SelectProps={{ native: true }}
>
<option value="0">همه برنامهها</option>
<option value="30">بیشتر از ۳۰ تن</option>
<option value="40">بیشتر از ۴۰ تن</option>
<option value="50">بیشتر از ۵۰ تن</option>
</CustomTextField>
</Stack>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>نام برنامه</TableCell>
<TableCell>محصول نهایی</TableCell>
2026-05-02 16:31:23 +03:30
<TableCell>تاریخ ایجاد</TableCell>
<TableCell>مرحله رشد</TableCell>
<TableCell>نوع کود / منبع</TableCell>
2026-05-02 06:23:34 +03:30
<TableCell>وضعیت</TableCell>
<TableCell align="center">عملیات</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredPlans.map((plan) => (
<TableRow hover key={plan.id}>
<TableCell>{plan.planName}</TableCell>
<TableCell>{plan.finalProduct}</TableCell>
<TableCell>{plan.harvestTime}</TableCell>
2026-05-02 16:31:23 +03:30
<TableCell>{plan.growthStage}</TableCell>
2026-05-02 06:23:34 +03:30
<TableCell>{plan.fertilizerType}</TableCell>
<TableCell>
<Chip
size="small"
label={plan.status === "active" ? "فعال" : "پیش‌نویس"}
color={plan.status === "active" ? "success" : "default"}
variant={plan.status === "active" ? "filled" : "tonal"}
/>
</TableCell>
<TableCell align="center">
<Stack direction="row" spacing={1} justifyContent="center">
<Tooltip title="فعال‌سازی برنامه">
<span>
<IconButton
color="success"
onClick={() => handleActivate(plan.id)}
2026-05-02 16:31:23 +03:30
disabled={plan.status === "active" || actionLoadingId === plan.id}
2026-05-02 06:23:34 +03:30
>
<i className="tabler-player-play text-[20px]" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="مشاهده جزئیات">
<IconButton
color="primary"
onClick={() => handleShowDetails(plan)}
>
<i className="tabler-eye text-[20px]" />
</IconButton>
</Tooltip>
<Tooltip title="حذف برنامه">
<IconButton
color="error"
onClick={() => handleDelete(plan.id)}
2026-05-02 16:31:23 +03:30
disabled={actionLoadingId === plan.id}
2026-05-02 06:23:34 +03:30
>
<i className="tabler-trash text-[20px]" />
</IconButton>
</Tooltip>
</Stack>
</TableCell>
</TableRow>
))}
{filteredPlans.length === 0 ? (
<TableRow>
<TableCell colSpan={7}>
<Alert severity="info">
برنامهای با این فیلتر پیدا نشد.
</Alert>
</TableCell>
</TableRow>
) : null}
</TableBody>
</Table>
</TableContainer>
<Divider />
<Stack spacing={2}>
<Typography variant="h6">جزئیات برنامه انتخابشده</Typography>
{selectedPlan ? (
<Stack
direction={{ xs: "column", md: "row" }}
spacing={2}
useFlexGap
flexWrap="wrap"
>
<Chip label={`محصول نهایی: ${selectedPlan.finalProduct}`} variant="tonal" />
2026-05-02 16:31:23 +03:30
<Chip label={`تاریخ ایجاد: ${selectedPlan.harvestTime}`} variant="tonal" />
<Chip label={`مرحله رشد: ${selectedPlan.growthStage}`} variant="tonal" />
2026-05-02 06:23:34 +03:30
<Chip label={`نوع کود: ${selectedPlan.fertilizerType}`} variant="tonal" />
</Stack>
) : (
<Alert severity="warning">
برای دیدن جزئیات، یکی از برنامههای کوددهی را انتخاب کنید.
</Alert>
)}
</Stack>
</Stack>
</CardContent>
</Card>
</Stack>
2026-05-02 16:31:23 +03:30
{detailsLoading ? <LinearProgress sx={{ borderRadius: 999, mt: 2 }} /> : null}
2026-05-02 06:23:34 +03:30
{selectedPlan ? (
<RelatedPlanSelector
open={detailsOpen}
onClose={() => setDetailsOpen(false)}
title="جزییات برنامه کوددهی"
description="برای تکمیل اجرای این برنامه کوددهی، یکی از برنامه‌های آبیاری مرتبط را انتخاب کنید."
currentPlanName={selectedPlan.planName}
currentPlanStatus={selectedPlan.status === "active" ? "برنامه فعال" : "آماده برای فعال‌سازی"}
2026-05-02 16:31:23 +03:30
currentPlanOutput={`منبع: ${selectedPlan.sourceLabel}`}
2026-05-02 06:23:34 +03:30
summaryItems={[
{ label: "محصول نهایی", value: selectedPlan.finalProduct },
2026-05-02 16:31:23 +03:30
{ label: "تاریخ ایجاد", value: selectedPlan.harvestTime },
{ label: "جزئیات", value: selectedPlan.fertilizerType },
2026-05-02 06:23:34 +03:30
]}
relatedTitle="برنامه‌های آبیاری پیشنهادی"
relatedDescription="از بین برنامه‌های زیر، مناسب‌ترین برنامه آبیاری را برای این برنامه کوددهی انتخاب کنید."
2026-05-02 16:31:23 +03:30
relatedPlans={relatedPlans}
2026-05-02 06:23:34 +03:30
selectedRelatedPlanId={selectedRelatedPlanId}
2026-05-02 16:31:23 +03:30
onSelectRelatedPlan={(planId) => setSelectedRelatedPlanId(planId)}
onConfirm={() => setDetailsOpen(false)}
2026-05-02 06:23:34 +03:30
/>
) : null}
</>
);
2026-04-30 04:00:19 +03:30
};
export default FertilizationPlanPage;