UPDATE
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
"irrigationRecommendation": "توصیه آبیاری",
|
"irrigationRecommendation": "توصیه آبیاری",
|
||||||
"fertilizationRecommendation": "توصیه کوددهی",
|
"fertilizationRecommendation": "توصیه کوددهی",
|
||||||
"fertilizationPlanParser": "برنامه آبیاری و کودهی",
|
"fertilizationPlanParser": "برنامه آبیاری و کودهی",
|
||||||
|
"irrigationPlanParser": "برنامه آبیاری",
|
||||||
"aiAssistant": "دستیار هوشمند",
|
"aiAssistant": "دستیار هوشمند",
|
||||||
"farmAiAssistant": "دستیار هوشمند مزرعه",
|
"farmAiAssistant": "دستیار هوشمند مزرعه",
|
||||||
"pestDetection": "تشخیص آفات گیاهی",
|
"pestDetection": "تشخیص آفات گیاهی",
|
||||||
|
|||||||
@@ -1,7 +1,361 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
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";
|
||||||
|
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";
|
||||||
|
|
||||||
import FertilizationPlanParserPage from "@views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage";
|
import FertilizationPlanParserPage from "@views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage";
|
||||||
|
import RelatedPlanSelector, {
|
||||||
|
type RelatedPlanItem,
|
||||||
|
} from "@views/dashboards/farm/planSelector/RelatedPlanSelector";
|
||||||
|
|
||||||
|
import CustomTextField from "@core/components/mui/TextField";
|
||||||
|
|
||||||
|
type FertilizationPlanRow = {
|
||||||
|
id: number;
|
||||||
|
planName: string;
|
||||||
|
finalProduct: string;
|
||||||
|
harvestTime: string;
|
||||||
|
outputTon: number;
|
||||||
|
fertilizerType: string;
|
||||||
|
status: "active" | "draft";
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFertilizationPlans: FertilizationPlanRow[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
planName: "برنامه کوددهی گوجه فرنگی گلخانهای",
|
||||||
|
finalProduct: "گوجه فرنگی ممتاز",
|
||||||
|
harvestTime: "۱۴۰۴/۰۶/۱۲",
|
||||||
|
outputTon: 52,
|
||||||
|
fertilizerType: "NPK 20-20-20",
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
planName: "برنامه کوددهی فلفل دلمهای",
|
||||||
|
finalProduct: "فلفل دلمهای رنگی",
|
||||||
|
harvestTime: "۱۴۰۴/۰۵/۲۰",
|
||||||
|
outputTon: 34,
|
||||||
|
fertilizerType: "نیترات کلسیم",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
planName: "برنامه کوددهی خیار گلخانهای",
|
||||||
|
finalProduct: "خیار ممتاز صادراتی",
|
||||||
|
harvestTime: "۱۴۰۴/۰۴/۲۸",
|
||||||
|
outputTon: 41,
|
||||||
|
fertilizerType: "سولفات پتاسیم",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
planName: "برنامه کوددهی هندوانه",
|
||||||
|
finalProduct: "هندوانه شیرین بازارپسند",
|
||||||
|
harvestTime: "۱۴۰۴/۰۵/۲۹",
|
||||||
|
outputTon: 57,
|
||||||
|
fertilizerType: "اوره + هیومیک اسید",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const relatedIrrigationPlans: RelatedPlanItem[] = [
|
||||||
|
{
|
||||||
|
id: 301,
|
||||||
|
title: "آبیاری قطرهای صبحگاهی",
|
||||||
|
finalProduct: "گوجه فرنگی ممتاز",
|
||||||
|
harvestTime: "۱۴۰۴/۰۶/۰۸",
|
||||||
|
outputTon: 45,
|
||||||
|
methodLabel: "روش آبیاری: قطرهای",
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 302,
|
||||||
|
title: "آبیاری تنظیمشده مرحله گلدهی",
|
||||||
|
finalProduct: "گوجه فرنگی ممتاز",
|
||||||
|
harvestTime: "۱۴۰۴/۰۶/۱۰",
|
||||||
|
outputTon: 49,
|
||||||
|
methodLabel: "روش آبیاری: تیپ",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 303,
|
||||||
|
title: "آبیاری تقویتی پیش از برداشت",
|
||||||
|
finalProduct: "گوجه فرنگی ممتاز",
|
||||||
|
harvestTime: "۱۴۰۴/۰۶/۱۴",
|
||||||
|
outputTon: 53,
|
||||||
|
methodLabel: "روش آبیاری: بارانی سبک",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const FertilizationPlanPage = () => {
|
const FertilizationPlanPage = () => {
|
||||||
return <FertilizationPlanParserPage />;
|
const router = useRouter();
|
||||||
|
const [plans, setPlans] = useState(mockFertilizationPlans);
|
||||||
|
const [minOutputTon, setMinOutputTon] = useState("0");
|
||||||
|
const [selectedPlan, setSelectedPlan] = useState<FertilizationPlanRow | null>(
|
||||||
|
mockFertilizationPlans[0],
|
||||||
|
);
|
||||||
|
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||||
|
const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState<number | null>(
|
||||||
|
relatedIrrigationPlans[0]?.id ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredPlans = useMemo(() => {
|
||||||
|
const minTon = Number(minOutputTon);
|
||||||
|
|
||||||
|
return plans
|
||||||
|
.filter((plan) => plan.outputTon >= minTon)
|
||||||
|
.sort((firstPlan, secondPlan) => secondPlan.outputTon - firstPlan.outputTon);
|
||||||
|
}, [minOutputTon, plans]);
|
||||||
|
|
||||||
|
const handleActivate = (planId: number) => {
|
||||||
|
setPlans((currentPlans) =>
|
||||||
|
currentPlans.map((plan) => ({
|
||||||
|
...plan,
|
||||||
|
status: plan.id === planId ? "active" : "draft",
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const activePlan = plans.find((plan) => plan.id === planId);
|
||||||
|
|
||||||
|
if (activePlan) {
|
||||||
|
setSelectedPlan({ ...activePlan, status: "active" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (planId: number) => {
|
||||||
|
setPlans((currentPlans) => currentPlans.filter((plan) => plan.id !== planId));
|
||||||
|
|
||||||
|
if (selectedPlan?.id === planId) {
|
||||||
|
setSelectedPlan(null);
|
||||||
|
setDetailsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowDetails = (plan: FertilizationPlanRow) => {
|
||||||
|
setSelectedPlan(plan);
|
||||||
|
setSelectedRelatedPlanId(relatedIrrigationPlans[0]?.id ?? null);
|
||||||
|
setDetailsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmRelatedPlan = (relatedPlan: RelatedPlanItem) => {
|
||||||
|
setDetailsOpen(false);
|
||||||
|
if (!selectedPlan) return;
|
||||||
|
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
from: "fertilization-plan",
|
||||||
|
planType: "fertilization",
|
||||||
|
planId: String(selectedPlan.id),
|
||||||
|
planName: selectedPlan.planName,
|
||||||
|
relatedType: "irrigation",
|
||||||
|
relatedPlanId: String(relatedPlan.id),
|
||||||
|
relatedPlanName: relatedPlan.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push(`/yield-harvest?${query.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack spacing={6}>
|
||||||
|
<FertilizationPlanParserPage />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<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">
|
||||||
|
نمایش همه برنامهها با داده ماک و امکان فیلتر بر اساس بیشترین تن خروجی محصول
|
||||||
|
</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>
|
||||||
|
<TableCell>زمان برداشت</TableCell>
|
||||||
|
<TableCell>تن خروجی</TableCell>
|
||||||
|
<TableCell>نوع کود</TableCell>
|
||||||
|
<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>
|
||||||
|
<TableCell>{plan.outputTon} تن</TableCell>
|
||||||
|
<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)}
|
||||||
|
disabled={plan.status === "active"}
|
||||||
|
>
|
||||||
|
<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)}
|
||||||
|
>
|
||||||
|
<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" />
|
||||||
|
<Chip label={`زمان برداشت: ${selectedPlan.harvestTime}`} variant="tonal" />
|
||||||
|
<Chip label={`تن خروجی: ${selectedPlan.outputTon} تن`} variant="tonal" />
|
||||||
|
<Chip label={`نوع کود: ${selectedPlan.fertilizerType}`} variant="tonal" />
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<i className="tabler-list-details" />}
|
||||||
|
onClick={() => handleShowDetails(selectedPlan)}
|
||||||
|
>
|
||||||
|
مشاهده و انتخاب برنامه آبیاری
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
startIcon={<i className="tabler-check" />}
|
||||||
|
onClick={() => handleActivate(selectedPlan.id)}
|
||||||
|
>
|
||||||
|
فعال کردن این برنامه
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Alert severity="warning">
|
||||||
|
برای دیدن جزئیات، یکی از برنامههای کوددهی را انتخاب کنید.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{selectedPlan ? (
|
||||||
|
<RelatedPlanSelector
|
||||||
|
open={detailsOpen}
|
||||||
|
onClose={() => setDetailsOpen(false)}
|
||||||
|
title="جزییات برنامه کوددهی"
|
||||||
|
description="برای تکمیل اجرای این برنامه کوددهی، یکی از برنامههای آبیاری مرتبط را انتخاب کنید."
|
||||||
|
currentPlanName={selectedPlan.planName}
|
||||||
|
currentPlanStatus={selectedPlan.status === "active" ? "برنامه فعال" : "آماده برای فعالسازی"}
|
||||||
|
currentPlanOutput={`خروجی هدف: ${selectedPlan.outputTon} تن محصول نهایی`}
|
||||||
|
summaryItems={[
|
||||||
|
{ label: "محصول نهایی", value: selectedPlan.finalProduct },
|
||||||
|
{ label: "زمان برداشت", value: selectedPlan.harvestTime },
|
||||||
|
{ label: "نوع کود", value: selectedPlan.fertilizerType },
|
||||||
|
]}
|
||||||
|
relatedTitle="برنامههای آبیاری پیشنهادی"
|
||||||
|
relatedDescription="از بین برنامههای زیر، مناسبترین برنامه آبیاری را برای این برنامه کوددهی انتخاب کنید."
|
||||||
|
relatedPlans={relatedIrrigationPlans}
|
||||||
|
selectedRelatedPlanId={selectedRelatedPlanId}
|
||||||
|
onSelectRelatedPlan={setSelectedRelatedPlanId}
|
||||||
|
onConfirm={handleConfirmRelatedPlan}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FertilizationPlanPage;
|
export default FertilizationPlanPage;
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
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";
|
||||||
|
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";
|
||||||
|
|
||||||
|
import FertilizationPlanParserPage from "@views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage";
|
||||||
|
import RelatedPlanSelector, {
|
||||||
|
type RelatedPlanItem,
|
||||||
|
} from "@views/dashboards/farm/planSelector/RelatedPlanSelector";
|
||||||
|
|
||||||
|
import CustomTextField from "@core/components/mui/TextField";
|
||||||
|
|
||||||
|
type IrrigationPlanRow = {
|
||||||
|
id: number;
|
||||||
|
planName: string;
|
||||||
|
finalProduct: string;
|
||||||
|
harvestTime: string;
|
||||||
|
outputTon: number;
|
||||||
|
irrigationMethod: string;
|
||||||
|
status: "active" | "draft";
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockIrrigationPlans: IrrigationPlanRow[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
planName: "برنامه آبیاری گوجه فرنگی گلخانهای",
|
||||||
|
finalProduct: "گوجه فرنگی ممتاز",
|
||||||
|
harvestTime: "۱۴۰۴/۰۶/۱۰",
|
||||||
|
outputTon: 48,
|
||||||
|
irrigationMethod: "قطرهای",
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
planName: "برنامه آبیاری ذرت علوفهای",
|
||||||
|
finalProduct: "ذرت علوفهای درجه یک",
|
||||||
|
harvestTime: "۱۴۰۴/۰۵/۲۲",
|
||||||
|
outputTon: 36,
|
||||||
|
irrigationMethod: "بارانی",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
planName: "برنامه آبیاری خیار فضای باز",
|
||||||
|
finalProduct: "خیار صادراتی",
|
||||||
|
harvestTime: "۱۴۰۴/۰۴/۱۸",
|
||||||
|
outputTon: 28,
|
||||||
|
irrigationMethod: "تیپ",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
planName: "برنامه آبیاری هندوانه",
|
||||||
|
finalProduct: "هندوانه شیرین بازارپسند",
|
||||||
|
harvestTime: "۱۴۰۴/۰۵/۳۰",
|
||||||
|
outputTon: 55,
|
||||||
|
irrigationMethod: "قطرهای",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const relatedFertilizationPlans: RelatedPlanItem[] = [
|
||||||
|
{
|
||||||
|
id: 201,
|
||||||
|
title: "کوددهی آغاز رشد رویشی",
|
||||||
|
finalProduct: "گوجه فرنگی ممتاز",
|
||||||
|
harvestTime: "۱۴۰۴/۰۵/۲۰",
|
||||||
|
outputTon: 42,
|
||||||
|
methodLabel: "نوع کود: NPK 20-20-20",
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 202,
|
||||||
|
title: "کوددهی تقویت گلدهی",
|
||||||
|
finalProduct: "گوجه فرنگی ممتاز",
|
||||||
|
harvestTime: "۱۴۰۴/۰۶/۰۵",
|
||||||
|
outputTon: 47,
|
||||||
|
methodLabel: "نوع کود: کلسیم بور",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 203,
|
||||||
|
title: "کوددهی پتاس بالا برای رنگگیری",
|
||||||
|
finalProduct: "گوجه فرنگی ممتاز",
|
||||||
|
harvestTime: "۱۴۰۴/۰۶/۱۲",
|
||||||
|
outputTon: 51,
|
||||||
|
methodLabel: "نوع کود: سولفات پتاسیم",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const IrrigationPlanPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [plans, setPlans] = useState(mockIrrigationPlans);
|
||||||
|
const [minOutputTon, setMinOutputTon] = useState("0");
|
||||||
|
const [selectedPlan, setSelectedPlan] = useState<IrrigationPlanRow | null>(
|
||||||
|
mockIrrigationPlans[0],
|
||||||
|
);
|
||||||
|
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||||
|
const [selectedRelatedPlanId, setSelectedRelatedPlanId] = useState<number | null>(
|
||||||
|
relatedFertilizationPlans[0]?.id ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredPlans = useMemo(() => {
|
||||||
|
const minTon = Number(minOutputTon);
|
||||||
|
|
||||||
|
return plans
|
||||||
|
.filter((plan) => plan.outputTon >= minTon)
|
||||||
|
.sort((firstPlan, secondPlan) => secondPlan.outputTon - firstPlan.outputTon);
|
||||||
|
}, [minOutputTon, plans]);
|
||||||
|
|
||||||
|
const handleActivate = (planId: number) => {
|
||||||
|
setPlans((currentPlans) =>
|
||||||
|
currentPlans.map((plan) => ({
|
||||||
|
...plan,
|
||||||
|
status: plan.id === planId ? "active" : "draft",
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const activePlan = plans.find((plan) => plan.id === planId);
|
||||||
|
|
||||||
|
if (activePlan) {
|
||||||
|
setSelectedPlan({ ...activePlan, status: "active" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (planId: number) => {
|
||||||
|
setPlans((currentPlans) => currentPlans.filter((plan) => plan.id !== planId));
|
||||||
|
|
||||||
|
if (selectedPlan?.id === planId) {
|
||||||
|
setSelectedPlan(null);
|
||||||
|
setDetailsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowDetails = (plan: IrrigationPlanRow) => {
|
||||||
|
setSelectedPlan(plan);
|
||||||
|
setSelectedRelatedPlanId(relatedFertilizationPlans[0]?.id ?? null);
|
||||||
|
setDetailsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmRelatedPlan = (relatedPlan: RelatedPlanItem) => {
|
||||||
|
setDetailsOpen(false);
|
||||||
|
if (!selectedPlan) return;
|
||||||
|
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
from: "irrigation-plan",
|
||||||
|
planType: "irrigation",
|
||||||
|
planId: String(selectedPlan.id),
|
||||||
|
planName: selectedPlan.planName,
|
||||||
|
relatedType: "fertilization",
|
||||||
|
relatedPlanId: String(relatedPlan.id),
|
||||||
|
relatedPlanName: relatedPlan.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push(`/yield-harvest?${query.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack spacing={6}>
|
||||||
|
<FertilizationPlanParserPage
|
||||||
|
initialTab="irrigation"
|
||||||
|
enabledTabs={["irrigation"]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<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">
|
||||||
|
نمایش همه برنامهها با داده ماک و امکان فیلتر بر اساس بیشترین تن خروجی محصول
|
||||||
|
</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>
|
||||||
|
<TableCell>زمان برداشت</TableCell>
|
||||||
|
<TableCell>تن خروجی</TableCell>
|
||||||
|
<TableCell>روش آبیاری</TableCell>
|
||||||
|
<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>
|
||||||
|
<TableCell>{plan.outputTon} تن</TableCell>
|
||||||
|
<TableCell>{plan.irrigationMethod}</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)}
|
||||||
|
disabled={plan.status === "active"}
|
||||||
|
>
|
||||||
|
<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)}
|
||||||
|
>
|
||||||
|
<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" />
|
||||||
|
<Chip label={`زمان برداشت: ${selectedPlan.harvestTime}`} variant="tonal" />
|
||||||
|
<Chip label={`تن خروجی: ${selectedPlan.outputTon} تن`} variant="tonal" />
|
||||||
|
<Chip label={`روش آبیاری: ${selectedPlan.irrigationMethod}`} variant="tonal" />
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<i className="tabler-list-details" />}
|
||||||
|
onClick={() => handleShowDetails(selectedPlan)}
|
||||||
|
>
|
||||||
|
مشاهده و انتخاب برنامه کوددهی
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
startIcon={<i className="tabler-check" />}
|
||||||
|
onClick={() => handleActivate(selectedPlan.id)}
|
||||||
|
>
|
||||||
|
فعال کردن این برنامه
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Alert severity="warning">
|
||||||
|
برای دیدن جزئیات، یکی از برنامههای آبیاری را انتخاب کنید.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{selectedPlan ? (
|
||||||
|
<RelatedPlanSelector
|
||||||
|
open={detailsOpen}
|
||||||
|
onClose={() => setDetailsOpen(false)}
|
||||||
|
title="جزییات برنامه آبیاری"
|
||||||
|
description="برای تکمیل اجرای این برنامه آبیاری، یکی از برنامههای کوددهی مرتبط را انتخاب کنید."
|
||||||
|
currentPlanName={selectedPlan.planName}
|
||||||
|
currentPlanStatus={selectedPlan.status === "active" ? "برنامه فعال" : "آماده برای فعالسازی"}
|
||||||
|
currentPlanOutput={`خروجی هدف: ${selectedPlan.outputTon} تن محصول نهایی`}
|
||||||
|
summaryItems={[
|
||||||
|
{ label: "محصول نهایی", value: selectedPlan.finalProduct },
|
||||||
|
{ label: "زمان برداشت", value: selectedPlan.harvestTime },
|
||||||
|
{ label: "روش آبیاری", value: selectedPlan.irrigationMethod },
|
||||||
|
]}
|
||||||
|
relatedTitle="برنامههای کوددهی پیشنهادی"
|
||||||
|
relatedDescription="از بین برنامههای زیر، مناسبترین برنامه کوددهی را برای این برنامه آبیاری انتخاب کنید."
|
||||||
|
relatedPlans={relatedFertilizationPlans}
|
||||||
|
selectedRelatedPlanId={selectedRelatedPlanId}
|
||||||
|
onSelectRelatedPlan={setSelectedRelatedPlanId}
|
||||||
|
onConfirm={handleConfirmRelatedPlan}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IrrigationPlanPage;
|
||||||
@@ -1,7 +1,105 @@
|
|||||||
import PlantProductionPage from '@views/dashboards/farm/PlantProductionPage'
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Chip from "@mui/material/Chip";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
import PlantProductionPage from "@views/dashboards/farm/PlantProductionPage";
|
||||||
|
|
||||||
const YieldHarvestPage = () => {
|
const YieldHarvestPage = () => {
|
||||||
return <PlantProductionPage />
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const selectionSummary = useMemo(() => {
|
||||||
|
const from = searchParams.get("from");
|
||||||
|
const planType = searchParams.get("planType");
|
||||||
|
const planName = searchParams.get("planName");
|
||||||
|
const relatedType = searchParams.get("relatedType");
|
||||||
|
const relatedPlanName = searchParams.get("relatedPlanName");
|
||||||
|
|
||||||
|
if (!from || !planType || !planName || !relatedType || !relatedPlanName) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default YieldHarvestPage
|
return {
|
||||||
|
from,
|
||||||
|
planType,
|
||||||
|
planName,
|
||||||
|
relatedType,
|
||||||
|
relatedPlanName,
|
||||||
|
};
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
const from = selectionSummary?.from;
|
||||||
|
|
||||||
|
if (from === "irrigation-plan") {
|
||||||
|
router.push("/irrigation-plan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from === "fertilization-plan") {
|
||||||
|
router.push("/fertilization-plan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={6}>
|
||||||
|
{selectionSummary ? (
|
||||||
|
<Card variant="outlined">
|
||||||
|
<CardContent>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
spacing={2}
|
||||||
|
alignItems={{ xs: "stretch", md: "center" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5">تحلیل عملکرد و برداشت</Typography>
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
بر اساس انتخاب برنامهها وارد این صفحه شدهاید.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<i className="tabler-arrow-right text-[18px]" />}
|
||||||
|
onClick={handleBack}
|
||||||
|
>
|
||||||
|
بازگشت به صفحه قبل
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack direction={{ xs: "column", md: "row" }} spacing={1.5} useFlexGap flexWrap="wrap" sx={{ mt: 3 }}>
|
||||||
|
<Chip
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
label={`برنامه ${selectionSummary.planType === "irrigation" ? "آبیاری" : "کوددهی"}: ${selectionSummary.planName}`}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
variant="tonal"
|
||||||
|
color="success"
|
||||||
|
label={`برنامه ${selectionSummary.relatedType === "irrigation" ? "آبیاری" : "کوددهی"} انتخابشده: ${selectionSummary.relatedPlanName}`}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<PlantProductionPage />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default YieldHarvestPage;
|
||||||
|
|||||||
@@ -157,6 +157,12 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
|
|||||||
</MenuSection>
|
</MenuSection>
|
||||||
|
|
||||||
<MenuSection label={t("recommendation")}>
|
<MenuSection label={t("recommendation")}>
|
||||||
|
<MenuItem
|
||||||
|
href="/irrigation-plan"
|
||||||
|
icon={<i className="tabler-droplet-half-2" />}
|
||||||
|
>
|
||||||
|
{t("irrigationPlanParser")}
|
||||||
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/irrigation-recommendation"
|
href="/irrigation-recommendation"
|
||||||
icon={<i className="tabler-droplet-half-2" />}
|
icon={<i className="tabler-droplet-half-2" />}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const navigationLabels = {
|
|||||||
sensor7In1: 'سنسور خاک 7 در 1',
|
sensor7In1: 'سنسور خاک 7 در 1',
|
||||||
dataSection: 'بخش دادهها',
|
dataSection: 'بخش دادهها',
|
||||||
recommendation: 'توصیهها',
|
recommendation: 'توصیهها',
|
||||||
|
irrigationPlanParser: 'برنامه آبیاری',
|
||||||
irrigationRecommendation: 'توصیه آبیاری',
|
irrigationRecommendation: 'توصیه آبیاری',
|
||||||
fertilizationRecommendation: 'توصیه کوددهی',
|
fertilizationRecommendation: 'توصیه کوددهی',
|
||||||
aiAssistant: 'دستیار هوشمند',
|
aiAssistant: 'دستیار هوشمند',
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"sensorSection": "سنسورها",
|
"sensorSection": "سنسورها",
|
||||||
"sensor7In1": "سنسور خاک 7 در 1",
|
"sensor7In1": "سنسور خاک 7 در 1",
|
||||||
"recommendation": "توصیهها",
|
"recommendation": "توصیهها",
|
||||||
|
"irrigationPlanParser": "برنامه آبیاری",
|
||||||
"irrigationRecommendation": "توصیه آبیاری",
|
"irrigationRecommendation": "توصیه آبیاری",
|
||||||
"fertilizationRecommendation": "توصیه کوددهی",
|
"fertilizationRecommendation": "توصیه کوددهی",
|
||||||
"aiAssistant": "دستیار هوشمند",
|
"aiAssistant": "دستیار هوشمند",
|
||||||
|
|||||||
@@ -10,20 +10,19 @@ export interface Event {
|
|||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
deadline: number // Unix timestamp
|
deadline?: number // Unix timestamp
|
||||||
tags: string[]
|
tags: string[]
|
||||||
author: Author
|
author?: Author
|
||||||
calendar: 'Personal' | 'Business' | 'Family' | 'Holiday' | 'ETC'
|
|
||||||
start: string // ISO 8601
|
start: string // ISO 8601
|
||||||
end: string // ISO 8601
|
end: string // ISO 8601
|
||||||
allDay: boolean
|
allDay?: boolean
|
||||||
extendedProps?: Record<string, any>
|
extendedProps?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListEventsParams {
|
export interface ListEventsParams {
|
||||||
start?: string // ISO 8601
|
start?: string // ISO 8601
|
||||||
end?: string // ISO 8601
|
end?: string // ISO 8601
|
||||||
calendar?: 'Personal' | 'Business' | 'Family' | 'Holiday' | 'ETC'
|
farm_uuid?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateEventRequest {
|
export interface CreateEventRequest {
|
||||||
@@ -31,10 +30,9 @@ export interface CreateEventRequest {
|
|||||||
description?: string
|
description?: string
|
||||||
deadline?: number
|
deadline?: number
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
calendar: 'Personal' | 'Business' | 'Family' | 'Holiday' | 'ETC'
|
|
||||||
start: string // ISO 8601
|
start: string // ISO 8601
|
||||||
end: string // ISO 8601
|
end: string // ISO 8601
|
||||||
allDay?: boolean
|
farm_uuid?: string
|
||||||
extendedProps?: Record<string, any>
|
extendedProps?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,14 +50,13 @@ export interface ListEventsResponse {
|
|||||||
|
|
||||||
export interface UpdateEventRequest {
|
export interface UpdateEventRequest {
|
||||||
id: string
|
id: string
|
||||||
title?: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
deadline?: number
|
deadline?: number
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
calendar?: 'Personal' | 'Business' | 'Family' | 'Holiday' | 'ETC'
|
start: string // ISO 8601
|
||||||
start?: string // ISO 8601
|
end: string // ISO 8601
|
||||||
end?: string // ISO 8601
|
farm_uuid?: string
|
||||||
allDay?: boolean
|
|
||||||
extendedProps?: Record<string, any>
|
extendedProps?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +68,16 @@ export interface DeleteEventResponse {
|
|||||||
success: boolean
|
success: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EventTag {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListEventTagsResponse {
|
||||||
|
tags: EventTag[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface NonRoutineTask {
|
export interface NonRoutineTask {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@@ -93,7 +100,7 @@ export const eventService = {
|
|||||||
* Create a new event
|
* Create a new event
|
||||||
*/
|
*/
|
||||||
async createEvent(data: CreateEventRequest): Promise<Event> {
|
async createEvent(data: CreateEventRequest): Promise<Event> {
|
||||||
const response = await apiClient.post<CreateEventResponse>('/api/events', data)
|
const response = await apiClient.post<CreateEventResponse>('/api/events/', data)
|
||||||
return response.event
|
return response.event
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -101,7 +108,7 @@ export const eventService = {
|
|||||||
* Get an event by ID
|
* Get an event by ID
|
||||||
*/
|
*/
|
||||||
async getEvent(id: string): Promise<Event> {
|
async getEvent(id: string): Promise<Event> {
|
||||||
const response = await apiClient.get<GetEventResponse>(`/api/events/${id}`)
|
const response = await apiClient.get<GetEventResponse>(`/api/events/${id}/`)
|
||||||
return response.event
|
return response.event
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -112,10 +119,10 @@ export const eventService = {
|
|||||||
const queryParams = new URLSearchParams()
|
const queryParams = new URLSearchParams()
|
||||||
if (params?.start) queryParams.append('start', params.start)
|
if (params?.start) queryParams.append('start', params.start)
|
||||||
if (params?.end) queryParams.append('end', params.end)
|
if (params?.end) queryParams.append('end', params.end)
|
||||||
if (params?.calendar) queryParams.append('calendar', params.calendar)
|
if (params?.farm_uuid) queryParams.append('farm_uuid', params.farm_uuid)
|
||||||
|
|
||||||
const queryString = queryParams.toString()
|
const queryString = queryParams.toString()
|
||||||
const endpoint = `/api/events${queryString ? `?${queryString}` : ''}`
|
const endpoint = `/api/events/${queryString ? `?${queryString}` : ''}`
|
||||||
|
|
||||||
const response = await apiClient.get<ListEventsResponse>(endpoint)
|
const response = await apiClient.get<ListEventsResponse>(endpoint)
|
||||||
return response.events || []
|
return response.events || []
|
||||||
@@ -126,7 +133,7 @@ export const eventService = {
|
|||||||
*/
|
*/
|
||||||
async updateEvent(data: UpdateEventRequest): Promise<Event> {
|
async updateEvent(data: UpdateEventRequest): Promise<Event> {
|
||||||
const { id, ...updateData } = data
|
const { id, ...updateData } = data
|
||||||
const response = await apiClient.put<UpdateEventResponse>(`/api/events/${id}`, updateData)
|
const response = await apiClient.put<UpdateEventResponse>(`/api/events/${id}/`, updateData)
|
||||||
return response.event
|
return response.event
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -134,10 +141,21 @@ export const eventService = {
|
|||||||
* Delete an event
|
* Delete an event
|
||||||
*/
|
*/
|
||||||
async deleteEvent(id: string): Promise<boolean> {
|
async deleteEvent(id: string): Promise<boolean> {
|
||||||
const response = await apiClient.delete<DeleteEventResponse>(`/api/events/${id}`)
|
const response = await apiClient.delete<DeleteEventResponse>(`/api/events/${id}/`)
|
||||||
return response.success
|
return response.success
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async listTags(farmUuid?: string): Promise<EventTag[]> {
|
||||||
|
const queryParams = new URLSearchParams()
|
||||||
|
if (farmUuid) queryParams.append('farm_uuid', farmUuid)
|
||||||
|
|
||||||
|
const queryString = queryParams.toString()
|
||||||
|
const endpoint = `/api/events/tags/${queryString ? `?${queryString}` : ''}`
|
||||||
|
const response = await apiClient.get<ListEventTagsResponse>(endpoint)
|
||||||
|
|
||||||
|
return response.tags || []
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* دریافت تسکهای غیرروتین (مشترک با Kanban و Todo)
|
* دریافت تسکهای غیرروتین (مشترک با Kanban و Todo)
|
||||||
*/
|
*/
|
||||||
@@ -146,4 +164,3 @@ export const eventService = {
|
|||||||
return response.tasks || []
|
return response.tasks || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import { apiClient } from "../client";
|
||||||
|
|
||||||
|
export type FarmerTodoPriority = "زیاد" | "متوسط" | "کم";
|
||||||
|
export type FarmerTodoPriorityInput =
|
||||||
|
| FarmerTodoPriority
|
||||||
|
| "high"
|
||||||
|
| "medium"
|
||||||
|
| "low";
|
||||||
|
export type FarmerTodoStatus = "open" | "done";
|
||||||
|
|
||||||
|
export interface FarmerTodoTask {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
zone: string;
|
||||||
|
scheduledDate: string;
|
||||||
|
time: string;
|
||||||
|
priority: FarmerTodoPriority;
|
||||||
|
note: string;
|
||||||
|
tags: string[];
|
||||||
|
status: FarmerTodoStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizePriority = (
|
||||||
|
priority: string | null | undefined,
|
||||||
|
): FarmerTodoPriority => {
|
||||||
|
switch (priority) {
|
||||||
|
case "high":
|
||||||
|
case "زیاد":
|
||||||
|
return "زیاد";
|
||||||
|
case "low":
|
||||||
|
case "کم":
|
||||||
|
return "کم";
|
||||||
|
case "medium":
|
||||||
|
case "متوسط":
|
||||||
|
default:
|
||||||
|
return "متوسط";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeStatus = (
|
||||||
|
status: string | null | undefined,
|
||||||
|
): FarmerTodoStatus => {
|
||||||
|
return status === "done" ? "done" : "open";
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeTask = (task: FarmerTodoTask): FarmerTodoTask => ({
|
||||||
|
...task,
|
||||||
|
priority: normalizePriority(task.priority),
|
||||||
|
status: normalizeStatus(task.status),
|
||||||
|
note: task.note ?? "",
|
||||||
|
tags: Array.isArray(task.tags) ? task.tags : [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface FarmerTodoSummary {
|
||||||
|
todayTasksCount: number;
|
||||||
|
completedCount: number;
|
||||||
|
urgentCount: number;
|
||||||
|
progressValue: number;
|
||||||
|
nextTask: FarmerTodoTask | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FarmerTodoZoneOption {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListFarmerTodosParams {
|
||||||
|
farm_uuid?: string;
|
||||||
|
status?: FarmerTodoStatus;
|
||||||
|
priority?: FarmerTodoPriorityInput;
|
||||||
|
date?: string;
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
zone?: string;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateFarmerTodoRequest {
|
||||||
|
farm_uuid?: string;
|
||||||
|
title: string;
|
||||||
|
zone: string;
|
||||||
|
scheduledDate: string;
|
||||||
|
time: string;
|
||||||
|
priority: FarmerTodoPriorityInput;
|
||||||
|
note?: string;
|
||||||
|
tags?: string[];
|
||||||
|
status?: FarmerTodoStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateFarmerTodoRequest {
|
||||||
|
farm_uuid?: string;
|
||||||
|
title?: string;
|
||||||
|
zone?: string;
|
||||||
|
scheduledDate?: string;
|
||||||
|
time?: string;
|
||||||
|
priority?: FarmerTodoPriorityInput;
|
||||||
|
note?: string;
|
||||||
|
tags?: string[];
|
||||||
|
status?: FarmerTodoStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListFarmerTodosResponse {
|
||||||
|
tasks: FarmerTodoTask[];
|
||||||
|
meta?: { total?: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FarmerTodoResponse {
|
||||||
|
task: FarmerTodoTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListZonesResponse {
|
||||||
|
zones: FarmerTodoZoneOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const withQuery = (
|
||||||
|
endpoint: string,
|
||||||
|
params?: Record<string, string | undefined>,
|
||||||
|
): string => {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(params ?? {}).forEach(([key, value]) => {
|
||||||
|
if (value) {
|
||||||
|
query.append(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryString = query.toString();
|
||||||
|
return `${endpoint}${queryString ? `?${queryString}` : ""}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const farmerTodoService = {
|
||||||
|
async listTasks(params?: ListFarmerTodosParams): Promise<FarmerTodoTask[]> {
|
||||||
|
const response = await apiClient.get<ListFarmerTodosResponse>(
|
||||||
|
withQuery("/api/farmer-todos/", {
|
||||||
|
farm_uuid: params?.farm_uuid,
|
||||||
|
status: params?.status,
|
||||||
|
priority: params?.priority,
|
||||||
|
date: params?.date,
|
||||||
|
from: params?.from,
|
||||||
|
to: params?.to,
|
||||||
|
zone: params?.zone,
|
||||||
|
search: params?.search,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (response.tasks ?? []).map(normalizeTask);
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTask(data: CreateFarmerTodoRequest): Promise<FarmerTodoTask> {
|
||||||
|
const response = await apiClient.post<FarmerTodoResponse>(
|
||||||
|
"/api/farmer-todos/",
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
return normalizeTask(response.task);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTask(
|
||||||
|
taskId: string,
|
||||||
|
data: UpdateFarmerTodoRequest,
|
||||||
|
): Promise<FarmerTodoTask> {
|
||||||
|
const response = await apiClient.put<FarmerTodoResponse>(
|
||||||
|
`/api/farmer-todos/${taskId}/`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
return normalizeTask(response.task);
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteTask(taskId: string): Promise<boolean> {
|
||||||
|
const response = await apiClient.delete<{ success: boolean }>(
|
||||||
|
`/api/farmer-todos/${taskId}/`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.success;
|
||||||
|
},
|
||||||
|
|
||||||
|
async listZones(farmUuid?: string): Promise<FarmerTodoZoneOption[]> {
|
||||||
|
const response = await apiClient.get<ListZonesResponse>(
|
||||||
|
withQuery("/api/farmer-todos/zones/", { farm_uuid: farmUuid }),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.zones ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSummary(farmUuid?: string): Promise<FarmerTodoSummary> {
|
||||||
|
return apiClient.get<FarmerTodoSummary>(
|
||||||
|
withQuery("/api/farmer-todos/summary/", { farm_uuid: farmUuid }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -8,11 +8,12 @@ import type { CalendarFiltersType, CalendarType } from '@/types/apps/calendarTyp
|
|||||||
|
|
||||||
// API Imports
|
// API Imports
|
||||||
import { eventService } from '@/libs/api'
|
import { eventService } from '@/libs/api'
|
||||||
|
import type { Event, ListEventsParams } from '@/libs/api'
|
||||||
|
|
||||||
// Async Thunks
|
// Async Thunks
|
||||||
export const fetchEvents = createAsyncThunk(
|
export const fetchEvents = createAsyncThunk(
|
||||||
'calendar/fetchEvents',
|
'calendar/fetchEvents',
|
||||||
async (params?: { start?: string; end?: string; calendar?: CalendarFiltersType }) => {
|
async (params?: ListEventsParams) => {
|
||||||
return await eventService.listEvents(params)
|
return await eventService.listEvents(params)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -45,6 +46,22 @@ const initialState: CalendarType & {
|
|||||||
error: null
|
error: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapEventToCalendarInput = (event: Event): EventInput => ({
|
||||||
|
id: event.id,
|
||||||
|
title: event.title,
|
||||||
|
start: event.start,
|
||||||
|
end: event.end,
|
||||||
|
allDay: event.allDay ?? false,
|
||||||
|
extendedProps: {
|
||||||
|
...(event.extendedProps || {}),
|
||||||
|
calendar: event.extendedProps?.calendar || 'Business',
|
||||||
|
description: event.description,
|
||||||
|
deadline: event.deadline,
|
||||||
|
tags: event.tags || [],
|
||||||
|
author: event.author
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const filterEventsUsingCheckbox = (events: EventInput[], selectedCalendars: CalendarFiltersType[]) => {
|
const filterEventsUsingCheckbox = (events: EventInput[], selectedCalendars: CalendarFiltersType[]) => {
|
||||||
return events.filter(event => selectedCalendars.includes(event.extendedProps?.calendar as CalendarFiltersType))
|
return events.filter(event => selectedCalendars.includes(event.extendedProps?.calendar as CalendarFiltersType))
|
||||||
}
|
}
|
||||||
@@ -54,7 +71,7 @@ export const calendarSlice = createSlice({
|
|||||||
initialState: initialState,
|
initialState: initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
filterEvents: state => {
|
filterEvents: state => {
|
||||||
state.filteredEvents = state.events
|
state.events = filterEventsUsingCheckbox(state.filteredEvents, state.selectedCalendars)
|
||||||
},
|
},
|
||||||
|
|
||||||
addEvent: (state, action) => {
|
addEvent: (state, action) => {
|
||||||
@@ -117,24 +134,9 @@ export const calendarSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(fetchEvents.fulfilled, (state, action) => {
|
.addCase(fetchEvents.fulfilled, (state, action) => {
|
||||||
state.loading = false
|
state.loading = false
|
||||||
// Convert API events to FullCalendar EventInput format
|
const events: EventInput[] = action.payload.map(mapEventToCalendarInput)
|
||||||
const events: EventInput[] = action.payload.map(event => ({
|
state.filteredEvents = events
|
||||||
id: event.id,
|
state.events = filterEventsUsingCheckbox(events, state.selectedCalendars)
|
||||||
title: event.title,
|
|
||||||
start: event.start,
|
|
||||||
end: event.end,
|
|
||||||
allDay: event.allDay,
|
|
||||||
extendedProps: {
|
|
||||||
...event.extendedProps,
|
|
||||||
calendar: event.calendar,
|
|
||||||
description: event.description,
|
|
||||||
deadline: event.deadline,
|
|
||||||
tags: event.tags,
|
|
||||||
author: event.author
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
state.events = events
|
|
||||||
state.filteredEvents = filterEventsUsingCheckbox(events, state.selectedCalendars)
|
|
||||||
})
|
})
|
||||||
.addCase(fetchEvents.rejected, (state, action) => {
|
.addCase(fetchEvents.rejected, (state, action) => {
|
||||||
state.loading = false
|
state.loading = false
|
||||||
@@ -142,51 +144,23 @@ export const calendarSlice = createSlice({
|
|||||||
})
|
})
|
||||||
// Create Event
|
// Create Event
|
||||||
.addCase(createEventAsync.fulfilled, (state, action) => {
|
.addCase(createEventAsync.fulfilled, (state, action) => {
|
||||||
const event: EventInput = {
|
const event = mapEventToCalendarInput(action.payload)
|
||||||
id: action.payload.id,
|
state.filteredEvents.push(event)
|
||||||
title: action.payload.title,
|
state.events = filterEventsUsingCheckbox(state.filteredEvents, state.selectedCalendars)
|
||||||
start: action.payload.start,
|
|
||||||
end: action.payload.end,
|
|
||||||
allDay: action.payload.allDay,
|
|
||||||
extendedProps: {
|
|
||||||
...action.payload.extendedProps,
|
|
||||||
calendar: action.payload.calendar,
|
|
||||||
description: action.payload.description,
|
|
||||||
deadline: action.payload.deadline,
|
|
||||||
tags: action.payload.tags,
|
|
||||||
author: action.payload.author
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.events.push(event)
|
|
||||||
state.filteredEvents = filterEventsUsingCheckbox(state.events, state.selectedCalendars)
|
|
||||||
})
|
})
|
||||||
// Update Event
|
// Update Event
|
||||||
.addCase(updateEventAsync.fulfilled, (state, action) => {
|
.addCase(updateEventAsync.fulfilled, (state, action) => {
|
||||||
const index = state.events.findIndex(event => event.id === action.payload.id)
|
const index = state.events.findIndex(event => event.id === action.payload.id)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const event: EventInput = {
|
const event = mapEventToCalendarInput(action.payload)
|
||||||
id: action.payload.id,
|
state.filteredEvents[index] = event
|
||||||
title: action.payload.title,
|
state.events = filterEventsUsingCheckbox(state.filteredEvents, state.selectedCalendars)
|
||||||
start: action.payload.start,
|
|
||||||
end: action.payload.end,
|
|
||||||
allDay: action.payload.allDay,
|
|
||||||
extendedProps: {
|
|
||||||
...action.payload.extendedProps,
|
|
||||||
calendar: action.payload.calendar,
|
|
||||||
description: action.payload.description,
|
|
||||||
deadline: action.payload.deadline,
|
|
||||||
tags: action.payload.tags,
|
|
||||||
author: action.payload.author
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.events[index] = event
|
|
||||||
state.filteredEvents = filterEventsUsingCheckbox(state.events, state.selectedCalendars)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Delete Event
|
// Delete Event
|
||||||
.addCase(deleteEventAsync.fulfilled, (state, action) => {
|
.addCase(deleteEventAsync.fulfilled, (state, action) => {
|
||||||
state.events = state.events.filter(event => event.id !== action.meta.arg)
|
state.filteredEvents = state.filteredEvents.filter(event => event.id !== action.meta.arg)
|
||||||
state.filteredEvents = filterEventsUsingCheckbox(state.events, state.selectedCalendars)
|
state.events = filterEventsUsingCheckbox(state.filteredEvents, state.selectedCalendars)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useEffect, useRef } from "react";
|
|||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
// Third-party imports
|
// Third-party imports
|
||||||
import type { Dispatch } from "@reduxjs/toolkit";
|
|
||||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||||
|
|
||||||
import FullCalendar from "@fullcalendar/react";
|
import FullCalendar from "@fullcalendar/react";
|
||||||
@@ -20,14 +19,16 @@ import faLocale from "@fullcalendar/core/locales/fa";
|
|||||||
import type {
|
import type {
|
||||||
AddEventType,
|
AddEventType,
|
||||||
CalendarColors,
|
CalendarColors,
|
||||||
|
CalendarFiltersType,
|
||||||
CalendarType,
|
CalendarType,
|
||||||
} from "@/types/apps/calendarTypes";
|
} from "@/types/apps/calendarTypes";
|
||||||
|
import type { AppDispatch } from "@/redux-store";
|
||||||
|
|
||||||
// Slice Imports
|
// Slice Imports
|
||||||
import {
|
import {
|
||||||
filterEvents,
|
|
||||||
selectedEvent,
|
selectedEvent,
|
||||||
updateEvent,
|
updateEvent,
|
||||||
|
updateEventAsync,
|
||||||
} from "@/redux-store/slices/calendar";
|
} from "@/redux-store/slices/calendar";
|
||||||
|
|
||||||
type CalenderProps = {
|
type CalenderProps = {
|
||||||
@@ -35,10 +36,11 @@ type CalenderProps = {
|
|||||||
calendarApi: any;
|
calendarApi: any;
|
||||||
setCalendarApi: (val: any) => void;
|
setCalendarApi: (val: any) => void;
|
||||||
calendarsColor: CalendarColors;
|
calendarsColor: CalendarColors;
|
||||||
dispatch: Dispatch;
|
dispatch: AppDispatch | Dispatch<AnyAction>;
|
||||||
|
dispatch: AppDispatch;
|
||||||
handleLeftSidebarToggle: () => void;
|
handleLeftSidebarToggle: () => void;
|
||||||
handleAddEventSidebarToggle: () => void;
|
handleAddEventSidebarToggle: () => void;
|
||||||
handleEventDetailsOpen: () => void;
|
handleEventDetailsOpen?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const blankEvent: AddEventType = {
|
const blankEvent: AddEventType = {
|
||||||
@@ -153,7 +155,10 @@ const Calendar = (props: CalenderProps) => {
|
|||||||
eventClassNames({ event: calendarEvent }: any) {
|
eventClassNames({ event: calendarEvent }: any) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const colorName =
|
const colorName =
|
||||||
calendarsColor[calendarEvent._def.extendedProps.calendar];
|
calendarsColor[
|
||||||
|
(calendarEvent._def.extendedProps.calendar ||
|
||||||
|
"ETC") as CalendarFiltersType
|
||||||
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
// Background Color
|
// Background Color
|
||||||
@@ -175,7 +180,7 @@ const Calendar = (props: CalenderProps) => {
|
|||||||
extendedProps: clickedEvent.extendedProps,
|
extendedProps: clickedEvent.extendedProps,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
handleEventDetailsOpen();
|
handleEventDetailsOpen?.();
|
||||||
|
|
||||||
//* Only grab required field otherwise it goes in infinity loop
|
//* Only grab required field otherwise it goes in infinity loop
|
||||||
//! Always grab all fields rendered by form (even if it get `undefined`)
|
//! Always grab all fields rendered by form (even if it get `undefined`)
|
||||||
@@ -210,7 +215,30 @@ const Calendar = (props: CalenderProps) => {
|
|||||||
*/
|
*/
|
||||||
eventDrop({ event: droppedEvent }: any) {
|
eventDrop({ event: droppedEvent }: any) {
|
||||||
dispatch(updateEvent(droppedEvent));
|
dispatch(updateEvent(droppedEvent));
|
||||||
dispatch(filterEvents());
|
|
||||||
|
const currentEvent = calendarStore.events.find(
|
||||||
|
(calendarEvent) => calendarEvent.id === droppedEvent.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
void dispatch(
|
||||||
|
updateEventAsync({
|
||||||
|
id: droppedEvent.id,
|
||||||
|
data: {
|
||||||
|
title: droppedEvent.title,
|
||||||
|
description: currentEvent?.extendedProps?.description,
|
||||||
|
deadline: currentEvent?.extendedProps?.deadline,
|
||||||
|
tags: currentEvent?.extendedProps?.tags || [],
|
||||||
|
start: droppedEvent.start?.toISOString(),
|
||||||
|
end:
|
||||||
|
droppedEvent.end?.toISOString() ||
|
||||||
|
droppedEvent.start?.toISOString(),
|
||||||
|
extendedProps: {
|
||||||
|
...(currentEvent?.extendedProps || {}),
|
||||||
|
...(droppedEvent.extendedProps || {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -219,7 +247,30 @@ const Calendar = (props: CalenderProps) => {
|
|||||||
*/
|
*/
|
||||||
eventResize({ event: resizedEvent }: any) {
|
eventResize({ event: resizedEvent }: any) {
|
||||||
dispatch(updateEvent(resizedEvent));
|
dispatch(updateEvent(resizedEvent));
|
||||||
dispatch(filterEvents());
|
|
||||||
|
const currentEvent = calendarStore.events.find(
|
||||||
|
(calendarEvent) => calendarEvent.id === resizedEvent.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
void dispatch(
|
||||||
|
updateEventAsync({
|
||||||
|
id: resizedEvent.id,
|
||||||
|
data: {
|
||||||
|
title: resizedEvent.title,
|
||||||
|
description: currentEvent?.extendedProps?.description,
|
||||||
|
deadline: currentEvent?.extendedProps?.deadline,
|
||||||
|
tags: currentEvent?.extendedProps?.tags || [],
|
||||||
|
start: resizedEvent.start?.toISOString(),
|
||||||
|
end:
|
||||||
|
resizedEvent.end?.toISOString() ||
|
||||||
|
resizedEvent.start?.toISOString(),
|
||||||
|
extendedProps: {
|
||||||
|
...(currentEvent?.extendedProps || {}),
|
||||||
|
...(resizedEvent.extendedProps || {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -23,15 +23,14 @@ import CustomTextField from "@core/components/mui/TextField";
|
|||||||
import { eventService } from "@/libs/api";
|
import { eventService } from "@/libs/api";
|
||||||
import AppReactDatepicker from "@/libs/styles/AppReactDatepicker";
|
import AppReactDatepicker from "@/libs/styles/AppReactDatepicker";
|
||||||
import {
|
import {
|
||||||
addEvent,
|
createEventAsync,
|
||||||
deleteEvent,
|
deleteEventAsync,
|
||||||
filterEvents,
|
fetchEvents,
|
||||||
selectedEvent,
|
selectedEvent,
|
||||||
updateEvent,
|
updateEventAsync,
|
||||||
} from "@/redux-store/slices/calendar";
|
} from "@/redux-store/slices/calendar";
|
||||||
import type {
|
import type {
|
||||||
AddEventSidebarType,
|
AddEventSidebarType,
|
||||||
AddEventType,
|
|
||||||
} from "@/types/apps/calendarTypes";
|
} from "@/types/apps/calendarTypes";
|
||||||
|
|
||||||
type FarmerCalendarEventModalProps = Omit<
|
type FarmerCalendarEventModalProps = Omit<
|
||||||
@@ -69,6 +68,16 @@ const defaultState: DefaultStateType = {
|
|||||||
startDate: new Date(),
|
startDate: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toDate = (value: unknown) => {
|
||||||
|
if (value instanceof Date) return value;
|
||||||
|
if (typeof value === "string" || typeof value === "number") {
|
||||||
|
const parsed = new Date(value);
|
||||||
|
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date();
|
||||||
|
};
|
||||||
|
|
||||||
const modalTransitionDuration = {
|
const modalTransitionDuration = {
|
||||||
appear: 0,
|
appear: 0,
|
||||||
enter: 140,
|
enter: 140,
|
||||||
@@ -108,6 +117,8 @@ const FarmerCalendarEventModal = ({
|
|||||||
const [values, setValues] = useState<DefaultStateType>(defaultState);
|
const [values, setValues] = useState<DefaultStateType>(defaultState);
|
||||||
const [tagOptions, setTagOptions] = useState<string[]>([]);
|
const [tagOptions, setTagOptions] = useState<string[]>([]);
|
||||||
const [tagLoading, setTagLoading] = useState(false);
|
const [tagLoading, setTagLoading] = useState(false);
|
||||||
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -131,8 +142,8 @@ const FarmerCalendarEventModal = ({
|
|||||||
: "",
|
: "",
|
||||||
allDay: event.allDay ?? true,
|
allDay: event.allDay ?? true,
|
||||||
description: event.extendedProps?.description || "",
|
description: event.extendedProps?.description || "",
|
||||||
endDate: event.end !== null ? event.end : event.start,
|
endDate: event.end !== null ? toDate(event.end) : toDate(event.start),
|
||||||
startDate: event.start !== null ? event.start : new Date(),
|
startDate: event.start !== null ? toDate(event.start) : new Date(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [calendarStore.selectedEvent, setValue]);
|
}, [calendarStore.selectedEvent, setValue]);
|
||||||
@@ -149,43 +160,62 @@ const FarmerCalendarEventModal = ({
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (data: { title: string }) => {
|
const onSubmit = async (data: { title: string }) => {
|
||||||
const modifiedEvent: AddEventType = {
|
const payload = {
|
||||||
display: "block",
|
|
||||||
title: data.title,
|
title: data.title,
|
||||||
end: values.endDate,
|
|
||||||
allDay: values.allDay,
|
|
||||||
start: values.startDate,
|
|
||||||
extendedProps: {
|
|
||||||
calendar:
|
|
||||||
calendarStore.selectedEvent?.extendedProps?.calendar || "Business",
|
|
||||||
description: values.description.length ? values.description : undefined,
|
description: values.description.length ? values.description : undefined,
|
||||||
tags: values.tag.trim().length ? [values.tag.trim()] : [],
|
tags: values.tag.trim().length ? [values.tag.trim()] : [],
|
||||||
|
start: values.startDate.toISOString(),
|
||||||
|
end: values.endDate.toISOString(),
|
||||||
|
extendedProps: {
|
||||||
|
...(calendarStore.selectedEvent?.extendedProps || {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setSubmitError(null);
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
if (
|
if (
|
||||||
calendarStore.selectedEvent === null ||
|
calendarStore.selectedEvent === null ||
|
||||||
(calendarStore.selectedEvent !== null &&
|
(calendarStore.selectedEvent !== null &&
|
||||||
!calendarStore.selectedEvent.title.length)
|
!calendarStore.selectedEvent.title.length)
|
||||||
) {
|
) {
|
||||||
dispatch(addEvent(modifiedEvent));
|
await dispatch(createEventAsync(payload)).unwrap();
|
||||||
} else {
|
} else {
|
||||||
dispatch(
|
await dispatch(
|
||||||
updateEvent({ ...modifiedEvent, id: calendarStore.selectedEvent.id }),
|
updateEventAsync({
|
||||||
);
|
id: calendarStore.selectedEvent.id,
|
||||||
|
data: payload,
|
||||||
|
}),
|
||||||
|
).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(filterEvents());
|
await dispatch(fetchEvents()).unwrap();
|
||||||
handleClose();
|
handleClose();
|
||||||
|
} catch (error: any) {
|
||||||
|
setSubmitError(error?.message || t("validation.submitFailed"));
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteButtonClick = () => {
|
const handleDeleteButtonClick = async () => {
|
||||||
if (calendarStore.selectedEvent?.id) {
|
if (calendarStore.selectedEvent?.id) {
|
||||||
dispatch(deleteEvent(calendarStore.selectedEvent.id));
|
setSubmitError(null);
|
||||||
dispatch(filterEvents());
|
setSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatch(deleteEventAsync(calendarStore.selectedEvent.id)).unwrap();
|
||||||
|
await dispatch(fetchEvents()).unwrap();
|
||||||
|
} catch (error: any) {
|
||||||
|
setSubmitError(error?.message || t("validation.submitFailed"));
|
||||||
|
setSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSubmitting(false);
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,19 +258,10 @@ const FarmerCalendarEventModal = ({
|
|||||||
setTagLoading(true);
|
setTagLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const events = await eventService.listEvents();
|
const tags = await eventService.listTags();
|
||||||
const options = Array.from(
|
const options = tags
|
||||||
new Set(
|
.map((tag) => tag.value?.trim())
|
||||||
events.flatMap((event) =>
|
.filter((tag): tag is string => Boolean(tag));
|
||||||
Array.isArray(event.tags)
|
|
||||||
? event.tags
|
|
||||||
.filter((tag): tag is string => typeof tag === "string")
|
|
||||||
.map((tag) => tag.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
: [],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
setTagOptions(options);
|
setTagOptions(options);
|
||||||
@@ -265,6 +286,11 @@ const FarmerCalendarEventModal = ({
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="flex flex-col gap-6"
|
className="flex flex-col gap-6"
|
||||||
>
|
>
|
||||||
|
{submitError ? (
|
||||||
|
<Typography color="error" variant="body2">
|
||||||
|
{submitError}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
<Controller
|
<Controller
|
||||||
name="title"
|
name="title"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -385,6 +411,7 @@ const FarmerCalendarEventModal = ({
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="error"
|
color="error"
|
||||||
onClick={handleDeleteButtonClick}
|
onClick={handleDeleteButtonClick}
|
||||||
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{t("actions.delete")}
|
{t("actions.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -392,7 +419,7 @@ const FarmerCalendarEventModal = ({
|
|||||||
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||||
{t("actions.cancel")}
|
{t("actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" variant="contained">
|
<Button type="submit" variant="contained" disabled={submitting}>
|
||||||
{isEditMode ? t("actions.update") : t("actions.create")}
|
{isEditMode ? t("actions.update") : t("actions.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -23,15 +23,12 @@ import useMediaQuery from "@mui/material/useMediaQuery";
|
|||||||
import type { EventInput } from "@fullcalendar/core";
|
import type { EventInput } from "@fullcalendar/core";
|
||||||
import { format, isThisWeek, isToday, parseISO } from "date-fns";
|
import { format, isThisWeek, isToday, parseISO } from "date-fns";
|
||||||
|
|
||||||
|
import { eventService } from "@/libs/api";
|
||||||
|
import { useFarmHub } from "@/hooks/useFarmHub";
|
||||||
import AppReactDatepicker from "@/libs/styles/AppReactDatepicker";
|
import AppReactDatepicker from "@/libs/styles/AppReactDatepicker";
|
||||||
import AppFullCalendar from "@/libs/styles/AppFullCalendar";
|
import AppFullCalendar from "@/libs/styles/AppFullCalendar";
|
||||||
import { useAppDispatch, useAppSelector } from "@/redux-store";
|
import { useAppDispatch, useAppSelector } from "@/redux-store";
|
||||||
import {
|
import { fetchEvents, selectedEvent } from "@/redux-store/slices/calendar";
|
||||||
fetchEvents,
|
|
||||||
filterAllCalendarLabels,
|
|
||||||
filterCalendarLabel,
|
|
||||||
selectedEvent,
|
|
||||||
} from "@/redux-store/slices/calendar";
|
|
||||||
import type {
|
import type {
|
||||||
CalendarColors,
|
CalendarColors,
|
||||||
CalendarFiltersType,
|
CalendarFiltersType,
|
||||||
@@ -51,6 +48,12 @@ type EventCardData = {
|
|||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CalendarTagChip = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
const calendarsColor: CalendarColors = {
|
const calendarsColor: CalendarColors = {
|
||||||
Personal: "error",
|
Personal: "error",
|
||||||
Business: "primary",
|
Business: "primary",
|
||||||
@@ -343,29 +346,78 @@ const FarmerCalendarPage = () => {
|
|||||||
const t = useTranslations("farmerCalendar");
|
const t = useTranslations("farmerCalendar");
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { farmHub } = useFarmHub();
|
||||||
|
const farmUuid = farmHub?.farm_uuid;
|
||||||
const mdUp = useMediaQuery(theme.breakpoints.up("lg"));
|
const mdUp = useMediaQuery(theme.breakpoints.up("lg"));
|
||||||
const [calendarApi, setCalendarApi] = useState<null | any>(null);
|
const [calendarApi, setCalendarApi] = useState<null | any>(null);
|
||||||
const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
|
const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
|
||||||
const [eventModalOpen, setEventModalOpen] = useState(false);
|
const [eventModalOpen, setEventModalOpen] = useState(false);
|
||||||
const [eventDetailsOpen, setEventDetailsOpen] = useState(false);
|
const [eventDetailsOpen, setEventDetailsOpen] = useState(false);
|
||||||
|
const [tagOptions, setTagOptions] = useState<CalendarTagChip[]>([]);
|
||||||
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||||
|
|
||||||
const calendarStore = useAppSelector((state) => state.calendarReducer);
|
const calendarStore = useAppSelector((state) => state.calendarReducer);
|
||||||
const { events, selectedCalendars, loading, error } = calendarStore;
|
const { events, loading, error } = calendarStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchEvents());
|
dispatch(fetchEvents(farmUuid ? { farm_uuid: farmUuid } : undefined));
|
||||||
}, [dispatch]);
|
}, [dispatch, farmUuid]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
const fetchTags = async () => {
|
||||||
|
try {
|
||||||
|
const tags = await eventService.listTags(farmUuid);
|
||||||
|
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
setTagOptions(
|
||||||
|
tags
|
||||||
|
.map((tag) => ({
|
||||||
|
id: tag.id,
|
||||||
|
label: tag.label?.trim() || tag.value?.trim() || "",
|
||||||
|
value: tag.value?.trim() || tag.label?.trim() || "",
|
||||||
|
}))
|
||||||
|
.filter((tag) => tag.value.length > 0),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
if (active) {
|
||||||
|
setTagOptions([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void fetchTags();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [farmUuid]);
|
||||||
|
|
||||||
const eventCards = useMemo(() => createEventCards(events), [events]);
|
const eventCards = useMemo(() => createEventCards(events), [events]);
|
||||||
const allEventCards = useMemo(
|
const baseEvents = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createEventCards(
|
calendarStore.filteredEvents.length ? calendarStore.filteredEvents : events,
|
||||||
calendarStore.filteredEvents.length
|
|
||||||
? calendarStore.filteredEvents
|
|
||||||
: events,
|
|
||||||
),
|
|
||||||
[calendarStore.filteredEvents, events],
|
[calendarStore.filteredEvents, events],
|
||||||
);
|
);
|
||||||
|
const tagFilteredEvents = useMemo(() => {
|
||||||
|
if (selectedTags.length === 0) return baseEvents;
|
||||||
|
|
||||||
|
return baseEvents.filter((event) => {
|
||||||
|
const tags = Array.isArray(event.extendedProps?.tags)
|
||||||
|
? event.extendedProps.tags
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return tags.some(
|
||||||
|
(tag) => typeof tag === "string" && selectedTags.includes(tag.trim()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [baseEvents, selectedTags]);
|
||||||
|
const allEventCards = useMemo(
|
||||||
|
() => createEventCards(tagFilteredEvents),
|
||||||
|
[tagFilteredEvents],
|
||||||
|
);
|
||||||
|
|
||||||
const todayCount = useMemo(
|
const todayCount = useMemo(
|
||||||
() => eventCards.filter((event) => isToday(event.start)).length,
|
() => eventCards.filter((event) => isToday(event.start)).length,
|
||||||
@@ -393,27 +445,25 @@ const FarmerCalendarPage = () => {
|
|||||||
.slice(0, 5),
|
.slice(0, 5),
|
||||||
[eventCards],
|
[eventCards],
|
||||||
);
|
);
|
||||||
const categoryCounts = useMemo(
|
const tagCounts = useMemo(
|
||||||
() =>
|
() =>
|
||||||
allEventCards.reduce(
|
tagOptions.reduce<Record<string, number>>((accumulator, tag) => {
|
||||||
(accumulator, event) => {
|
accumulator[tag.value] = baseEvents.filter((event) => {
|
||||||
accumulator[event.category] += 1;
|
const eventTags = Array.isArray(event.extendedProps?.tags)
|
||||||
|
? event.extendedProps.tags
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return eventTags.some(
|
||||||
|
(item) => typeof item === "string" && item.trim() === tag.value,
|
||||||
|
);
|
||||||
|
}).length;
|
||||||
|
|
||||||
return accumulator;
|
return accumulator;
|
||||||
},
|
}, {}),
|
||||||
{
|
[baseEvents, tagOptions],
|
||||||
Personal: 0,
|
|
||||||
Business: 0,
|
|
||||||
Family: 0,
|
|
||||||
Holiday: 0,
|
|
||||||
ETC: 0,
|
|
||||||
} as Record<CalendarFiltersType, number>,
|
|
||||||
),
|
|
||||||
[allEventCards],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedCount = selectedCalendars.length;
|
const allTagsSelected = selectedTags.length === 0;
|
||||||
const allSelected = selectedCount === Object.keys(calendarsColor).length;
|
|
||||||
|
|
||||||
const handleLeftSidebarToggle = () =>
|
const handleLeftSidebarToggle = () =>
|
||||||
setLeftSidebarOpen((previous) => !previous);
|
setLeftSidebarOpen((previous) => !previous);
|
||||||
@@ -433,6 +483,13 @@ const FarmerCalendarPage = () => {
|
|||||||
setEventDetailsOpen(false);
|
setEventDetailsOpen(false);
|
||||||
setEventModalOpen(true);
|
setEventModalOpen(true);
|
||||||
};
|
};
|
||||||
|
const toggleTagFilter = (value: string) => {
|
||||||
|
setSelectedTags((current) =>
|
||||||
|
current.includes(value)
|
||||||
|
? current.filter((item) => item !== value)
|
||||||
|
: [...current, value],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const insightCards = [
|
const insightCards = [
|
||||||
{
|
{
|
||||||
@@ -595,46 +652,57 @@ const FarmerCalendarPage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Chip
|
<Chip
|
||||||
label={
|
label={
|
||||||
allSelected
|
allTagsSelected
|
||||||
? t("sidebar.focusLanes.allVisible")
|
? t("sidebar.focusLanes.allVisible")
|
||||||
: t("sidebar.focusLanes.activeCount", {
|
: t("sidebar.focusLanes.activeCount", {
|
||||||
count: selectedCount,
|
count: selectedTags.length,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
color={allSelected ? "success" : "default"}
|
color={allTagsSelected ? "success" : "default"}
|
||||||
variant={allSelected ? "filled" : "outlined"}
|
variant={allTagsSelected ? "filled" : "outlined"}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Stack direction="row" flexWrap="wrap" gap={1.2}>
|
<Stack direction="row" flexWrap="wrap" gap={1.2}>
|
||||||
<Chip
|
<Chip
|
||||||
label={
|
label={
|
||||||
allSelected
|
allTagsSelected
|
||||||
? t("sidebar.focusLanes.clearFilters")
|
? t("sidebar.focusLanes.clearFilters")
|
||||||
: t("sidebar.focusLanes.viewAll")
|
: t("sidebar.focusLanes.viewAll")
|
||||||
}
|
}
|
||||||
clickable
|
clickable
|
||||||
color={allSelected ? "success" : "default"}
|
color={allTagsSelected ? "success" : "default"}
|
||||||
variant={allSelected ? "filled" : "outlined"}
|
variant={allTagsSelected ? "filled" : "outlined"}
|
||||||
onClick={() => dispatch(filterAllCalendarLabels(!allSelected))}
|
onClick={() => setSelectedTags([])}
|
||||||
/>
|
/>
|
||||||
{(Object.keys(categoryDetails) as CalendarFiltersType[]).map(
|
{tagOptions.map((tag, index) => {
|
||||||
(key) => {
|
const paletteSequence: PaletteAccent[] = [
|
||||||
const detail = categoryDetails[key];
|
"error",
|
||||||
const selected = selectedCalendars.includes(key);
|
"primary",
|
||||||
|
"warning",
|
||||||
|
"success",
|
||||||
|
"info",
|
||||||
|
];
|
||||||
|
const iconSequence = [
|
||||||
|
"tabler-sun-high",
|
||||||
|
"tabler-tractor",
|
||||||
|
"tabler-users-group",
|
||||||
|
"tabler-leaf",
|
||||||
|
"tabler-sparkles",
|
||||||
|
];
|
||||||
|
const selected = selectedTags.includes(tag.value);
|
||||||
|
const accent = paletteSequence[index % paletteSequence.length];
|
||||||
|
const icon = iconSequence[index % iconSequence.length];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
key={key}
|
key={tag.id}
|
||||||
icon={<i className={detail.icon} />}
|
icon={<i className={icon} />}
|
||||||
label={t("sidebar.focusLanes.categoryCount", {
|
label={`${tag.label} (${tagCounts[tag.value] ?? 0})`}
|
||||||
label: t(detail.labelKey),
|
|
||||||
count: categoryCounts[key],
|
|
||||||
})}
|
|
||||||
clickable
|
clickable
|
||||||
color={detail.accent}
|
color={accent}
|
||||||
variant={selected ? "filled" : "outlined"}
|
variant={selected ? "filled" : "outlined"}
|
||||||
onClick={() => dispatch(filterCalendarLabel(key))}
|
onClick={() => toggleTagFilter(tag.value)}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiChip-icon": {
|
"& .MuiChip-icon": {
|
||||||
fontSize: "1rem",
|
fontSize: "1rem",
|
||||||
@@ -642,8 +710,7 @@ const FarmerCalendarPage = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
})}
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -908,26 +975,18 @@ const FarmerCalendarPage = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
|
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
|
||||||
{(Object.keys(categoryDetails) as CalendarFiltersType[]).map(
|
{tagOptions.map((tag) => (
|
||||||
(key) => (
|
|
||||||
<Chip
|
<Chip
|
||||||
key={key}
|
key={tag.id}
|
||||||
size="small"
|
size="small"
|
||||||
label={t("hero.categoryCount", {
|
label={`${tag.label}: ${tagCounts[tag.value] ?? 0}`}
|
||||||
label: t(categoryDetails[key].labelKey),
|
|
||||||
count: categoryCounts[key],
|
|
||||||
})}
|
|
||||||
sx={{
|
sx={{
|
||||||
color: "common.white",
|
color: "common.white",
|
||||||
backgroundColor: alpha(
|
backgroundColor: alpha(theme.palette.common.white, 0.12),
|
||||||
theme.palette.common.white,
|
|
||||||
0.12,
|
|
||||||
),
|
|
||||||
border: `1px solid ${alpha(theme.palette.common.white, 0.14)}`,
|
border: `1px solid ${alpha(theme.palette.common.white, 0.14)}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
))}
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -994,7 +1053,10 @@ const FarmerCalendarPage = () => {
|
|||||||
<Calendar
|
<Calendar
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
calendarApi={calendarApi}
|
calendarApi={calendarApi}
|
||||||
calendarStore={calendarStore}
|
calendarStore={{
|
||||||
|
...calendarStore,
|
||||||
|
events: tagFilteredEvents,
|
||||||
|
}}
|
||||||
setCalendarApi={setCalendarApi}
|
setCalendarApi={setCalendarApi}
|
||||||
calendarsColor={calendarsColor}
|
calendarsColor={calendarsColor}
|
||||||
handleLeftSidebarToggle={handleLeftSidebarToggle}
|
handleLeftSidebarToggle={handleLeftSidebarToggle}
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ type ParserConfig = {
|
|||||||
getItemCount: (plan: unknown) => number;
|
getItemCount: (plan: unknown) => number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FertilizationPlanParserPageProps = {
|
||||||
|
initialTab?: ParserTabKey;
|
||||||
|
enabledTabs?: ParserTabKey[];
|
||||||
|
};
|
||||||
|
|
||||||
const createInitialTabState = (): TabState => ({
|
const createInitialTabState = (): TabState => ({
|
||||||
message: "",
|
message: "",
|
||||||
response: null,
|
response: null,
|
||||||
@@ -551,12 +556,22 @@ const PARSER_CONFIGS: Record<ParserTabKey, ParserConfig> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const FertilizationPlanParserPage = () => {
|
const FertilizationPlanParserPage = ({
|
||||||
|
initialTab = "fertilization",
|
||||||
|
enabledTabs = ["fertilization", "irrigation"],
|
||||||
|
}: FertilizationPlanParserPageProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { farmHub } = useFarmHub();
|
const { farmHub } = useFarmHub();
|
||||||
const farmUuid = farmHub?.farm_uuid;
|
const farmUuid = farmHub?.farm_uuid;
|
||||||
|
const availableTabs = enabledTabs.filter(
|
||||||
|
(tab): tab is ParserTabKey => tab in PARSER_CONFIGS,
|
||||||
|
);
|
||||||
|
const resolvedInitialTab = availableTabs.includes(initialTab)
|
||||||
|
? initialTab
|
||||||
|
: availableTabs[0] ?? "fertilization";
|
||||||
|
const singleTabMode = availableTabs.length === 1;
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<ParserTabKey>("fertilization");
|
const [activeTab, setActiveTab] = useState<ParserTabKey>(resolvedInitialTab);
|
||||||
const [tabStates, setTabStates] = useState<Record<ParserTabKey, TabState>>({
|
const [tabStates, setTabStates] = useState<Record<ParserTabKey, TabState>>({
|
||||||
fertilization: createInitialTabState(),
|
fertilization: createInitialTabState(),
|
||||||
irrigation: createInitialTabState(),
|
irrigation: createInitialTabState(),
|
||||||
@@ -907,11 +922,14 @@ const FertilizationPlanParserPage = () => {
|
|||||||
variant="h5"
|
variant="h5"
|
||||||
sx={{ fontWeight: 800, mb: 1.5 }}
|
sx={{ fontWeight: 800, mb: 1.5 }}
|
||||||
>
|
>
|
||||||
دو جریان هوشمند، یک صفحه واحد
|
{singleTabMode
|
||||||
|
? `جریان هوشمند ${config.label}`
|
||||||
|
: "دو جریان هوشمند، یک صفحه واحد"}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color="text.secondary">
|
<Typography color="text.secondary">
|
||||||
بین تب آبیاری و تب کودهی جابه جا شو و هر کدام را با
|
{singleTabMode
|
||||||
همان flow سوال های تکمیلی تا JSON نهایی پیش ببر.
|
? `ورودی متنی برنامه ${config.label} را بفرست، ابهام ها را کامل کن و خروجی JSON نهایی بگیر.`
|
||||||
|
: "بین تب آبیاری و تب کودهی جابه جا شو و هر کدام را با همان flow سوال های تکمیلی تا JSON نهایی پیش ببر."}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Stepper
|
<Stepper
|
||||||
@@ -948,6 +966,7 @@ const FertilizationPlanParserPage = () => {
|
|||||||
<CardContent sx={{ p: { xs: 4, md: 5 } }}>
|
<CardContent sx={{ p: { xs: 4, md: 5 } }}>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Box>
|
<Box>
|
||||||
|
{!singleTabMode ? (
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={(_, value: ParserTabKey) => setActiveTab(value)}
|
onChange={(_, value: ParserTabKey) => setActiveTab(value)}
|
||||||
@@ -960,7 +979,10 @@ const FertilizationPlanParserPage = () => {
|
|||||||
"& .MuiTabs-indicator": { display: "none" },
|
"& .MuiTabs-indicator": { display: "none" },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Object.values(PARSER_CONFIGS).map((tabConfig) => (
|
{availableTabs.map((tabKey) => {
|
||||||
|
const tabConfig = PARSER_CONFIGS[tabKey];
|
||||||
|
|
||||||
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
key={tabConfig.key}
|
key={tabConfig.key}
|
||||||
value={tabConfig.key}
|
value={tabConfig.key}
|
||||||
@@ -979,8 +1001,10 @@ const FertilizationPlanParserPage = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { Theme } from "@mui/material/styles";
|
||||||
|
import { alpha, useTheme } from "@mui/material/styles";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Chip from "@mui/material/Chip";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import Drawer from "@mui/material/Drawer";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Radio from "@mui/material/Radio";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
|
||||||
|
export type RelatedPlanItem = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
finalProduct: string;
|
||||||
|
harvestTime: string;
|
||||||
|
outputTon: number;
|
||||||
|
methodLabel: string;
|
||||||
|
status: "active" | "draft";
|
||||||
|
};
|
||||||
|
|
||||||
|
type SummaryItem = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RelatedPlanSelectorProps = {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
currentPlanName: string;
|
||||||
|
currentPlanStatus: string;
|
||||||
|
currentPlanOutput: string;
|
||||||
|
summaryItems: SummaryItem[];
|
||||||
|
relatedTitle: string;
|
||||||
|
relatedDescription: string;
|
||||||
|
relatedPlans: RelatedPlanItem[];
|
||||||
|
selectedRelatedPlanId: number | null;
|
||||||
|
onSelectRelatedPlan: (planId: number) => void;
|
||||||
|
onConfirm: (selectedPlan: RelatedPlanItem) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const backdropBlurSx = { backdropFilter: "blur(6px)", backgroundColor: "rgba(15, 23, 42, 0.32)" };
|
||||||
|
|
||||||
|
const RelatedPlanSelectorContent = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
currentPlanName,
|
||||||
|
currentPlanStatus,
|
||||||
|
currentPlanOutput,
|
||||||
|
summaryItems,
|
||||||
|
relatedTitle,
|
||||||
|
relatedDescription,
|
||||||
|
relatedPlans,
|
||||||
|
selectedRelatedPlanId,
|
||||||
|
onSelectRelatedPlan,
|
||||||
|
onConfirm,
|
||||||
|
onClose,
|
||||||
|
}: RelatedPlanSelectorProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const selectedPlan = useMemo(
|
||||||
|
() => relatedPlans.find((plan) => plan.id === selectedRelatedPlanId) ?? null,
|
||||||
|
[relatedPlans, selectedRelatedPlanId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={2}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" sx={{ mb: 1 }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary">{description}</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<IconButton onClick={onClose} size="small">
|
||||||
|
<i className="tabler-x text-[22px]" />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
borderColor: alpha(theme.palette.primary.main, 0.22),
|
||||||
|
background: `linear-gradient(135deg, ${alpha(theme.palette.primary.main, 0.12)}, ${alpha(theme.palette.success.main, 0.08)})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Stack spacing={2.5}>
|
||||||
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={1.5} justifyContent="space-between">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6">{currentPlanName}</Typography>
|
||||||
|
<Typography color="text.secondary">{currentPlanOutput}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Chip color="success" variant="tonal" label={currentPlanStatus} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack direction={{ xs: "column", md: "row" }} spacing={1.5} useFlexGap flexWrap="wrap">
|
||||||
|
{summaryItems.map((item) => (
|
||||||
|
<Box
|
||||||
|
key={item.label}
|
||||||
|
sx={{
|
||||||
|
minWidth: 160,
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 3,
|
||||||
|
backgroundColor: alpha(theme.palette.background.paper, 0.7),
|
||||||
|
border: `1px solid ${alpha(theme.palette.divider, 0.8)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{item.label}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" fontWeight={700}>
|
||||||
|
{item.value}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="h6">{relatedTitle}</Typography>
|
||||||
|
<Typography color="text.secondary">{relatedDescription}</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{relatedPlans.map((plan) => {
|
||||||
|
const isSelected = plan.id === selectedRelatedPlanId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={plan.id}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => onSelectRelatedPlan(plan.id)}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
borderRadius: 4,
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
borderColor: isSelected
|
||||||
|
? theme.palette.primary.main
|
||||||
|
: alpha(theme.palette.divider, 0.9),
|
||||||
|
boxShadow: isSelected
|
||||||
|
? `0 0 0 3px ${alpha(theme.palette.primary.main, 0.14)}`
|
||||||
|
: "none",
|
||||||
|
backgroundColor: isSelected
|
||||||
|
? alpha(theme.palette.primary.main, 0.05)
|
||||||
|
: theme.palette.background.paper,
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-2px)",
|
||||||
|
boxShadow: theme.shadows[3],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Stack direction="row" spacing={2} alignItems="flex-start">
|
||||||
|
<Radio checked={isSelected} value={plan.id} />
|
||||||
|
|
||||||
|
<Stack spacing={1.5} sx={{ flexGrow: 1 }}>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", sm: "row" }}
|
||||||
|
spacing={1.5}
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6">{plan.title}</Typography>
|
||||||
|
<Typography color="text.secondary">{plan.finalProduct}</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap">
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={`${plan.outputTon} تن`}
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={plan.status === "active" ? "فعال" : "پیشنویس"}
|
||||||
|
color={plan.status === "active" ? "success" : "default"}
|
||||||
|
variant={plan.status === "active" ? "filled" : "tonal"}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Stack direction={{ xs: "column", md: "row" }} spacing={1.5} useFlexGap flexWrap="wrap">
|
||||||
|
<Chip label={`زمان برداشت: ${plan.harvestTime}`} variant="tonal" />
|
||||||
|
<Chip label={plan.methodLabel} variant="tonal" />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
borderStyle: "dashed",
|
||||||
|
backgroundColor: alpha(theme.palette.success.main, 0.05),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Stack direction={{ xs: "column", md: "row" }} spacing={2} alignItems={{ xs: "stretch", md: "center" }} justifyContent="space-between">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" fontWeight={700}>
|
||||||
|
{selectedPlan ? `انتخاب شد: ${selectedPlan.title}` : "یک برنامه مرتبط انتخاب کنید"}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
{selectedPlan
|
||||||
|
? `محصول نهایی ${selectedPlan.finalProduct} با برداشت ${selectedPlan.harvestTime}`
|
||||||
|
: "برای ادامه، یکی از برنامههای لیست بالا را انتخاب کنید."}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
size="large"
|
||||||
|
startIcon={<i className="tabler-check text-[20px]" />}
|
||||||
|
disabled={!selectedPlan}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedPlan) {
|
||||||
|
onConfirm(selectedPlan);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
تایید انتخاب
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RelatedPlanSelector = (props: RelatedPlanSelectorProps) => {
|
||||||
|
const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down("sm"));
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
anchor="bottom"
|
||||||
|
open={props.open}
|
||||||
|
onClose={props.onClose}
|
||||||
|
ModalProps={{ keepMounted: true }}
|
||||||
|
slotProps={{ backdrop: { sx: backdropBlurSx } }}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
height: "88vh",
|
||||||
|
borderTopLeftRadius: 24,
|
||||||
|
borderTopRightRadius: 24,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ px: 2.5, pt: 1.5, pb: 0.5, display: "flex", justifyContent: "center" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 54,
|
||||||
|
height: 6,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "divider",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ p: 3, overflowY: "auto" }}>
|
||||||
|
<RelatedPlanSelectorContent {...props} />
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
fullWidth
|
||||||
|
maxWidth="md"
|
||||||
|
open={props.open}
|
||||||
|
onClose={props.onClose}
|
||||||
|
slotProps={{ backdrop: { sx: backdropBlurSx } }}
|
||||||
|
PaperProps={{ sx: { borderRadius: 6 } }}
|
||||||
|
>
|
||||||
|
<DialogContent sx={{ p: 4 }}>
|
||||||
|
<RelatedPlanSelectorContent {...props} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RelatedPlanSelector;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
@@ -24,97 +25,117 @@ import OptionMenu from "@core/components/option-menu";
|
|||||||
import CustomAvatar from "@core/components/mui/Avatar";
|
import CustomAvatar from "@core/components/mui/Avatar";
|
||||||
import CustomTextField from "@core/components/mui/TextField";
|
import CustomTextField from "@core/components/mui/TextField";
|
||||||
import HorizontalWithAvatar from "@components/card-statistics/HorizontalWithAvatar";
|
import HorizontalWithAvatar from "@components/card-statistics/HorizontalWithAvatar";
|
||||||
|
import { useFarmHub } from "@/hooks/useFarmHub";
|
||||||
|
import { farmerTodoService } from "@/libs/api/services/farmerTodoService";
|
||||||
|
import type {
|
||||||
|
FarmerTodoPriority,
|
||||||
|
FarmerTodoStatus,
|
||||||
|
FarmerTodoSummary,
|
||||||
|
FarmerTodoTask,
|
||||||
|
FarmerTodoZoneOption,
|
||||||
|
} from "@/libs/api/services/farmerTodoService";
|
||||||
|
|
||||||
type TaskPriority = "زیاد" | "متوسط" | "کم";
|
|
||||||
type TaskStatus = "open" | "done";
|
|
||||||
type TaskSegment = "همه" | "امروز" | "فوری" | "انجام شده";
|
type TaskSegment = "همه" | "امروز" | "فوری" | "انجام شده";
|
||||||
|
|
||||||
type FarmerTask = {
|
const priorityMeta: Record<
|
||||||
id: number;
|
FarmerTodoPriority,
|
||||||
title: string;
|
{ color: ThemeColor; icon: string }
|
||||||
zone: string;
|
> =
|
||||||
time: string;
|
|
||||||
priority: TaskPriority;
|
|
||||||
note: string;
|
|
||||||
tags: string[];
|
|
||||||
status: TaskStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialTasks: FarmerTask[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "بررسی رطوبت ردیف شمالی و تنظیم آبیاری قطره ای",
|
|
||||||
zone: "قطعه گندم - شمال مزرعه",
|
|
||||||
time: "06:30",
|
|
||||||
priority: "زیاد",
|
|
||||||
note: "اگر رطوبت کمتر از 28٪ بود، دور دوم آبیاری را 20 دقیقه جلو بینداز.",
|
|
||||||
tags: ["آبیاری", "صبح زود"],
|
|
||||||
status: "open",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "نمونه برداری خاک برای بخش سبزیجات",
|
|
||||||
zone: "گلخانه شماره 2",
|
|
||||||
time: "09:15",
|
|
||||||
priority: "متوسط",
|
|
||||||
note: "سه نقطه از بستر برداشت شود و برای آزمایش فسفر ثبت گردد.",
|
|
||||||
tags: ["خاک", "آزمایش"],
|
|
||||||
status: "open",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "هماهنگی با راننده برای ارسال بار جو",
|
|
||||||
zone: "انبار مرکزی",
|
|
||||||
time: "12:00",
|
|
||||||
priority: "کم",
|
|
||||||
note: "وزن نهایی بارنامه قبل از خروج با باسکول تطبیق داده شود.",
|
|
||||||
tags: ["لجستیک", "فروش"],
|
|
||||||
status: "done",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "بازدید سریع برای نشانه های آفت روی برگ های تازه",
|
|
||||||
zone: "باغچه آزمایشی غربی",
|
|
||||||
time: "17:40",
|
|
||||||
priority: "زیاد",
|
|
||||||
note: "فقط لکه های جدید را علامت بزن و برای تیم سمپاشی عکس بگیر.",
|
|
||||||
tags: ["آفت", "بازدید عصر"],
|
|
||||||
status: "open",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const priorityMeta: Record<TaskPriority, { color: ThemeColor; icon: string }> =
|
|
||||||
{
|
{
|
||||||
زیاد: { color: "error", icon: "tabler-alert-triangle" },
|
زیاد: { color: "error", icon: "tabler-alert-triangle" },
|
||||||
متوسط: { color: "warning", icon: "tabler-sun-high" },
|
متوسط: { color: "warning", icon: "tabler-sun-high" },
|
||||||
کم: { color: "success", icon: "tabler-leaf" },
|
کم: { color: "success", icon: "tabler-leaf" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPriorityMeta = (priority: string) =>
|
||||||
|
priorityMeta[priority as FarmerTodoPriority] ?? priorityMeta["متوسط"];
|
||||||
|
|
||||||
const segments: TaskSegment[] = ["همه", "امروز", "فوری", "انجام شده"];
|
const segments: TaskSegment[] = ["همه", "امروز", "فوری", "انجام شده"];
|
||||||
|
|
||||||
|
const todayDate = () => new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
const defaultSummary: FarmerTodoSummary = {
|
||||||
|
todayTasksCount: 0,
|
||||||
|
completedCount: 0,
|
||||||
|
urgentCount: 0,
|
||||||
|
progressValue: 0,
|
||||||
|
nextTask: null,
|
||||||
|
};
|
||||||
|
|
||||||
const FarmerTodoPage = () => {
|
const FarmerTodoPage = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { farmHub } = useFarmHub();
|
||||||
|
const farmUuid = farmHub?.farm_uuid;
|
||||||
|
|
||||||
const [tasks, setTasks] = useState(initialTasks);
|
const [tasks, setTasks] = useState<FarmerTodoTask[]>([]);
|
||||||
|
const [summary, setSummary] = useState<FarmerTodoSummary>(defaultSummary);
|
||||||
|
const [zones, setZones] = useState<FarmerTodoZoneOption[]>([]);
|
||||||
const [segment, setSegment] = useState<TaskSegment>("همه");
|
const [segment, setSegment] = useState<TaskSegment>("همه");
|
||||||
const [draftTitle, setDraftTitle] = useState("");
|
const [draftTitle, setDraftTitle] = useState("");
|
||||||
const [draftZone, setDraftZone] = useState("قطعه گندم - شمال مزرعه");
|
const [draftZone, setDraftZone] = useState("");
|
||||||
const [draftTime, setDraftTime] = useState("07:00");
|
const [draftTime, setDraftTime] = useState("07:00");
|
||||||
const [draftPriority, setDraftPriority] = useState<TaskPriority>("متوسط");
|
const [draftPriority, setDraftPriority] =
|
||||||
|
useState<FarmerTodoPriority>("متوسط");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [togglingId, setTogglingId] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const completedCount = useMemo(
|
const completedCount = summary.completedCount;
|
||||||
() => tasks.filter((task) => task.status === "done").length,
|
const openCount = useMemo(
|
||||||
[tasks],
|
|
||||||
);
|
|
||||||
const openCount = tasks.length - completedCount;
|
|
||||||
const urgentCount = useMemo(
|
|
||||||
() =>
|
() =>
|
||||||
tasks.filter((task) => task.priority === "زیاد" && task.status === "open")
|
tasks.filter(
|
||||||
.length,
|
(task) => task.scheduledDate === todayDate() && task.status === "open",
|
||||||
|
).length,
|
||||||
[tasks],
|
[tasks],
|
||||||
);
|
);
|
||||||
const progressValue =
|
const urgentCount = summary.urgentCount;
|
||||||
tasks.length === 0 ? 0 : Math.round((completedCount / tasks.length) * 100);
|
const progressValue = summary.progressValue;
|
||||||
|
const nextTask = summary.nextTask;
|
||||||
|
|
||||||
|
const refreshSummary = async () => {
|
||||||
|
if (!farmUuid) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const latestSummary = await farmerTodoService.getSummary(farmUuid);
|
||||||
|
setSummary(latestSummary);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
if (!farmUuid) {
|
||||||
|
setTasks([]);
|
||||||
|
setZones([]);
|
||||||
|
setSummary(defaultSummary);
|
||||||
|
setDraftZone("");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [taskList, summaryData, zoneList] = await Promise.all([
|
||||||
|
farmerTodoService.listTasks({ farm_uuid: farmUuid }),
|
||||||
|
farmerTodoService.getSummary(farmUuid),
|
||||||
|
farmerTodoService.listZones(farmUuid),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setTasks(taskList);
|
||||||
|
setSummary(summaryData);
|
||||||
|
setZones(zoneList);
|
||||||
|
setDraftZone((current) => current || zoneList[0]?.value || "");
|
||||||
|
} catch (apiError: any) {
|
||||||
|
setError(apiError?.message || "دریافت اطلاعات تسکها انجام نشد.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadData();
|
||||||
|
}, [farmUuid]);
|
||||||
|
|
||||||
const filteredTasks = useMemo(() => {
|
const filteredTasks = useMemo(() => {
|
||||||
switch (segment) {
|
switch (segment) {
|
||||||
@@ -125,7 +146,7 @@ const FarmerTodoPage = () => {
|
|||||||
case "انجام شده":
|
case "انجام شده":
|
||||||
return tasks.filter((task) => task.status === "done");
|
return tasks.filter((task) => task.status === "done");
|
||||||
case "امروز":
|
case "امروز":
|
||||||
return tasks;
|
return tasks.filter((task) => task.scheduledDate === todayDate());
|
||||||
default:
|
default:
|
||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
@@ -152,36 +173,61 @@ const FarmerTodoPage = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const toggleTask = (taskId: number) => {
|
const toggleTask = async (task: FarmerTodoTask) => {
|
||||||
|
if (!farmUuid) return;
|
||||||
|
|
||||||
|
const nextStatus: FarmerTodoStatus =
|
||||||
|
task.status === "done" ? "open" : "done";
|
||||||
|
|
||||||
|
setTogglingId(task.id);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedTask = await farmerTodoService.updateTask(task.id, {
|
||||||
|
farm_uuid: farmUuid,
|
||||||
|
status: nextStatus,
|
||||||
|
});
|
||||||
|
|
||||||
setTasks((currentTasks) =>
|
setTasks((currentTasks) =>
|
||||||
currentTasks.map((task) =>
|
currentTasks.map((item) => (item.id === task.id ? updatedTask : item)),
|
||||||
task.id === taskId
|
|
||||||
? { ...task, status: task.status === "done" ? "open" : "done" }
|
|
||||||
: task,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
await refreshSummary();
|
||||||
|
} catch (apiError: any) {
|
||||||
|
setError(apiError?.message || "تغییر وضعیت تسک انجام نشد.");
|
||||||
|
} finally {
|
||||||
|
setTogglingId(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTask = () => {
|
const addTask = async () => {
|
||||||
if (!draftTitle.trim()) return;
|
if (!farmUuid || !draftTitle.trim() || !draftZone.trim()) return;
|
||||||
|
|
||||||
setTasks((currentTasks) => [
|
setSubmitting(true);
|
||||||
{
|
setError(null);
|
||||||
id: Date.now(),
|
|
||||||
|
try {
|
||||||
|
const createdTask = await farmerTodoService.createTask({
|
||||||
|
farm_uuid: farmUuid,
|
||||||
title: draftTitle.trim(),
|
title: draftTitle.trim(),
|
||||||
zone: draftZone,
|
zone: draftZone.trim(),
|
||||||
|
scheduledDate: todayDate(),
|
||||||
time: draftTime,
|
time: draftTime,
|
||||||
priority: draftPriority,
|
priority: draftPriority,
|
||||||
note: "بعد از ثبت انجام، اگر موردی غیرعادی بود برای شیفت بعدی یادداشت بگذار.",
|
note: "بعد از ثبت انجام، اگر موردی غیرعادی بود برای شیفت بعدی یادداشت بگذار.",
|
||||||
tags: [draftPriority === "زیاد" ? "فوری" : "روزانه", "ثبت دستی"],
|
tags: [draftPriority === "زیاد" ? "فوری" : "روزانه", "ثبت دستی"],
|
||||||
status: "open",
|
status: "open",
|
||||||
},
|
});
|
||||||
...currentTasks,
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
setTasks((currentTasks) => [createdTask, ...currentTasks]);
|
||||||
|
await refreshSummary();
|
||||||
setDraftTitle("");
|
setDraftTitle("");
|
||||||
setDraftTime("07:00");
|
setDraftTime("07:00");
|
||||||
setDraftPriority("متوسط");
|
setDraftPriority("متوسط");
|
||||||
|
} catch (apiError: any) {
|
||||||
|
setError(apiError?.message || "ایجاد تسک جدید انجام نشد.");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -275,7 +321,9 @@ const FarmerTodoPage = () => {
|
|||||||
<Typography
|
<Typography
|
||||||
sx={{ color: "common.white", fontWeight: 700 }}
|
sx={{ color: "common.white", fontWeight: 700 }}
|
||||||
>
|
>
|
||||||
بازدید 06:30 از ردیف شمالی
|
{nextTask
|
||||||
|
? `${nextTask.time} - ${nextTask.title}`
|
||||||
|
: "هنوز کاری برای امروز ثبت نشده"}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
@@ -299,7 +347,9 @@ const FarmerTodoPage = () => {
|
|||||||
<Typography
|
<Typography
|
||||||
sx={{ color: "common.white", fontWeight: 700 }}
|
sx={{ color: "common.white", fontWeight: 700 }}
|
||||||
>
|
>
|
||||||
آبیاری، آفت و هماهنگی بار خروجی
|
{nextTask?.tags?.length
|
||||||
|
? nextTask.tags.join("، ")
|
||||||
|
: "آبیاری، آفت و هماهنگی بار خروجی"}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
@@ -421,6 +471,12 @@ const FarmerTodoPage = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{!farmUuid ? (
|
||||||
|
<Alert severity="warning">
|
||||||
|
برای مشاهده و ثبت کارها، ابتدا یک مزرعه فعال انتخاب کن.
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
{error ? <Alert severity="error">{error}</Alert> : null}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{segments.map((item) => (
|
{segments.map((item) => (
|
||||||
<Chip
|
<Chip
|
||||||
@@ -434,8 +490,18 @@ const FarmerTodoPage = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
{loading ? (
|
||||||
|
<Box className="flex items-center justify-center py-12">
|
||||||
|
<CircularProgress size={32} />
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
{!loading && filteredTasks.length === 0 ? (
|
||||||
|
<Alert severity="info">
|
||||||
|
هنوز کاری برای این بخش ثبت نشده است.
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
{filteredTasks.map((task) => {
|
{filteredTasks.map((task) => {
|
||||||
const meta = priorityMeta[task.priority];
|
const meta = getPriorityMeta(task.priority);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -455,7 +521,8 @@ const FarmerTodoPage = () => {
|
|||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={task.status === "done"}
|
checked={task.status === "done"}
|
||||||
onChange={() => toggleTask(task.id)}
|
disabled={togglingId === task.id}
|
||||||
|
onChange={() => void toggleTask(task)}
|
||||||
sx={{ mt: -0.5 }}
|
sx={{ mt: -0.5 }}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -518,6 +585,9 @@ const FarmerTodoPage = () => {
|
|||||||
<Typography variant="body2">{task.zone}</Typography>
|
<Typography variant="body2">{task.zone}</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Typography variant="caption" color="text.disabled">
|
||||||
|
{task.scheduledDate}
|
||||||
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{task.status === "done"
|
{task.status === "done"
|
||||||
? "انجام شده و ثبت شده"
|
? "انجام شده و ثبت شده"
|
||||||
@@ -549,21 +619,28 @@ const FarmerTodoPage = () => {
|
|||||||
onChange={(event) => setDraftTitle(event.target.value)}
|
onChange={(event) => setDraftTitle(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
select
|
|
||||||
fullWidth
|
fullWidth
|
||||||
label="محل اجرا"
|
label="محل اجرا"
|
||||||
|
placeholder="مثلا انبار مرکزی"
|
||||||
value={draftZone}
|
value={draftZone}
|
||||||
onChange={(event) => setDraftZone(event.target.value)}
|
onChange={(event) => setDraftZone(event.target.value)}
|
||||||
>
|
disabled={!farmUuid || submitting}
|
||||||
<MenuItem value="قطعه گندم - شمال مزرعه">
|
/>
|
||||||
قطعه گندم - شمال مزرعه
|
{zones.length > 0 ? (
|
||||||
</MenuItem>
|
<div className="flex flex-wrap gap-2">
|
||||||
<MenuItem value="گلخانه شماره 2">گلخانه شماره 2</MenuItem>
|
{zones.slice(0, 6).map((zone) => (
|
||||||
<MenuItem value="انبار مرکزی">انبار مرکزی</MenuItem>
|
<Chip
|
||||||
<MenuItem value="باغچه آزمایشی غربی">
|
key={zone.id}
|
||||||
باغچه آزمایشی غربی
|
clickable
|
||||||
</MenuItem>
|
label={zone.label}
|
||||||
</CustomTextField>
|
size="small"
|
||||||
|
variant={draftZone === zone.value ? "filled" : "tonal"}
|
||||||
|
color={draftZone === zone.value ? "primary" : "default"}
|
||||||
|
onClick={() => setDraftZone(zone.value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid size={6}>
|
<Grid size={6}>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
@@ -571,6 +648,7 @@ const FarmerTodoPage = () => {
|
|||||||
label="ساعت"
|
label="ساعت"
|
||||||
value={draftTime}
|
value={draftTime}
|
||||||
onChange={(event) => setDraftTime(event.target.value)}
|
onChange={(event) => setDraftTime(event.target.value)}
|
||||||
|
disabled={!farmUuid || submitting}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={6}>
|
<Grid size={6}>
|
||||||
@@ -580,8 +658,9 @@ const FarmerTodoPage = () => {
|
|||||||
label="اولویت"
|
label="اولویت"
|
||||||
value={draftPriority}
|
value={draftPriority}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setDraftPriority(event.target.value as TaskPriority)
|
setDraftPriority(event.target.value as FarmerTodoPriority)
|
||||||
}
|
}
|
||||||
|
disabled={!farmUuid || submitting}
|
||||||
>
|
>
|
||||||
<MenuItem value="زیاد">زیاد</MenuItem>
|
<MenuItem value="زیاد">زیاد</MenuItem>
|
||||||
<MenuItem value="متوسط">متوسط</MenuItem>
|
<MenuItem value="متوسط">متوسط</MenuItem>
|
||||||
@@ -593,9 +672,15 @@ const FarmerTodoPage = () => {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
size="large"
|
size="large"
|
||||||
startIcon={<i className="tabler-plus text-lg" />}
|
startIcon={<i className="tabler-plus text-lg" />}
|
||||||
onClick={addTask}
|
disabled={
|
||||||
|
!farmUuid ||
|
||||||
|
submitting ||
|
||||||
|
!draftTitle.trim() ||
|
||||||
|
!draftZone.trim()
|
||||||
|
}
|
||||||
|
onClick={() => void addTask()}
|
||||||
>
|
>
|
||||||
ثبت در لیست امروز
|
{submitting ? "در حال ثبت..." : "ثبت در لیست امروز"}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user