From 07240b03bb3068c8c4075a2ff17b2e9753614fd8 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Thu, 30 Apr 2026 04:00:19 +0330 Subject: [PATCH] UPDATE --- messages/fa.json | 3 + .../(dashboard)/(private)/economy/page.tsx | 7 + .../(private)/farmer-todos/page.tsx | 7 + .../(private)/fertilization-plan/page.tsx | 7 + src/app/(dashboard)/(private)/wallet/page.tsx | 7 + .../layout/vertical/VerticalMenu.tsx | 102 +- src/data/dictionaries/ar.json | 2 + src/data/dictionaries/en.json | 2 + src/data/dictionaries/fa.json | 2 + src/data/dictionaries/fr.json | 2 + src/data/navigation/horizontalMenuData.tsx | 20 + src/data/navigation/verticalMenuData.tsx | 10 + .../fertilizationPlanParserService.ts | 74 + .../services/irrigationPlanParserService.ts | 71 + .../farm/EconomicOverviewPageWrapper.tsx | 722 +++++++- .../FertilizationPlanParserPage.tsx | 1607 +++++++++++++++++ .../dashboards/farm/todos/FarmerTodoPage.tsx | 665 +++++++ .../dashboards/farm/wallet/FarmWalletPage.tsx | 1047 +++++++++++ 18 files changed, 4295 insertions(+), 62 deletions(-) create mode 100644 src/app/(dashboard)/(private)/economy/page.tsx create mode 100644 src/app/(dashboard)/(private)/farmer-todos/page.tsx create mode 100644 src/app/(dashboard)/(private)/fertilization-plan/page.tsx create mode 100644 src/app/(dashboard)/(private)/wallet/page.tsx create mode 100644 src/libs/api/services/fertilizationPlanParserService.ts create mode 100644 src/libs/api/services/irrigationPlanParserService.ts create mode 100644 src/views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage.tsx create mode 100644 src/views/dashboards/farm/todos/FarmerTodoPage.tsx create mode 100644 src/views/dashboards/farm/wallet/FarmWalletPage.tsx diff --git a/messages/fa.json b/messages/fa.json index 810f4e8..c72a096 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -38,6 +38,7 @@ "recommendation": "توصیه‌ها", "irrigationRecommendation": "توصیه آبیاری", "fertilizationRecommendation": "توصیه کوددهی", + "fertilizationPlanParser": "برنامه آبیاری و کودهی", "aiAssistant": "دستیار هوشمند", "farmAiAssistant": "دستیار هوشمند مزرعه", "pestDetection": "تشخیص آفات گیاهی", @@ -151,7 +152,9 @@ "yieldHarvest": "عملکرد و برداشت", "farmAlerts": "هشدارهای مزرعه", "pestDiseaseRisk": "ریسک آفات و بیماری", + "farmerTodos": "کارهای روزانه", "economicOverview": "نمای اقتصادی", + "farmWallet": "کیف پول مزرعه", "farmCalendar": "تقویم کشاورز", "sensorSection": "سنسورها", "sensor7In1": "سنسور خاک 7 در 1" diff --git a/src/app/(dashboard)/(private)/economy/page.tsx b/src/app/(dashboard)/(private)/economy/page.tsx new file mode 100644 index 0000000..86d1483 --- /dev/null +++ b/src/app/(dashboard)/(private)/economy/page.tsx @@ -0,0 +1,7 @@ +import EconomicOverviewPageWrapper from "@views/dashboards/farm/EconomicOverviewPageWrapper"; + +const EconomyPage = () => { + return ; +}; + +export default EconomyPage; diff --git a/src/app/(dashboard)/(private)/farmer-todos/page.tsx b/src/app/(dashboard)/(private)/farmer-todos/page.tsx new file mode 100644 index 0000000..1f58580 --- /dev/null +++ b/src/app/(dashboard)/(private)/farmer-todos/page.tsx @@ -0,0 +1,7 @@ +import FarmerTodoPage from "@views/dashboards/farm/todos/FarmerTodoPage"; + +const FarmerTodos = () => { + return ; +}; + +export default FarmerTodos; diff --git a/src/app/(dashboard)/(private)/fertilization-plan/page.tsx b/src/app/(dashboard)/(private)/fertilization-plan/page.tsx new file mode 100644 index 0000000..7918141 --- /dev/null +++ b/src/app/(dashboard)/(private)/fertilization-plan/page.tsx @@ -0,0 +1,7 @@ +import FertilizationPlanParserPage from "@views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage"; + +const FertilizationPlanPage = () => { + return ; +}; + +export default FertilizationPlanPage; diff --git a/src/app/(dashboard)/(private)/wallet/page.tsx b/src/app/(dashboard)/(private)/wallet/page.tsx new file mode 100644 index 0000000..e40dc7f --- /dev/null +++ b/src/app/(dashboard)/(private)/wallet/page.tsx @@ -0,0 +1,7 @@ +import FarmWalletPage from "@views/dashboards/farm/wallet/FarmWalletPage"; + +const WalletPage = () => { + return ; +}; + +export default WalletPage; diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index e39b9af..add3aec 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -1,5 +1,5 @@ // React Imports -import { useTranslations } from 'next-intl' +import { useTranslations } from "next-intl"; // MUI Imports import { useTheme } from "@mui/material/styles"; @@ -49,7 +49,7 @@ const RenderExpandIcon = ({ ); const VerticalMenu = ({ scrollMenu }: Props) => { - const t = useTranslations('navigation') + const t = useTranslations("navigation"); const theme = useTheme(); const verticalNavOptions = useVerticalNav(); @@ -64,13 +64,13 @@ const VerticalMenu = ({ scrollMenu }: Props) => { scrollMenu(container, false), - } + className: "bs-full overflow-y-auto overflow-x-hidden", + onScroll: (container) => scrollMenu(container, false), + } : { - options: { wheelPropagation: false, suppressScrollX: true }, - onScrollY: (container) => scrollMenu(container, true), - })} + options: { wheelPropagation: false, suppressScrollX: true }, + onScrollY: (container) => scrollMenu(container, true), + })} > {/* Incase you also want to scroll NavHeader to scroll with Vertical Menu, remove NavHeader from above and paste it below this comment */} {/* Vertical Menu */} @@ -92,53 +92,91 @@ const VerticalMenu = ({ scrollMenu }: Props) => { href={`/dashboard`} icon={} > - {t('dashboards')} + {t("dashboards")} - - }> - {t('yieldHarvest')} + + } + > + {t("yieldHarvest")} - }> - {t('farmAlerts')} + } + > + {t("farmAlerts")} }> - {t('pestDiseaseRisk')} + {t("pestDiseaseRisk")} - }> - {t('farmCalendar')} + } + > + {t("farmerTodos")} + + }> + {t("farmWallet")} + + } + > + {t("farmCalendar")} + + } + > + {t("economicOverview")} - + }> - {t('waterData')} + {t("waterData")} }> - {t('soilData')} + {t("soilData")} }> - {t('cropZoning')} + {t("cropZoning")} - + }> - {t('sensor7In1')} + {t("sensor7In1")} - - }> - {t('irrigationRecommendation')} + + } + > + {t("irrigationRecommendation")} - }> - {t('fertilizationRecommendation')} + } + > + {t("fertilizationRecommendation")} + + } + > + {t("fertilizationPlanParser")} - - }> - {t('farmAiAssistant')} + + } + > + {t("farmAiAssistant")} - {/* [ icon: 'tabler-bug', href: '/pest-risk' }, + { + label: 'farmerTodos', + icon: 'tabler-checklist', + href: '/farmer-todos' + }, + { + label: 'farmWallet', + icon: 'tabler-wallet', + href: '/wallet' + }, { label: 'economicOverview', icon: 'tabler-cash-banknote', @@ -126,6 +136,16 @@ const horizontalMenuData = (): HorizontalMenuDataType[] => [ icon: 'tabler-bug', href: '/pest-risk' }, + { + label: 'farmerTodos', + icon: 'tabler-checklist', + href: '/farmer-todos' + }, + { + label: 'farmWallet', + icon: 'tabler-wallet', + href: '/wallet' + }, { label: 'economicOverview', icon: 'tabler-cash-banknote', diff --git a/src/data/navigation/verticalMenuData.tsx b/src/data/navigation/verticalMenuData.tsx index 5055ab2..75663bb 100644 --- a/src/data/navigation/verticalMenuData.tsx +++ b/src/data/navigation/verticalMenuData.tsx @@ -74,6 +74,16 @@ const verticalMenuData = (): VerticalMenuDataType[] => [ icon: 'tabler-bug', href: '/pest-risk' }, + { + label: 'farmerTodos', + icon: 'tabler-checklist', + href: '/farmer-todos' + }, + { + label: 'farmWallet', + icon: 'tabler-wallet', + href: '/wallet' + }, { label: 'economicOverview', icon: 'tabler-cash-banknote', diff --git a/src/libs/api/services/fertilizationPlanParserService.ts b/src/libs/api/services/fertilizationPlanParserService.ts new file mode 100644 index 0000000..01c8b7b --- /dev/null +++ b/src/libs/api/services/fertilizationPlanParserService.ts @@ -0,0 +1,74 @@ +import { apiClient } from "../client"; + +const PREFIX = "/api/fertilization"; + +interface ApiResponse { + code: number; + msg: string; + data: T; +} + +export type FertilizationPlanParserStatus = "completed" | "needs_clarification"; + +export interface FertilizationPlanQuestion { + id: string; + field: string; + question: string; + rationale: string; +} + +export interface FertilizationPlanApplication { + fertilizer_name: string | null; + formula: string | null; + amount: string | null; + application_method: string | null; + timing: string | null; + interval_days: number | null; + purpose: string | null; +} + +export interface FertilizationPlanData { + crop_name: string | null; + growth_stage: string | null; + objective: string | null; + applications: FertilizationPlanApplication[]; + notes: string[]; +} + +export interface FertilizationPlanParserResult { + status: FertilizationPlanParserStatus; + status_fa: string; + summary: string; + missing_fields: string[]; + questions: FertilizationPlanQuestion[]; + collected_data: FertilizationPlanData; + final_plan: FertilizationPlanData | null; +} + +export type FertilizationPlanAnswerValue = string | number | boolean | null; + +export interface FertilizationPlanParserPayload { + message?: string; + answers?: Record; + partial_plan?: FertilizationPlanData; + farm_uuid?: string; +} + +async function unwrap(promise: Promise>): Promise { + const response = await promise; + + return response.data; +} + +export const fertilizationPlanParserService = { + parseFromText( + payload: FertilizationPlanParserPayload, + ): Promise { + return unwrap( + apiClient.post>( + `${PREFIX}/plan-from-text/`, + payload, + ), + ); + }, +}; diff --git a/src/libs/api/services/irrigationPlanParserService.ts b/src/libs/api/services/irrigationPlanParserService.ts new file mode 100644 index 0000000..ac9307d --- /dev/null +++ b/src/libs/api/services/irrigationPlanParserService.ts @@ -0,0 +1,71 @@ +import { apiClient } from "../client"; + +const PREFIX = "/api/irrigation"; + +interface ApiResponse { + code: number; + msg: string; + data: T; +} + +export type IrrigationPlanParserStatus = "completed" | "needs_clarification"; + +export interface IrrigationPlanQuestion { + id: string; + field: string; + question: string; + rationale: string; +} + +export interface IrrigationPlanData { + crop_name: string | null; + growth_stage: string | null; + irrigation_method: string | null; + water_amount_per_event: string | null; + duration_minutes: number | null; + frequency_text: string | null; + interval_days: number | null; + preferred_time_of_day: string | null; + start_date: string | null; + target_area: string | null; + trigger_conditions: string[]; + notes: string[]; +} + +export interface IrrigationPlanParserResult { + status: IrrigationPlanParserStatus; + status_fa: string; + summary: string; + missing_fields: string[]; + questions: IrrigationPlanQuestion[]; + collected_data: IrrigationPlanData; + final_plan: IrrigationPlanData | null; +} + +export type IrrigationPlanAnswerValue = string | number | boolean | null; + +export interface IrrigationPlanParserPayload { + message?: string; + answers?: Record; + partial_plan?: IrrigationPlanData; + farm_uuid?: string; +} + +async function unwrap(promise: Promise>): Promise { + const response = await promise; + + return response.data; +} + +export const irrigationPlanParserService = { + parseFromText( + payload: IrrigationPlanParserPayload, + ): Promise { + return unwrap( + apiClient.post>( + `${PREFIX}/plan-from-text/`, + payload, + ), + ); + }, +}; diff --git a/src/views/dashboards/farm/EconomicOverviewPageWrapper.tsx b/src/views/dashboards/farm/EconomicOverviewPageWrapper.tsx index c307922..1ead19d 100644 --- a/src/views/dashboards/farm/EconomicOverviewPageWrapper.tsx +++ b/src/views/dashboards/farm/EconomicOverviewPageWrapper.tsx @@ -1,60 +1,722 @@ -'use client' +"use client"; -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from "react"; +import { useTranslations } from "next-intl"; -import Box from '@mui/material/Box' -import CircularProgress from '@mui/material/CircularProgress' -import Grid from '@mui/material/Grid2' +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 CardHeader from "@mui/material/CardHeader"; +import Chip from "@mui/material/Chip"; +import CircularProgress from "@mui/material/CircularProgress"; +import Grid from "@mui/material/Grid2"; +import LinearProgress from "@mui/material/LinearProgress"; +import Typography from "@mui/material/Typography"; +import { alpha, useTheme } from "@mui/material/styles"; -import { useFarmHub } from '@/hooks/useFarmHub' -import { economicOverviewService } from '@/libs/api/services/economicOverviewService' -import EconomicOverview from '@views/dashboards/farm/EconomicOverview' +import type { ThemeColor } from "@core/types"; + +import { useFarmHub } from "@/hooks/useFarmHub"; +import { economicOverviewService } from "@/libs/api/services/economicOverviewService"; +import OptionMenu from "@core/components/option-menu"; +import CustomAvatar from "@core/components/mui/Avatar"; +import HorizontalWithAvatar from "@components/card-statistics/HorizontalWithAvatar"; +import Link from "@components/Link"; +import EconomicOverview from "@views/dashboards/farm/EconomicOverview"; const cardRowSx = { - display: 'flex', - flexDirection: 'column', + display: "flex", + flexDirection: "column", minHeight: 380, - '& > *': { flex: 1, minHeight: 0 } -} + "& > *": { flex: 1, minHeight: 0 }, +}; + +type EconomicItem = { + title: string; + value: string; + subtitle: string; + avatarIcon: string; + avatarColor: ThemeColor; +}; + +type ChartSeriesItem = { + name: string; + data: number[]; +}; + +type FertilizerProductItem = { + fertilizerCode: string; + fertilizerName: string; + productName: string; + storeName: string; + price: string; + packageSize: string; + storeUrl: string; + color: ThemeColor; +}; + +const fertilizerProductsMock: FertilizerProductItem[] = [ + { + fertilizerCode: "NPK-20-20-20", + fertilizerName: "کود کامل 20-20-20", + productName: "NPK یونیورسال گرین پلاس", + storeName: "فروشگاه سبزینه", + price: "۱,۹۸۰,۰۰۰ تومان", + packageSize: "کیسه 25 کیلوگرم", + storeUrl: "/apps/ecommerce/products/list", + color: "primary", + }, + { + fertilizerCode: "UREA-46", + fertilizerName: "اوره 46 درصد", + productName: "اوره دانه ای کشاورزی مهر", + storeName: "مارکت نهاده یار", + price: "۱,۴۲۰,۰۰۰ تومان", + packageSize: "کیسه 50 کیلوگرم", + storeUrl: "/apps/ecommerce/products/list", + color: "success", + }, + { + fertilizerCode: "MAP-12-61", + fertilizerName: "مونوآمونیوم فسفات", + productName: "MAP خالص برای شروع رشد", + storeName: "فروشگاه آگرومال", + price: "۲,۳۵۰,۰۰۰ تومان", + packageSize: "کیسه 25 کیلوگرم", + storeUrl: "/apps/ecommerce/products/list", + color: "warning", + }, +]; const EconomicOverviewPageWrapper = () => { - const { farmHub } = useFarmHub() - const farmUuid = farmHub?.farm_uuid - const [data, setData] = useState>({}) - const [loading, setLoading] = useState(true) + const t = useTranslations("farmDashboard"); + const theme = useTheme(); + const { farmHub } = useFarmHub(); + const farmUuid = farmHub?.farm_uuid; + const [data, setData] = useState>({}); + const [loading, setLoading] = useState(true); + + const economicData = (data?.economicData as EconomicItem[] | undefined) ?? []; + const chartSeries = + (data?.chartSeries as ChartSeriesItem[] | undefined) ?? []; + + const headlineCards = useMemo( + () => + economicData.slice(0, 3).map((item) => ({ + stats: item.value, + title: item.title, + avatarIcon: item.avatarIcon, + avatarColor: item.avatarColor, + })), + [economicData], + ); + + const portfolioMix = useMemo(() => { + const fallbackColors: ThemeColor[] = [ + "primary", + "info", + "success", + "warning", + ]; + + return chartSeries.map((series, index) => { + const total = series.data.reduce((sum, current) => sum + current, 0); + const max = chartSeries.reduce( + (best, item) => + Math.max( + best, + item.data.reduce((sum, current) => sum + current, 0), + ), + 0, + ); + + return { + name: series.name, + total, + share: max > 0 ? Math.round((total / max) * 100) : 0, + color: fallbackColors[index % fallbackColors.length], + }; + }); + }, [chartSeries]); + + const completionValue = useMemo(() => { + const completed = economicData.filter( + (item) => item.value && item.value !== "-", + ).length; + + return economicData.length > 0 + ? Math.round((completed / economicData.length) * 100) + : 72; + }, [economicData]); useEffect(() => { if (!farmUuid) { - setData({}) - setLoading(false) - return + setData({}); + setLoading(false); + return; } - setLoading(true) + setLoading(true); economicOverviewService .getSummary(farmUuid) - .then(summary => setData((summary.economicOverview as Record) ?? {})) + .then((summary) => + setData((summary.economicOverview as Record) ?? {}), + ) .catch(() => setData({})) - .finally(() => setLoading(false)) - }, [farmUuid]) + .finally(() => setLoading(false)); + }, [farmUuid]); if (loading) { return ( - + - ) + ); } return ( - + - + + + + + + + +
+ +
+ + صفحه اقتصاد مزرعه با همان کامپوننت های مالی موجود + + + خلاصه عملکرد اقتصادی، روند درآمد و هزینه، و چند نمای + سریع برای تصمیم گیری روزانه را در یک صفحه یکپارچه ببین. + +
+
+ + + وضعیت تصمیم گیری + + + {headlineCards[0]?.stats ?? + "در حال بارگذاری داده مالی"} + + + + + تمرکز این صفحه + + + هزینه، بازگشت سرمایه و سهم درآمد + + +
+
+
+ + +
+
+ + آمادگی داده های اقتصادی + + + {completionValue}% تکمیل + +
+ + + +
+ +
+ {economicData.slice(0, 2).map((item) => ( +
+
+ + + +
+ + {item.title} + + + {item.subtitle} + +
+
+ + {item.value} + +
+ ))} +
+
+
+
+
+
+
+ + {headlineCards.map((item) => ( + + + + ))} + + + + + + + } + /> + + {portfolioMix.length > 0 ? ( + portfolioMix.map((item) => ( + +
+
+ + + + + {item.name} + +
+ + {item.total.toLocaleString("fa-IR")} + +
+ +
+ )) + ) : ( + + هنوز داده سری های اقتصادی برای نمایش سهم درآمد و هزینه در + دسترس نیست. + + )} +
+
+
+ + + + + + {economicData.length > 0 ? ( + economicData.map((item) => ( + +
+
+ + + +
+ + {item.title} + + + {item.subtitle} + +
+
+ + {item.value} + +
+
+ )) + ) : ( + + هنوز شاخص مالی برای این مزرعه دریافت نشده است. + + )} +
+
+
+ + + + + } + /> + + {fertilizerProductsMock.map((item) => ( + +
+
+ + + +
+
+ + {item.productName} + + +
+ + {item.fertilizerName} + +
+ + } + label={item.storeName} + size="small" + variant="outlined" + /> + } + label={item.packageSize} + size="small" + variant="outlined" + /> +
+
+
+ + + + قیمت امروز + + + {item.price} + + + +
+
+ ))} +
+
+
+ + + + + + +
+ + + +
+ + جمع بندی سریع + + + این صفحه با همان کامپوننت اقتصادی موجود ساخته شده و نمای + خلاصه، کارت های سریع و وضعیت ترکیب جریان مالی را در کنار + چارت اصلی نشان می دهد. + +
+
+
+ + + پیشنهاد استفاده + + + اگر خواستی مرحله بعدی می شود این صفحه را به داده های ریزتر مثل + هزینه آبیاری، حمل، کوددهی و درآمد محصول به تفکیک فصل وصل کرد. + + +
+
+
- ) -} + ); +}; -export default EconomicOverviewPageWrapper +export default EconomicOverviewPageWrapper; diff --git a/src/views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage.tsx b/src/views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage.tsx new file mode 100644 index 0000000..9f00bcf --- /dev/null +++ b/src/views/dashboards/farm/fertilizationPlanParser/FertilizationPlanParserPage.tsx @@ -0,0 +1,1607 @@ +"use client"; + +import { useMemo, useState } from "react"; + +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 Grid from "@mui/material/Grid2"; +import LinearProgress from "@mui/material/LinearProgress"; +import Stack from "@mui/material/Stack"; +import Step from "@mui/material/Step"; +import StepLabel from "@mui/material/StepLabel"; +import Stepper from "@mui/material/Stepper"; +import Tab from "@mui/material/Tab"; +import Tabs from "@mui/material/Tabs"; +import Typography from "@mui/material/Typography"; +import { alpha, useTheme } from "@mui/material/styles"; + +import type { ThemeColor } from "@core/types"; + +import HorizontalWithAvatar from "@/components/card-statistics/HorizontalWithAvatar"; +import { useFarmHub } from "@/hooks/useFarmHub"; +import { + fertilizationPlanParserService, + type FertilizationPlanAnswerValue, + type FertilizationPlanApplication, + type FertilizationPlanData, + type FertilizationPlanParserResult, +} from "@/libs/api/services/fertilizationPlanParserService"; +import { + irrigationPlanParserService, + type IrrigationPlanAnswerValue, + type IrrigationPlanData, + type IrrigationPlanParserResult, +} from "@/libs/api/services/irrigationPlanParserService"; +import OptionMenu from "@core/components/option-menu"; +import CustomAvatar from "@core/components/mui/Avatar"; +import CustomTextField from "@core/components/mui/TextField"; + +type ParserTabKey = "fertilization" | "irrigation"; +type ParserAnswerValue = + | FertilizationPlanAnswerValue + | IrrigationPlanAnswerValue; + +type ParserQuestion = { + id: string; + field: string; + question: string; + rationale: string; +}; + +type ParserResponse = + | FertilizationPlanParserResult + | IrrigationPlanParserResult; + +type TabState = { + message: string; + response: ParserResponse | null; + answers: Record; + activeQuestion: number; + requestError: string | null; + statusNote: string | null; + loading: boolean; +}; + +type FieldMeta = { + key: string; + label: string; + value: string | null; +}; + +type ParserConfig = { + key: ParserTabKey; + label: string; + badge: string; + icon: string; + heroTitle: string; + heroDescription: string; + panelTitle: string; + panelDescription: string; + inputLabel: string; + inputPlaceholder: string; + samplePrompts: string[]; + fieldLabels: Record; + previewOrder: string[]; + itemCountTitle: string; + primaryCardTitle: string; + primaryFallbackTitle: string; + finalCardTitle: string; + finalCardDescription: string; + buildPayload: (payload: { + message?: string; + answers?: Record; + partialPlan?: unknown; + farmUuid?: string; + }) => Record; + submit: (payload: Record) => Promise; + getFieldValue: (plan: unknown, field: string) => ParserAnswerValue; + formatFieldValue: (field: string, value: ParserAnswerValue) => string; + getPrimaryItem: (plan: unknown) => unknown | null; + getPrimaryMeta: (item: unknown) => FieldMeta[]; + getPrimaryHeadline: (item: unknown) => string; + getItemCount: (plan: unknown) => number; +}; + +const createInitialTabState = (): TabState => ({ + message: "", + response: null, + answers: {}, + activeQuestion: 0, + requestError: null, + statusNote: null, + loading: false, +}); + +const formatNumber = (value: number) => + new Intl.NumberFormat("fa-IR").format(value); + +const defaultFormatFieldValue = (value: ParserAnswerValue) => { + if (value === null || value === undefined || value === "") { + return "—"; + } + + if (typeof value === "number") { + return formatNumber(value); + } + + if (typeof value === "boolean") { + return value ? "بله" : "خیر"; + } + + return value; +}; + +const formatNumberWithUnit = ( + value: number | null | undefined, + unit: string, +) => { + if (value === null || value === undefined) { + return null; + } + + return `${formatNumber(value)} ${unit}`; +}; + +const getStatusMeta = (status?: ParserResponse["status"]) => { + if (status === "completed") { + return { + label: "آماده اجرا", + color: "success" as const, + icon: "tabler-rosette-discount-check", + }; + } + + if (status === "needs_clarification") { + return { + label: "نیازمند تکمیل", + color: "warning" as const, + icon: "tabler-help-hexagon", + }; + } + + return { + label: "در انتظار تحلیل", + color: "secondary" as const, + icon: "tabler-sparkles", + }; +}; + +const extractErrorMessage = (error: unknown, fallback: string) => { + if (typeof error !== "object" || error === null) { + return fallback; + } + + if ( + "details" in error && + error.details && + typeof error.details === "object" && + "data" in error.details && + error.details.data && + typeof error.details.data === "object" + ) { + const data = error.details.data as Record; + const nonFieldErrors = data.non_field_errors; + + if ( + Array.isArray(nonFieldErrors) && + typeof nonFieldErrors[0] === "string" + ) { + return nonFieldErrors[0]; + } + } + + if ("message" in error && typeof error.message === "string") { + return error.message; + } + + return fallback; +}; + +const createJsonSnapshot = (plan: unknown) => { + if (!plan) { + return "{}"; + } + + return JSON.stringify(plan, null, 2); +}; + +const getFertilizationFieldValue = (plan: unknown, field: string) => { + const data = plan as FertilizationPlanData | null | undefined; + + if (!data) return null; + if (field === "crop_name") return data.crop_name; + if (field === "growth_stage") return data.growth_stage; + if (field === "objective") return data.objective; + + const application = data.applications?.[0]; + + if (!application) return null; + if (field === "fertilizer_name") return application.fertilizer_name; + if (field === "formula") return application.formula; + if (field === "amount") return application.amount; + if (field === "application_method") return application.application_method; + if (field === "timing") return application.timing; + if (field === "interval_days") return application.interval_days; + if (field === "purpose") return application.purpose; + + return null; +}; + +const getIrrigationFieldValue = (plan: unknown, field: string) => { + const data = plan as IrrigationPlanData | null | undefined; + + if (!data) return null; + if (field === "crop_name") return data.crop_name; + if (field === "growth_stage") return data.growth_stage; + if (field === "irrigation_method") return data.irrigation_method; + if (field === "water_amount_per_event") return data.water_amount_per_event; + if (field === "duration_minutes") return data.duration_minutes; + if (field === "frequency_text") return data.frequency_text; + if (field === "interval_days") return data.interval_days; + if (field === "preferred_time_of_day") return data.preferred_time_of_day; + if (field === "start_date") return data.start_date; + if (field === "target_area") return data.target_area; + + return null; +}; + +const buildNextAnswersState = ( + response: ParserResponse, + previousAnswers: Record, + config: ParserConfig, +) => { + return response.questions.reduce>((acc, question) => { + const previousValue = previousAnswers[question.field]; + + if (previousValue) { + acc[question.field] = previousValue; + return acc; + } + + const extractedValue = config.getFieldValue( + response.collected_data, + question.field, + ); + + if (extractedValue !== null && extractedValue !== undefined) { + acc[question.field] = String(extractedValue); + } + + return acc; + }, {}); +}; + +const normalizeAnswers = ( + questions: ParserQuestion[], + answers: Record, +) => { + return questions.reduce>( + (acc, question) => { + const rawValue = answers[question.field]?.trim(); + + if (!rawValue) { + return acc; + } + + if ( + question.field === "interval_days" || + question.field === "duration_minutes" + ) { + const parsed = Number(rawValue); + acc[question.field] = Number.isFinite(parsed) ? parsed : rawValue; + return acc; + } + + acc[question.field] = rawValue; + return acc; + }, + {}, + ); +}; + +const FERTILIZATION_FIELD_LABELS: Record = { + crop_name: "محصول", + growth_stage: "مرحله رشد", + objective: "هدف برنامه", + fertilizer_name: "نام کود", + formula: "فرمول کود", + amount: "مقدار مصرف", + application_method: "روش مصرف", + timing: "زمان بندی", + interval_days: "فاصله نوبت ها", + purpose: "هدف هر نوبت", +}; + +const IRRIGATION_FIELD_LABELS: Record = { + crop_name: "محصول", + growth_stage: "مرحله رشد", + irrigation_method: "روش آبیاری", + water_amount_per_event: "مقدار آب هر نوبت", + duration_minutes: "مدت هر نوبت", + frequency_text: "تناوب آبیاری", + interval_days: "فاصله آبیاری", + preferred_time_of_day: "زمان مناسب اجرا", + start_date: "زمان شروع", + target_area: "محدوده اجرا", +}; + +const PARSER_CONFIGS: Record = { + fertilization: { + key: "fertilization", + label: "کودهی", + badge: "Fertilization AI Planner", + icon: "tabler-flask-2", + heroTitle: "برنامه کودهی با ورودی متنی و خروجی JSON آماده اجرا", + heroDescription: + "متن آزاد کشاورز را بگیر، اگر داده ناقص بود سوال تکمیلی بپرس و در نهایت یک نسخه ساختاریافته و خوش خوان از برنامه کودهی تحویل بده.", + panelTitle: "اتاق فرمان برنامه کودهی", + panelDescription: + "هر چقدر متن طبیعی تر و نزدیک به زبان خود کشاورز باشد، API بهتر می تواند ساختار نهایی را استخراج کند.", + inputLabel: "متن آزاد برنامه کودهی", + inputPlaceholder: + "مثلا: برای گندم در مرحله پنجه زنی هر 12 روز یک بار کود 20-20-20 به مقدار 35 کیلوگرم در هکتار از طریق کودآبیاری می دهم.", + samplePrompts: [ + "برای گندم در مرحله پنجه زنی هر 12 روز یک بار کود 20-20-20 به مقدار 35 کیلوگرم در هکتار از طریق کودآبیاری می دهم.", + "برای ذرت از کود کامل استفاده می کنم اما فاصله بین نوبت ها را دقیق نمی دانم و می خواهم برنامه ام کامل شود.", + "برای گوجه فرنگی در شروع گلدهی یک برنامه کودهی دقیق با کود NPK و مصرف مرحله ای می خواهم.", + ], + fieldLabels: FERTILIZATION_FIELD_LABELS, + previewOrder: [ + "crop_name", + "growth_stage", + "objective", + "fertilizer_name", + "formula", + "amount", + "application_method", + "timing", + "interval_days", + "purpose", + ], + itemCountTitle: "نوبت های کودی", + primaryCardTitle: "نسخه پیشنهادی برای اولین نوبت کودی", + primaryFallbackTitle: "نام کود هنوز مشخص نیست", + finalCardTitle: "خروجی نهایی کودهی آماده تحویل", + finalCardDescription: + "اگر لازم بود همین JSON را برای ذخیره، اشتراک گذاری یا مرحله بعدی workflow استفاده کن.", + buildPayload: ({ message, answers, partialPlan, farmUuid }) => ({ + ...(message ? { message } : {}), + ...(answers ? { answers } : {}), + ...(partialPlan ? { partial_plan: partialPlan } : {}), + ...(farmUuid ? { farm_uuid: farmUuid } : {}), + }), + submit: (payload) => + fertilizationPlanParserService.parseFromText( + payload as Parameters< + typeof fertilizationPlanParserService.parseFromText + >[0], + ), + getFieldValue: getFertilizationFieldValue, + formatFieldValue: (field, value) => { + if (field === "interval_days" && typeof value === "number") { + return `${formatNumber(value)} روز`; + } + + return defaultFormatFieldValue(value); + }, + getPrimaryItem: (plan) => + (plan as FertilizationPlanData | null | undefined)?.applications?.[0] ?? + null, + getPrimaryMeta: (item) => { + const application = item as FertilizationPlanApplication | null; + + if (!application) return []; + + return [ + { + key: "formula", + label: FERTILIZATION_FIELD_LABELS.formula, + value: application.formula, + }, + { + key: "amount", + label: FERTILIZATION_FIELD_LABELS.amount, + value: application.amount, + }, + { + key: "application_method", + label: FERTILIZATION_FIELD_LABELS.application_method, + value: application.application_method, + }, + { + key: "timing", + label: FERTILIZATION_FIELD_LABELS.timing, + value: application.timing, + }, + { + key: "interval_days", + label: FERTILIZATION_FIELD_LABELS.interval_days, + value: formatNumberWithUnit(application.interval_days, "روز"), + }, + { + key: "purpose", + label: FERTILIZATION_FIELD_LABELS.purpose, + value: application.purpose, + }, + ]; + }, + getPrimaryHeadline: (item) => + (item as FertilizationPlanApplication | null)?.fertilizer_name || + "نام کود هنوز مشخص نیست", + getItemCount: (plan) => + (plan as FertilizationPlanData | null | undefined)?.applications + ?.length ?? 0, + }, + irrigation: { + key: "irrigation", + label: "آبیاری", + badge: "Irrigation AI Planner", + icon: "tabler-droplet-half-2", + heroTitle: "برنامه آبیاری با ورودی متنی و خروجی JSON آماده اجرا", + heroDescription: + "برای آبیاری هم همان flow هوشمند را داشته باش: متن آزاد را بفرست، ابهام ها را کامل کن و یک برنامه ساختاریافته و قابل اجرا بگیر.", + panelTitle: "اتاق فرمان برنامه آبیاری", + panelDescription: + "جزئیات روش آبیاری، زمان بندی و مقدار آب را با زبان طبیعی بنویس تا API آن را به برنامه دقیق تبدیل کند.", + inputLabel: "متن آزاد برنامه آبیاری", + inputPlaceholder: + "مثلا: برای گوجه فرنگی با آبیاری قطره ای هر سه روز یک بار صبح زود 25 دقیقه آبیاری می کنم و حدود 18 لیتر برای هر بوته می دهم.", + samplePrompts: [ + "برای گوجه فرنگی با آبیاری قطره ای هر سه روز یک بار صبح زود 25 دقیقه آبیاری می کنم و حدود 18 لیتر برای هر بوته می دهم.", + "برای هندوانه هر دو روز یک بار آبیاری می کنم اما زمان شروع برنامه و محدوده اجرا را هنوز مشخص نکرده ام.", + "برای خیار گلخانه ای با تیپ آبیاری می کنم و می خواهم برنامه ام بر اساس زمان و حجم آب کامل شود.", + ], + fieldLabels: IRRIGATION_FIELD_LABELS, + previewOrder: [ + "crop_name", + "growth_stage", + "irrigation_method", + "water_amount_per_event", + "duration_minutes", + "frequency_text", + "interval_days", + "preferred_time_of_day", + "start_date", + "target_area", + ], + itemCountTitle: "بخش های آبیاری", + primaryCardTitle: "چکیده اجرای برنامه آبیاری", + primaryFallbackTitle: "روش آبیاری هنوز مشخص نیست", + finalCardTitle: "خروجی نهایی آبیاری آماده تحویل", + finalCardDescription: + "از همین JSON می توانی برای نمایش، ذخیره یا ادامه workflow آبیاری استفاده کنی.", + buildPayload: ({ message, answers, partialPlan, farmUuid }) => ({ + ...(message ? { message } : {}), + ...(answers ? { answers } : {}), + ...(partialPlan ? { partial_plan: partialPlan } : {}), + ...(farmUuid ? { farm_uuid: farmUuid } : {}), + }), + submit: (payload) => + irrigationPlanParserService.parseFromText( + payload as Parameters< + typeof irrigationPlanParserService.parseFromText + >[0], + ), + getFieldValue: getIrrigationFieldValue, + formatFieldValue: (field, value) => { + if (field === "duration_minutes" && typeof value === "number") { + return `${formatNumber(value)} دقیقه`; + } + + if (field === "interval_days" && typeof value === "number") { + return `${formatNumber(value)} روز`; + } + + return defaultFormatFieldValue(value); + }, + getPrimaryItem: (plan) => plan ?? null, + getPrimaryMeta: (item) => { + const data = item as IrrigationPlanData | null; + + if (!data) return []; + + return [ + { + key: "irrigation_method", + label: IRRIGATION_FIELD_LABELS.irrigation_method, + value: data.irrigation_method, + }, + { + key: "water_amount_per_event", + label: IRRIGATION_FIELD_LABELS.water_amount_per_event, + value: data.water_amount_per_event, + }, + { + key: "duration_minutes", + label: IRRIGATION_FIELD_LABELS.duration_minutes, + value: formatNumberWithUnit(data.duration_minutes, "دقیقه"), + }, + { + key: "frequency_text", + label: IRRIGATION_FIELD_LABELS.frequency_text, + value: data.frequency_text, + }, + { + key: "preferred_time_of_day", + label: IRRIGATION_FIELD_LABELS.preferred_time_of_day, + value: data.preferred_time_of_day, + }, + { + key: "target_area", + label: IRRIGATION_FIELD_LABELS.target_area, + value: data.target_area, + }, + ]; + }, + getPrimaryHeadline: (item) => + (item as IrrigationPlanData | null)?.irrigation_method || + "روش آبیاری هنوز مشخص نیست", + getItemCount: (plan) => { + const data = plan as IrrigationPlanData | null | undefined; + + if (!data) return 0; + + return 1 + (data.trigger_conditions?.length ?? 0); + }, + }, +}; + +const FertilizationPlanParserPage = () => { + const theme = useTheme(); + const { farmHub } = useFarmHub(); + const farmUuid = farmHub?.farm_uuid; + + const [activeTab, setActiveTab] = useState("fertilization"); + const [tabStates, setTabStates] = useState>({ + fertilization: createInitialTabState(), + irrigation: createInitialTabState(), + }); + + const config = PARSER_CONFIGS[activeTab]; + const currentState = tabStates[activeTab]; + const statusMeta = getStatusMeta(currentState.response?.status); + const planPreview = + currentState.response?.final_plan ?? + currentState.response?.collected_data ?? + null; + const primaryItem = config.getPrimaryItem(planPreview); + const currentQuestion = + currentState.response?.questions[currentState.activeQuestion] ?? null; + + const completionValue = currentState.response + ? Math.max( + 20, + Math.round( + ((config.previewOrder.length - + currentState.response.missing_fields.length) / + config.previewOrder.length) * + 100, + ), + ) + : 18; + + const statCards = useMemo( + () => [ + { + stats: currentState.response?.status_fa ?? "شروع نشده", + title: "وضعیت پردازش", + avatarIcon: statusMeta.icon, + avatarColor: statusMeta.color as ThemeColor, + }, + { + stats: formatNumber(currentState.response?.missing_fields.length ?? 0), + title: "فیلدهای ناقص", + avatarIcon: "tabler-help-circle", + avatarColor: "warning" as ThemeColor, + }, + { + stats: formatNumber(config.getItemCount(planPreview)), + title: config.itemCountTitle, + avatarIcon: config.icon, + avatarColor: "success" as ThemeColor, + }, + ], + [ + config, + currentState.response?.missing_fields.length, + currentState.response?.status_fa, + planPreview, + statusMeta.color, + statusMeta.icon, + ], + ); + + const updateTabState = ( + tab: ParserTabKey, + updater: Partial | ((previous: TabState) => TabState), + ) => { + setTabStates((previous) => ({ + ...previous, + [tab]: + typeof updater === "function" + ? updater(previous[tab]) + : { ...previous[tab], ...updater }, + })); + }; + + const handleReset = (tab: ParserTabKey) => { + updateTabState(tab, createInitialTabState()); + }; + + const submitRequest = async ( + tab: ParserTabKey, + payload: Record, + ) => { + const tabConfig = PARSER_CONFIGS[tab]; + + updateTabState(tab, (previous) => ({ + ...previous, + loading: true, + requestError: null, + statusNote: null, + })); + + try { + const nextResponse = await tabConfig.submit(payload); + + updateTabState(tab, (previous) => ({ + ...previous, + response: nextResponse, + answers: buildNextAnswersState( + nextResponse, + previous.answers, + tabConfig, + ), + activeQuestion: 0, + statusNote: + nextResponse.status === "completed" + ? `برنامه ${tabConfig.label} نهایی آماده شد و می توانی آن را با تیم مزرعه به اشتراک بگذاری.` + : "سیستم چند ابهام پیدا کرده؛ جواب ها را کامل کن تا JSON نهایی ساخته شود.", + })); + } catch (error) { + updateTabState(tab, (previous) => ({ + ...previous, + requestError: extractErrorMessage( + error, + `در ساخت برنامه ${tabConfig.label} مشکلی پیش آمد. دوباره تلاش کن.`, + ), + })); + } finally { + updateTabState(tab, (previous) => ({ + ...previous, + loading: false, + })); + } + }; + + const handleGeneratePlan = async () => { + const trimmedMessage = currentState.message.trim(); + + if (!trimmedMessage) { + updateTabState(activeTab, (previous) => ({ + ...previous, + requestError: `اول متن برنامه ${config.label} را بنویس تا تحلیل را شروع کنیم.`, + })); + return; + } + + await submitRequest( + activeTab, + config.buildPayload({ + message: trimmedMessage, + farmUuid, + }), + ); + }; + + const handleSubmitAnswers = async () => { + if (!currentState.response) { + return; + } + + const normalizedAnswers = normalizeAnswers( + currentState.response.questions, + currentState.answers, + ); + + const unansweredQuestion = currentState.response.questions.find( + (question) => { + const value = normalizedAnswers[question.field]; + + return value === undefined || value === null || value === ""; + }, + ); + + if (unansweredQuestion) { + updateTabState(activeTab, (previous) => ({ + ...previous, + requestError: `پاسخ سوال «${config.fieldLabels[unansweredQuestion.field] ?? unansweredQuestion.question}» هنوز کامل نشده است.`, + })); + return; + } + + await submitRequest( + activeTab, + config.buildPayload({ + answers: normalizedAnswers, + partialPlan: currentState.response.collected_data, + farmUuid, + }), + ); + }; + + const handleCopyJson = async () => { + if ( + !planPreview || + typeof navigator === "undefined" || + !navigator.clipboard + ) { + updateTabState(activeTab, (previous) => ({ + ...previous, + statusNote: "کپی خودکار روی این مرورگر در دسترس نیست.", + })); + return; + } + + await navigator.clipboard.writeText(createJsonSnapshot(planPreview)); + + updateTabState(activeTab, (previous) => ({ + ...previous, + statusNote: `نسخه JSON برنامه ${config.label} در کلیپ بورد کپی شد.`, + })); + }; + + return ( + + + + + + + + + + + + + + {config.heroTitle} + + + {config.heroDescription} + + + + + + موتور تحلیل + + + Free-text parser + clarification flow + + + + + مزرعه فعال + + + {farmHub?.name || farmUuid || "بدون مزرعه فعال"} + + + + + + + + + + + + دو جریان هوشمند، یک صفحه واحد + + + بین تب آبیاری و تب کودهی جابه جا شو و هر کدام را با + همان flow سوال های تکمیلی تا JSON نهایی پیش ببر. + + + + + متن آزاد + + + تکمیل ابهام ها + + + برنامه نهایی + + + + + + + + + + + + + + + + + setActiveTab(value)} + variant="fullWidth" + sx={{ + p: 1, + borderRadius: 4, + backgroundColor: alpha(theme.palette.primary.main, 0.05), + minHeight: 64, + "& .MuiTabs-indicator": { display: "none" }, + }} + > + {Object.values(PARSER_CONFIGS).map((tabConfig) => ( + } + iconPosition="start" + label={tabConfig.label} + sx={{ + minHeight: 54, + borderRadius: 3, + fontWeight: 700, + transition: "all 0.2s ease", + "&.Mui-selected": { + color: theme.palette.common.white, + background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.info.main} 100%)`, + boxShadow: `0 14px 32px ${alpha(theme.palette.primary.main, 0.24)}`, + }, + }} + /> + ))} + + + + + + + {config.panelTitle} + + + {config.panelDescription} + + + + updateTabState(activeTab, (previous) => ({ + ...previous, + message: config.samplePrompts[0], + })), + }, + }, + { + text: "کپی JSON", + icon: "tabler-copy", + menuItemProps: { + onClick: handleCopyJson, + }, + }, + { + text: "شروع دوباره", + icon: "tabler-rotate-clockwise-2", + menuItemProps: { + onClick: () => handleReset(activeTab), + }, + }, + ]} + /> + + + {!farmUuid && ( + + مزرعه فعالی پیدا نشد. صفحه هنوز کار می کند، اما اگر + `farm_uuid` فعال باشد پاسخ API دقیق تر می شود. + + )} + + + updateTabState(activeTab, (previous) => ({ + ...previous, + message: event.target.value, + })) + } + /> + + + + نمونه های آماده برای شروع سریع + + + {config.samplePrompts.map((prompt) => ( + + updateTabState(activeTab, (previous) => ({ + ...previous, + message: prompt, + })) + } + variant="outlined" + sx={{ + justifyContent: "flex-start", + px: 1, + py: 2.75, + maxWidth: "100%", + height: "auto", + borderRadius: 3, + backgroundColor: alpha( + theme.palette.success.main, + 0.05, + ), + "& .MuiChip-label": { + display: "block", + whiteSpace: "normal", + }, + }} + /> + ))} + + + + + + + + + {currentState.loading && ( + + )} + + {currentState.requestError && ( + {currentState.requestError} + )} + {currentState.statusNote && ( + {currentState.statusNote} + )} + + {currentState.response && ( + + + + + + + + + + + {currentState.response.status_fa} + + + {currentState.response.summary} + + + + + + + {currentState.response.status === + "needs_clarification" && + currentState.response.questions.length > 0 && + currentQuestion && ( + + + + + مرحله تکمیل ابهام ها + + + سوال ها را یکی یکی جلو ببر؛ بعد از تکمیل، همان + endpoint با `answers` و `partial_plan` دوباره + صدا زده می شود. + + + + {currentState.response.questions.map( + (question) => ( + + + {config.fieldLabels[question.field] ?? + question.field} + + + ), + )} + + + + + + + + + + + سوال{" "} + {formatNumber( + currentState.activeQuestion + 1, + )}{" "} + از{" "} + {formatNumber( + currentState.response.questions + .length, + )} + + + {config.fieldLabels[ + currentQuestion.field + ] ?? currentQuestion.field} + + + + + + {currentQuestion.question} + + + {currentQuestion.rationale} + + + + updateTabState( + activeTab, + (previous) => ({ + ...previous, + answers: { + ...previous.answers, + [currentQuestion.field]: + event.target.value, + }, + }), + ) + } + placeholder="پاسخ را اینجا بنویس..." + /> + + + + + + + + + + + + )} + + + + )} + + + + + + + + + {statCards.map((card) => ( + + + + ))} + + + + + + + پیش نمایش زنده داده ساختاریافته + + + این بخش نشان می دهد سیستم تا این لحظه چه چیزهایی را فهمیده + است. + + + + + + + درصد تکمیل برنامه + + + {formatNumber(completionValue)}٪ + + + + + + {planPreview ? ( + + {config.previewOrder.map((field) => { + const value = config.getFieldValue(planPreview, field); + + return ( + + + {config.fieldLabels[field] ?? field} + + + {config.formatFieldValue(field, value)} + + + ); + })} + + ) : ( + + هنوز خروجی ای نداریم. متن را ارسال کن تا collected_data و + سپس final_plan اینجا شکل بگیرد. + + )} + + + + + {primaryItem !== null && primaryItem !== undefined && ( + + + + + + + {config.primaryCardTitle} + + + {config.getPrimaryHeadline(primaryItem) || + config.primaryFallbackTitle} + + + + + + + {config.getPrimaryMeta(primaryItem).map((item) => ( + + + + {item.label} + + + {item.value || "—"} + + + + ))} + + + + + )} + + {currentState.response?.final_plan && ( + + + + + + + {config.finalCardTitle} + + + {config.finalCardDescription} + + + + + + {createJsonSnapshot(currentState.response.final_plan)} + + + + + )} + + + + + ); +}; + +export default FertilizationPlanParserPage; diff --git a/src/views/dashboards/farm/todos/FarmerTodoPage.tsx b/src/views/dashboards/farm/todos/FarmerTodoPage.tsx new file mode 100644 index 0000000..cc4b4cc --- /dev/null +++ b/src/views/dashboards/farm/todos/FarmerTodoPage.tsx @@ -0,0 +1,665 @@ +"use client"; + +import { useMemo, useState } 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 CardHeader from "@mui/material/CardHeader"; +import Checkbox from "@mui/material/Checkbox"; +import Chip from "@mui/material/Chip"; +import CircularProgress from "@mui/material/CircularProgress"; +import Divider from "@mui/material/Divider"; +import Grid from "@mui/material/Grid2"; +import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; +import { alpha, useTheme } from "@mui/material/styles"; + +import classnames from "classnames"; + +import type { ThemeColor } from "@core/types"; + +import OptionMenu from "@core/components/option-menu"; +import CustomAvatar from "@core/components/mui/Avatar"; +import CustomTextField from "@core/components/mui/TextField"; +import HorizontalWithAvatar from "@components/card-statistics/HorizontalWithAvatar"; + +type TaskPriority = "زیاد" | "متوسط" | "کم"; +type TaskStatus = "open" | "done"; +type TaskSegment = "همه" | "امروز" | "فوری" | "انجام شده"; + +type FarmerTask = { + id: number; + title: 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 = + { + زیاد: { color: "error", icon: "tabler-alert-triangle" }, + متوسط: { color: "warning", icon: "tabler-sun-high" }, + کم: { color: "success", icon: "tabler-leaf" }, + }; + +const segments: TaskSegment[] = ["همه", "امروز", "فوری", "انجام شده"]; + +const FarmerTodoPage = () => { + const theme = useTheme(); + + const [tasks, setTasks] = useState(initialTasks); + const [segment, setSegment] = useState("همه"); + const [draftTitle, setDraftTitle] = useState(""); + const [draftZone, setDraftZone] = useState("قطعه گندم - شمال مزرعه"); + const [draftTime, setDraftTime] = useState("07:00"); + const [draftPriority, setDraftPriority] = useState("متوسط"); + + const completedCount = useMemo( + () => tasks.filter((task) => task.status === "done").length, + [tasks], + ); + const openCount = tasks.length - completedCount; + const urgentCount = useMemo( + () => + tasks.filter((task) => task.priority === "زیاد" && task.status === "open") + .length, + [tasks], + ); + const progressValue = + tasks.length === 0 ? 0 : Math.round((completedCount / tasks.length) * 100); + + const filteredTasks = useMemo(() => { + switch (segment) { + case "فوری": + return tasks.filter( + (task) => task.priority === "زیاد" && task.status === "open", + ); + case "انجام شده": + return tasks.filter((task) => task.status === "done"); + case "امروز": + return tasks; + default: + return tasks; + } + }, [segment, tasks]); + + const stats = [ + { + stats: `${openCount} کار`, + title: "کارهای باز امروز", + avatarIcon: "tabler-plant-2", + avatarColor: "primary" as const, + }, + { + stats: `${completedCount} مورد`, + title: "انجام شده تا الان", + avatarIcon: "tabler-circle-check", + avatarColor: "success" as const, + }, + { + stats: `${urgentCount} تسک`, + title: "اولویت خیلی بالا", + avatarIcon: "tabler-bolt", + avatarColor: "error" as const, + }, + ]; + + const toggleTask = (taskId: number) => { + setTasks((currentTasks) => + currentTasks.map((task) => + task.id === taskId + ? { ...task, status: task.status === "done" ? "open" : "done" } + : task, + ), + ); + }; + + const addTask = () => { + if (!draftTitle.trim()) return; + + setTasks((currentTasks) => [ + { + id: Date.now(), + title: draftTitle.trim(), + zone: draftZone, + time: draftTime, + priority: draftPriority, + note: "بعد از ثبت انجام، اگر موردی غیرعادی بود برای شیفت بعدی یادداشت بگذار.", + tags: [draftPriority === "زیاد" ? "فوری" : "روزانه", "ثبت دستی"], + status: "open", + }, + ...currentTasks, + ]); + + setDraftTitle(""); + setDraftTime("07:00"); + setDraftPriority("متوسط"); + }; + + return ( + + + + + + + + +
+ +
+ + تودولیست ساده، تمیز و کاربردی برای مدیریت کارهای مزرعه + + + کارهای مهم روز، بازدیدهای میدانی و کارهای پیگیری را یکجا + نگه دار تا بین آبیاری، خاک، برداشت و هماهنگی تیم چیزی از + قلم نیفتد. + +
+
+ + + شروع روز + + + بازدید 06:30 از ردیف شمالی + + + + + تمرکز امروز + + + آبیاری، آفت و هماهنگی بار خروجی + + +
+
+
+ + +
+
+ + پیشرفت امروز + + + {progressValue}% تکمیل شده + +
+ + + + + {progressValue}% + + + +
+ +
+ + + +
+ + پنجره طلایی صبح + + + بهترین زمان برای آبیاری و بازدید سریع تا قبل از اوج + گرما. + +
+
+
+
+
+
+
+
+ + {stats.map((item) => ( + + + + ))} + + + + + } + /> + +
+ {segments.map((item) => ( + setSegment(item)} + /> + ))} +
+
+ {filteredTasks.map((task) => { + const meta = priorityMeta[task.priority]; + + return ( + +
+
+ toggleTask(task.id)} + sx={{ mt: -0.5 }} + /> +
+
+ + {task.title} + + +
+ {task.note} +
+ {task.tags.map((tag) => ( + + ))} +
+
+
+ +
+ + + +
+ + {task.time} + + {task.zone} +
+
+ + {task.status === "done" + ? "انجام شده و ثبت شده" + : "منتظر اقدام تیم مزرعه"} + +
+
+
+ ); + })} +
+
+
+
+ + +
+ + + + setDraftTitle(event.target.value)} + /> + setDraftZone(event.target.value)} + > + + قطعه گندم - شمال مزرعه + + گلخانه شماره 2 + انبار مرکزی + + باغچه آزمایشی غربی + + + + + setDraftTime(event.target.value)} + /> + + + + setDraftPriority(event.target.value as TaskPriority) + } + > + زیاد + متوسط + کم + + + + + + + + + + + {[ + { + title: "اول بازدیدها، بعد تماس ها", + text: "کارهای میدانی صبح را قبل از تماس ها و هماهنگی های اداری جمع کن.", + color: "primary" as const, + icon: "tabler-tractor", + }, + { + title: "یادداشت های کوتاه ولی دقیق", + text: "برای هر کار فقط یک یادداشت عملی ثبت کن تا شیفت بعدی سریع متوجه شود.", + color: "info" as const, + icon: "tabler-notes", + }, + { + title: "فوری ها را جدا نگه دار", + text: "اگر کاری روی کیفیت محصول یا آب تاثیر مستقیم دارد، آن را در دسته فوری نگه دار.", + color: "error" as const, + icon: "tabler-bolt", + }, + ].map((item) => ( + +
+ + + +
+ + {item.title} + + {item.text} +
+
+
+ ))} +
+
+
+
+
+ ); +}; + +export default FarmerTodoPage; diff --git a/src/views/dashboards/farm/wallet/FarmWalletPage.tsx b/src/views/dashboards/farm/wallet/FarmWalletPage.tsx new file mode 100644 index 0000000..7275547 --- /dev/null +++ b/src/views/dashboards/farm/wallet/FarmWalletPage.tsx @@ -0,0 +1,1047 @@ +"use client"; + +import { useMemo, useState } 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 CardHeader from "@mui/material/CardHeader"; +import Chip from "@mui/material/Chip"; +import Divider from "@mui/material/Divider"; +import Grid from "@mui/material/Grid2"; +import InputAdornment from "@mui/material/InputAdornment"; +import LinearProgress from "@mui/material/LinearProgress"; +import Typography from "@mui/material/Typography"; +import { alpha, useTheme } from "@mui/material/styles"; + +import classnames from "classnames"; +import { rankItem } from "@tanstack/match-sorter-utils"; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import type { ColumnDef, FilterFn, SortingState } from "@tanstack/react-table"; +import type { RankingInfo } from "@tanstack/match-sorter-utils"; + +import type { ThemeColor } from "@core/types"; + +import OptionMenu from "@core/components/option-menu"; +import CustomAvatar from "@core/components/mui/Avatar"; +import CustomTextField from "@core/components/mui/TextField"; +import TablePaginationComponent from "@components/TablePaginationComponent"; +import HorizontalWithAvatar from "@components/card-statistics/HorizontalWithAvatar"; + +import tableStyles from "@core/styles/table.module.css"; + +declare module "@tanstack/table-core" { + interface FilterFns { + fuzzy: FilterFn; + } + interface FilterMeta { + itemRank: RankingInfo; + } +} + +type TransactionCategory = "واریز" | "برداشت" | "تسویه" | "بازگشت وجه"; +type TransactionStatus = "موفق" | "در انتظار" | "ناموفق"; + +type WalletTransaction = { + id: string; + title: string; + subtitle: string; + category: TransactionCategory; + amount: number; + status: TransactionStatus; + method: string; + createdAt: string; + createdAtLabel: string; + balanceAfter: number; + icon: string; + iconColor: ThemeColor; +}; + +type InsightItem = { + label: string; + value: string; + progress: number; + color: ThemeColor; +}; + +type LinkedAccount = { + title: string; + iban: string; + amount: number; + tone: ThemeColor; + status: string; +}; + +const categoryFilters = [ + "همه", + "واریز", + "برداشت", + "تسویه", + "بازگشت وجه", +] as const; + +type CategoryFilter = (typeof categoryFilters)[number]; + +const walletBalance = 248_500_000; +const pendingSettlement = 12_400_000; +const monthlyInflow = 94_800_000; +const monthlyOutflow = 51_200_000; + +const walletTransactions: WalletTransaction[] = [ + { + id: "TXN-9012", + title: "فروش محصول گندم", + subtitle: "تسویه بازارچه مرکزی", + category: "واریز", + amount: 18_700_000, + status: "موفق", + method: "واریز مستقیم به کیف پول", + createdAt: "2025-02-16T10:45:00", + createdAtLabel: "۲۸ بهمن ۱۴۰۳ - ۱۰:۴۵", + balanceAfter: 248_500_000, + icon: "tabler-wheat", + iconColor: "success", + }, + { + id: "TXN-9011", + title: "برداشت به حساب کشاورزی", + subtitle: "بانک کشاورزی - حساب اصلی", + category: "برداشت", + amount: -9_500_000, + status: "در انتظار", + method: "انتقال بانکی پایا", + createdAt: "2025-02-15T13:10:00", + createdAtLabel: "۲۷ بهمن ۱۴۰۳ - ۱۳:۱۰", + balanceAfter: 229_800_000, + icon: "tabler-building-bank", + iconColor: "warning", + }, + { + id: "TXN-9008", + title: "شارژ کیف پول عملیاتی", + subtitle: "افزایش اعتبار از حساب شرکت", + category: "واریز", + amount: 32_000_000, + status: "موفق", + method: "شتاب کارت به کارت", + createdAt: "2025-02-14T08:20:00", + createdAtLabel: "۲۶ بهمن ۱۴۰۳ - ۰۸:۲۰", + balanceAfter: 239_300_000, + icon: "tabler-plus", + iconColor: "primary", + }, + { + id: "TXN-9004", + title: "تسویه خرید کود", + subtitle: "پرداخت به تامین کننده سبزینه", + category: "تسویه", + amount: -14_250_000, + status: "موفق", + method: "کیف پول به کیف پول", + createdAt: "2025-02-12T17:05:00", + createdAtLabel: "۲۴ بهمن ۱۴۰۳ - ۱۷:۰۵", + balanceAfter: 207_300_000, + icon: "tabler-atom-2", + iconColor: "info", + }, + { + id: "TXN-8997", + title: "برگشت هزینه حمل", + subtitle: "اصلاح صورتحساب حمل بار", + category: "بازگشت وجه", + amount: 4_680_000, + status: "موفق", + method: "اعتبار فروشنده", + createdAt: "2025-02-10T11:30:00", + createdAtLabel: "۲۲ بهمن ۱۴۰۳ - ۱۱:۳۰", + balanceAfter: 221_550_000, + icon: "tabler-rotate-2", + iconColor: "secondary", + }, + { + id: "TXN-8992", + title: "برداشت اضطراری آبیاری", + subtitle: "پرداخت هزینه سوخت پمپ", + category: "برداشت", + amount: -6_300_000, + status: "موفق", + method: "برداشت به کارت", + createdAt: "2025-02-09T18:50:00", + createdAtLabel: "۲۱ بهمن ۱۴۰۳ - ۱۸:۵۰", + balanceAfter: 216_870_000, + icon: "tabler-droplet", + iconColor: "error", + }, + { + id: "TXN-8988", + title: "تسویه فروش جو", + subtitle: "قرارداد شماره ۴۸۱", + category: "واریز", + amount: 21_900_000, + status: "ناموفق", + method: "درگاه شریک تجاری", + createdAt: "2025-02-08T09:15:00", + createdAtLabel: "۲۰ بهمن ۱۴۰۳ - ۰۹:۱۵", + balanceAfter: 223_170_000, + icon: "tabler-leaf", + iconColor: "success", + }, + { + id: "TXN-8981", + title: "تسویه بیمه محصولات", + subtitle: "بیمه کشاورزی استان", + category: "تسویه", + amount: -11_150_000, + status: "موفق", + method: "دستور پرداخت زمان بندی شده", + createdAt: "2025-02-06T15:40:00", + createdAtLabel: "۱۸ بهمن ۱۴۰۳ - ۱۵:۴۰", + balanceAfter: 201_270_000, + icon: "tabler-shield-check", + iconColor: "secondary", + }, +]; + +const insightItems: InsightItem[] = [ + { + label: "نرخ موفقیت تراکنش ها", + value: "۹۲٪", + progress: 92, + color: "success", + }, + { + label: "پرداخت های خودکار این هفته", + value: "۶۴٪", + progress: 64, + color: "primary", + }, + { + label: "درخواست های در انتظار تسویه", + value: "۳ از ۱۱ مورد", + progress: 27, + color: "warning", + }, +]; + +const linkedAccounts: LinkedAccount[] = [ + { + title: "حساب اصلی مزرعه", + iban: "IR 8205 6000 0000 1024 5501 10", + amount: 96_000_000, + tone: "primary", + status: "پیش فرض", + }, + { + title: "حساب هزینه های جاری", + iban: "IR 1201 7000 0000 7722 9911 20", + amount: 31_500_000, + tone: "info", + status: "متصل", + }, + { + title: "حساب پاداش و مشوق ها", + iban: "IR 5401 5000 0000 2119 8831 77", + amount: 14_800_000, + tone: "success", + status: "فعال", + }, +]; + +const formatter = new Intl.NumberFormat("fa-IR"); + +const formatCurrency = (amount: number) => + `${formatter.format(Math.abs(amount))} تومان`; + +const categoryColorMap: Record = { + واریز: "success", + برداشت: "warning", + تسویه: "info", + "بازگشت وجه": "secondary", +}; + +const statusColorMap: Record = { + موفق: "success", + "در انتظار": "warning", + ناموفق: "error", +}; + +const fuzzyFilter: FilterFn = ( + row, + columnId, + value, + addMeta, +) => { + const itemRank = rankItem(String(row.getValue(columnId) ?? ""), value); + + addMeta({ + itemRank, + }); + + return itemRank.passed; +}; + +const columnHelper = createColumnHelper(); + +const FarmWalletPage = () => { + const theme = useTheme(); + + const [activeFilter, setActiveFilter] = useState("همه"); + const [globalFilter, setGlobalFilter] = useState(""); + const [sorting, setSorting] = useState([ + { id: "createdAt", desc: true }, + ]); + + const filteredTransactions = useMemo(() => { + if (activeFilter === "همه") return walletTransactions; + + return walletTransactions.filter((item) => item.category === activeFilter); + }, [activeFilter]); + + const walletStats = useMemo( + () => [ + { + stats: formatCurrency(176_900_000), + title: "موجودی قابل برداشت", + avatarIcon: "tabler-wallet", + avatarColor: "success" as const, + }, + { + stats: "۱۸ ساعت", + title: "میانگین زمان تسویه", + avatarIcon: "tabler-clock-hour-4", + avatarColor: "info" as const, + }, + { + stats: formatCurrency(12_400_000), + title: "تسویه های در انتظار", + avatarIcon: "tabler-arrows-transfer-up-down", + avatarColor: "warning" as const, + }, + { + stats: "۹۲٪", + title: "تراکنش های موفق", + avatarIcon: "tabler-circle-check", + avatarColor: "primary" as const, + }, + ], + [], + ); + + const columns = useMemo[]>( + () => [ + columnHelper.accessor("title", { + header: "تراکنش", + cell: ({ row }) => ( +
+ + + +
+ + {row.original.title} + + {`${row.original.subtitle} • ${row.original.id}`} +
+
+ ), + }), + columnHelper.accessor("createdAt", { + header: "تاریخ", + cell: ({ row }) => ( + {row.original.createdAtLabel} + ), + }), + columnHelper.accessor("category", { + header: "نوع", + cell: ({ row }) => ( + + ), + }), + columnHelper.accessor("method", { + header: "کانال پرداخت", + cell: ({ row }) => {row.original.method}, + }), + columnHelper.accessor("amount", { + header: "مبلغ", + cell: ({ row }) => ( +
+ 0 ? "success.main" : "error.main"} + className="font-semibold" + > + {`${row.original.amount > 0 ? "+" : "-"}${formatCurrency(row.original.amount)}`} + + {`موجودی بعدی: ${formatCurrency(row.original.balanceAfter)}`} +
+ ), + }), + columnHelper.accessor("status", { + header: "وضعیت", + cell: ({ row }) => ( + + ), + }), + columnHelper.display({ + id: "actions", + header: "اقدامات", + cell: () => ( + + ), + enableSorting: false, + }), + ], + [], + ); + + const table = useReactTable({ + data: filteredTransactions, + columns, + filterFns: { + fuzzy: fuzzyFilter, + }, + state: { + globalFilter, + sorting, + }, + initialState: { + pagination: { + pageSize: 6, + }, + }, + globalFilterFn: fuzzyFilter, + onGlobalFilterChange: setGlobalFilter, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + return ( + + + + + + + + +
+ +
+ + کیف پول مزرعه و مرکز تراکنش ها + + + همه واریزها، برداشت ها و تسویه های روزانه را از یک نمای + یکپارچه کنترل کن و روی جریان نقدی مزرعه دید لحظه ای داشته + باش. + +
+
+ + موجودی کل + + + {formatCurrency(walletBalance)} + +
+
+ + + + +
+ + در انتظار تسویه + + + {formatCurrency(pendingSettlement)} + +
+
+ + + + +
+ + ورودی ۳۰ روز اخیر + + + {formatCurrency(monthlyInflow)} + +
+
+ + + + +
+ + خروجی ۳۰ روز اخیر + + + {formatCurrency(monthlyOutflow)} + +
+
+
+
+
+ + +
+
+ + سلامت نقدینگی + + + جریان پول این ماه کاملا تحت کنترل است + +
+ + + +
+ +
+
+ + ظرفیت برداشت آزاد + + + ۷۱٪ + +
+ +
+
+ + + + +
+
+
+
+
+
+
+
+ + {walletStats.map((item) => ( + + + + ))} + + + + + } + /> + + {insightItems.map((item) => ( +
+
+ + {item.label} + + + {item.value} + +
+ +
+ ))} + +
+ + + +
+ + پیشنهاد هوشمند امروز + + + برای پرداخت های تکرارشونده تامین کننده ها، زمان بندی خودکار + فعال کن تا نوسان نقدینگی کمتر شود و تاخیرهای دستی حذف شوند. + +
+
+
+
+
+
+ + + + + } + /> + + {linkedAccounts.map((account, index) => ( + +
+
+ + + +
+ + {account.title} + + {account.iban} +
+
+
+ + {formatCurrency(account.amount)} + + +
+
+ {index !== linkedAccounts.length - 1 && ( + + )} +
+ ))} +
+
+
+ + + + + } + /> + +
+
+ {categoryFilters.map((filter) => ( + setActiveFilter(filter)} + /> + ))} +
+ setGlobalFilter(event.target.value)} + placeholder="جستجو در تراکنش ها" + size="small" + className="max-sm:is-full lg:min-is-[320px]" + InputProps={{ + startAdornment: ( + + + + ), + }} + /> +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.length === 0 ? ( + + + + ) : ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + )} + +
+ {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {{ + asc: ( + + ), + desc: ( + + ), + }[header.column.getIsSorted() as string] ?? null} +
+ )} +
+ هیچ تراکنشی با این فیلتر پیدا نشد. +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
+ +
+
+
+ ); +}; + +export default FarmWalletPage;