UPDATE
This commit is contained in:
@@ -142,6 +142,31 @@ interface ApiResponse<T> {
|
|||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FertilizationRecommendationHistoryItem {
|
||||||
|
recommendation_uuid: string;
|
||||||
|
plant_name: string;
|
||||||
|
growth_stage: string;
|
||||||
|
fertilizer_type: string;
|
||||||
|
status: "pending_confirmation" | "in_progress" | "completed" | string;
|
||||||
|
status_label: string;
|
||||||
|
requested_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FertilizationRecommendationHistoryPagination {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
total_pages: number;
|
||||||
|
total_items: number;
|
||||||
|
has_next: boolean;
|
||||||
|
has_previous: boolean;
|
||||||
|
next: string | null;
|
||||||
|
previous: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaginatedApiResponse<T> extends ApiResponse<T> {
|
||||||
|
pagination: FertilizationRecommendationHistoryPagination;
|
||||||
|
}
|
||||||
|
|
||||||
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
|
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
|
||||||
const res = await promise;
|
const res = await promise;
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -166,4 +191,34 @@ export const fertilizationRecommendationService = {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getRecommendationsHistory(
|
||||||
|
farmUuid: string,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10,
|
||||||
|
): Promise<{
|
||||||
|
data: FertilizationRecommendationHistoryItem[];
|
||||||
|
pagination: FertilizationRecommendationHistoryPagination;
|
||||||
|
}> {
|
||||||
|
const response = await apiClient.get<
|
||||||
|
PaginatedApiResponse<FertilizationRecommendationHistoryItem[]>
|
||||||
|
>(
|
||||||
|
`${RECOMMEND_PREFIX}/recommendations/?farm_uuid=${encodeURIComponent(farmUuid)}&page=${page}&page_size=${pageSize}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response.data,
|
||||||
|
pagination: response.pagination,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getRecommendationDetail(
|
||||||
|
recommendationUuid: string,
|
||||||
|
): Promise<FertilizationRecommendationResult> {
|
||||||
|
return unwrap(
|
||||||
|
apiClient.get<ApiResponse<FertilizationRecommendationResult>>(
|
||||||
|
`${RECOMMEND_PREFIX}/recommendations/${encodeURIComponent(recommendationUuid)}/`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ import IconButton from "@mui/material/IconButton";
|
|||||||
import LinearProgress from "@mui/material/LinearProgress";
|
import LinearProgress from "@mui/material/LinearProgress";
|
||||||
import Paper from "@mui/material/Paper";
|
import Paper from "@mui/material/Paper";
|
||||||
import Slider from "@mui/material/Slider";
|
import Slider from "@mui/material/Slider";
|
||||||
|
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 TablePagination from "@mui/material/TablePagination";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import Step from "@mui/material/Step";
|
import Step from "@mui/material/Step";
|
||||||
import StepContent from "@mui/material/StepContent";
|
import StepContent from "@mui/material/StepContent";
|
||||||
import StepLabel from "@mui/material/StepLabel";
|
import StepLabel from "@mui/material/StepLabel";
|
||||||
@@ -28,6 +36,8 @@ import {
|
|||||||
type CropOption,
|
type CropOption,
|
||||||
type FertilizationAlternativeRecommendation,
|
type FertilizationAlternativeRecommendation,
|
||||||
type FertilizationNutrientItem,
|
type FertilizationNutrientItem,
|
||||||
|
type FertilizationRecommendationHistoryItem,
|
||||||
|
type FertilizationRecommendationHistoryPagination,
|
||||||
type FertilizationRecommendationResult,
|
type FertilizationRecommendationResult,
|
||||||
type GrowthStage,
|
type GrowthStage,
|
||||||
} from "@/libs/api/services/fertilizationRecommendationService";
|
} from "@/libs/api/services/fertilizationRecommendationService";
|
||||||
@@ -93,6 +103,29 @@ const formatUnitLabel = (unit: string) => {
|
|||||||
return unit;
|
return unit;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (value: string) => {
|
||||||
|
const date = new Date(value);
|
||||||
|
|
||||||
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat("fa-IR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}).format(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusChipColor = (
|
||||||
|
status: string,
|
||||||
|
): "warning" | "info" | "default" => {
|
||||||
|
if (status === "in_progress") return "info";
|
||||||
|
if (status === "completed") return "default";
|
||||||
|
|
||||||
|
return "warning";
|
||||||
|
};
|
||||||
|
|
||||||
export default function SmartFertilizationRecommendation() {
|
export default function SmartFertilizationRecommendation() {
|
||||||
const t = useTranslations("fertilization");
|
const t = useTranslations("fertilization");
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -116,6 +149,24 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
||||||
const [reasoningExpanded, setReasoningExpanded] = useState(false);
|
const [reasoningExpanded, setReasoningExpanded] = useState(false);
|
||||||
const [area, setArea] = useState(1);
|
const [area, setArea] = useState(1);
|
||||||
|
const [historyItems, setHistoryItems] = useState<
|
||||||
|
FertilizationRecommendationHistoryItem[]
|
||||||
|
>([]);
|
||||||
|
const [historyPage, setHistoryPage] = useState(0);
|
||||||
|
const [historyPageSize, setHistoryPageSize] = useState(10);
|
||||||
|
const [historyPagination, setHistoryPagination] =
|
||||||
|
useState<FertilizationRecommendationHistoryPagination>({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
total_pages: 0,
|
||||||
|
total_items: 0,
|
||||||
|
has_next: false,
|
||||||
|
has_previous: false,
|
||||||
|
next: null,
|
||||||
|
previous: null,
|
||||||
|
});
|
||||||
|
const [historyLoading, setHistoryLoading] = useState(false);
|
||||||
|
const [historyError, setHistoryError] = useState<string | null>(null);
|
||||||
const [detailsSheet, setDetailsSheet] = useState({
|
const [detailsSheet, setDetailsSheet] = useState({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
title: "",
|
title: "",
|
||||||
@@ -170,6 +221,33 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
.finally(() => setConfigLoading(false));
|
.finally(() => setConfigLoading(false));
|
||||||
}, [farmUuid, t]);
|
}, [farmUuid, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!farmUuid) {
|
||||||
|
setHistoryItems([]);
|
||||||
|
setHistoryError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHistoryLoading(true);
|
||||||
|
setHistoryError(null);
|
||||||
|
fertilizationRecommendationService
|
||||||
|
.getRecommendationsHistory(
|
||||||
|
farmUuid,
|
||||||
|
historyPage + 1,
|
||||||
|
historyPageSize,
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setHistoryItems(response.data);
|
||||||
|
setHistoryPagination(response.pagination);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setHistoryError(
|
||||||
|
getErrorMessage(error, "خطا در دریافت تاریخچه توصیه های کودهی"),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => setHistoryLoading(false));
|
||||||
|
}, [farmUuid, historyPage, historyPageSize]);
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!selectedCrop || !growthStage || !farmUuid) return;
|
if (!selectedCrop || !growthStage || !farmUuid) return;
|
||||||
|
|
||||||
@@ -295,6 +373,30 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
setDetailsSheet((prev) => ({ ...prev, isOpen: false }));
|
setDetailsSheet((prev) => ({ ...prev, isOpen: false }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewRecommendationReport = async (recommendationUuid: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
setRequestError(null);
|
||||||
|
setStatusMessage("در حال دریافت گزارش توصیه");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response =
|
||||||
|
await fertilizationRecommendationService.getRecommendationDetail(
|
||||||
|
recommendationUuid,
|
||||||
|
);
|
||||||
|
|
||||||
|
setRecommendation(response);
|
||||||
|
setReasoningExpanded(false);
|
||||||
|
setArea(1);
|
||||||
|
} catch (error) {
|
||||||
|
setRequestError(
|
||||||
|
getErrorMessage(error, "خطا در دریافت گزارش توصیه کودهی"),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setStatusMessage(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="min-bs-screen"
|
className="min-bs-screen"
|
||||||
@@ -997,10 +1099,164 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
{requestError}
|
{requestError}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Box className="mt-8">
|
||||||
|
{loading ? (
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
className="animate-fade-in"
|
||||||
|
sx={{
|
||||||
|
borderRadius: "28px",
|
||||||
|
background: `linear-gradient(160deg, ${paperBg} 0%, ${alpha(primaryMain, 0.06)} 100%)`,
|
||||||
|
boxShadow: `0 8px 32px ${alpha(primaryMain, 0.1)}`,
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.12)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-12 flex flex-col items-center gap-4">
|
||||||
|
<Box
|
||||||
|
className="w-14 h-14 rounded-2xl flex items-center justify-center animate-pulse"
|
||||||
|
sx={{
|
||||||
|
background: `linear-gradient(135deg, ${alpha(primaryMain, 0.15)} 0%, ${alpha(primaryMain, 0.08)} 100%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="tabler-sparkles text-2xl"
|
||||||
|
style={{ color: primaryMain }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{statusMessage ?? "در حال تحلیل و تولید نسخه تغذیه ای..."}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Box className="mb-4 flex items-center justify-between gap-3">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" fontWeight={700}>
|
||||||
|
تاریخچه توصیه های کودهی
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" className="mt-1">
|
||||||
|
همه توصیه های قبلی مزرعه را اینجا ببینید و گزارش کامل هرکدام را باز کنید.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
className="animate-fade-in"
|
||||||
|
sx={{
|
||||||
|
borderRadius: "24px",
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.12)}`,
|
||||||
|
background: `linear-gradient(180deg, ${paperBg} 0%, ${alpha(primaryMain, 0.03)} 100%)`,
|
||||||
|
boxShadow: `0 8px 28px ${alpha(primaryMain, 0.08)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{historyError && (
|
||||||
|
<Typography variant="body2" color="error" className="px-4 py-4">
|
||||||
|
{historyError}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>تاریخ ثبت</TableCell>
|
||||||
|
<TableCell>محصول / مرحله رشد</TableCell>
|
||||||
|
<TableCell>نوع کود</TableCell>
|
||||||
|
<TableCell>وضعیت</TableCell>
|
||||||
|
<TableCell align="center">گزارش</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{historyLoading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} align="center" className="py-8">
|
||||||
|
<CircularProgress size={28} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : historyItems.length ? (
|
||||||
|
historyItems.map((item) => (
|
||||||
|
<TableRow key={item.recommendation_uuid} hover>
|
||||||
|
<TableCell>{formatDateTime(item.requested_at)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" fontWeight={700}>
|
||||||
|
{item.plant_name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{formatStageLabel(item.growth_stage)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{item.fertilizer_type}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={item.status_label}
|
||||||
|
color={getStatusChipColor(item.status)}
|
||||||
|
variant="outlined"
|
||||||
|
className="rounded-xl"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Tooltip title="مشاهده گزارش">
|
||||||
|
<IconButton
|
||||||
|
onClick={() =>
|
||||||
|
handleViewRecommendationReport(
|
||||||
|
item.recommendation_uuid,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.16)}`,
|
||||||
|
borderRadius: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="tabler-file-description text-lg"
|
||||||
|
style={{ color: primaryMain }}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} align="center" className="py-8">
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
هنوز توصیه ای برای این مزرعه ثبت نشده است.
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
<TablePagination
|
||||||
|
component="div"
|
||||||
|
count={historyPagination.total_items}
|
||||||
|
page={historyPage}
|
||||||
|
onPageChange={(_, nextPage) => setHistoryPage(nextPage)}
|
||||||
|
rowsPerPage={historyPageSize}
|
||||||
|
onRowsPerPageChange={(event) => {
|
||||||
|
setHistoryPage(0);
|
||||||
|
setHistoryPageSize(Number(event.target.value));
|
||||||
|
}}
|
||||||
|
rowsPerPageOptions={[5, 10, 20]}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading && (
|
{loading && recommendation && (
|
||||||
<Card
|
<Card
|
||||||
elevation={0}
|
elevation={0}
|
||||||
className="mb-6 animate-fade-in"
|
className="mb-6 animate-fade-in"
|
||||||
|
|||||||
@@ -8,7 +8,37 @@ import CardContent from "@mui/material/CardContent";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import AlertTitle from "@mui/material/AlertTitle";
|
||||||
|
import Checkbox from "@mui/material/Checkbox";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import FormGroup from "@mui/material/FormGroup";
|
||||||
|
import Tab from "@mui/material/Tab";
|
||||||
|
import Tabs from "@mui/material/Tabs";
|
||||||
|
import Timeline from "@mui/lab/Timeline";
|
||||||
|
import TimelineConnector from "@mui/lab/TimelineConnector";
|
||||||
|
import TimelineContent from "@mui/lab/TimelineContent";
|
||||||
|
import TimelineDot from "@mui/lab/TimelineDot";
|
||||||
|
import TimelineItem from "@mui/lab/TimelineItem";
|
||||||
|
import TimelineSeparator from "@mui/lab/TimelineSeparator";
|
||||||
import { useTheme, alpha } from "@mui/material/styles";
|
import { useTheme, alpha } from "@mui/material/styles";
|
||||||
|
import {
|
||||||
|
CalendarDays,
|
||||||
|
Download,
|
||||||
|
Droplets,
|
||||||
|
Save,
|
||||||
|
Sprout,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
import { useFarmHub } from "@/hooks/useFarmHub";
|
import { useFarmHub } from "@/hooks/useFarmHub";
|
||||||
import type {
|
import type {
|
||||||
CropOption,
|
CropOption,
|
||||||
@@ -65,6 +95,105 @@ const getErrorMessage = (error: unknown, fallback: string) =>
|
|||||||
? error.message
|
? error.message
|
||||||
: fallback;
|
: fallback;
|
||||||
|
|
||||||
|
type FertilizerPlan = {
|
||||||
|
generated: boolean;
|
||||||
|
crop: string;
|
||||||
|
status: string;
|
||||||
|
alerts: Array<{
|
||||||
|
severity: "success" | "info" | "warning" | "error";
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
nutrients: Array<{
|
||||||
|
name: string;
|
||||||
|
current: number;
|
||||||
|
target: number;
|
||||||
|
}>;
|
||||||
|
recommendedFertilizers: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
dosage: string;
|
||||||
|
method: string;
|
||||||
|
}>;
|
||||||
|
stages: Array<{
|
||||||
|
id: number;
|
||||||
|
phase: string;
|
||||||
|
time: string;
|
||||||
|
summary: string;
|
||||||
|
completed: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MOCK_FERTILIZER_PLAN: FertilizerPlan = {
|
||||||
|
generated: true,
|
||||||
|
crop: "Wheat",
|
||||||
|
status: "Plan ready",
|
||||||
|
alerts: [
|
||||||
|
{
|
||||||
|
severity: "warning",
|
||||||
|
title: "Low Nitrogen",
|
||||||
|
message: "Soil N is below optimal levels.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
severity: "error",
|
||||||
|
title: "High Salinity",
|
||||||
|
message: "EC levels are critical, avoid high-salt fertilizers.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nutrients: [
|
||||||
|
{ name: "Nitrogen (N)", current: 30, target: 80 },
|
||||||
|
{ name: "Phosphorus (P)", current: 45, target: 60 },
|
||||||
|
{ name: "Potassium (K)", current: 70, target: 90 },
|
||||||
|
],
|
||||||
|
recommendedFertilizers: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Urea",
|
||||||
|
type: "46-0-0",
|
||||||
|
dosage: "50 kg/ha",
|
||||||
|
method: "Soil Application",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Potassium Sulfate",
|
||||||
|
type: "0-0-50",
|
||||||
|
dosage: "25 kg/ha",
|
||||||
|
method: "Fertigation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Micronutrient Mix",
|
||||||
|
type: "Liquid",
|
||||||
|
dosage: "2 L/ha",
|
||||||
|
method: "Foliar Spray",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
phase: "Vegetative Growth",
|
||||||
|
time: "Week 2 - 4",
|
||||||
|
summary: "High Nitrogen application",
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
phase: "Flowering",
|
||||||
|
time: "Week 5 - 7",
|
||||||
|
summary: "Switch to Phosphorus focus",
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
phase: "Fruiting / Maturation",
|
||||||
|
time: "Week 8 - 10",
|
||||||
|
summary: "Potassium boost for fruit size",
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export default function SmartIrrigationRecommendation() {
|
export default function SmartIrrigationRecommendation() {
|
||||||
const t = useTranslations("irrigation");
|
const t = useTranslations("irrigation");
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -78,7 +207,10 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
const [selectedGrowthStage, setSelectedGrowthStage] = useState<string | null>(
|
const [selectedGrowthStage, setSelectedGrowthStage] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [plan, setPlan] = useState<IrrigationPlan | null>(null);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [irrigationPlan, setIrrigationPlan] = useState<IrrigationPlan | null>(null);
|
||||||
|
const [fertilizerPlan, setFertilizerPlan] = useState<FertilizerPlan | null>(null);
|
||||||
|
const [completedTasks, setCompletedTasks] = useState<number[]>([]);
|
||||||
const [waterBalance, setWaterBalance] = useState<WaterBalance | null>(null);
|
const [waterBalance, setWaterBalance] = useState<WaterBalance | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [requestError, setRequestError] = useState<string | null>(null);
|
const [requestError, setRequestError] = useState<string | null>(null);
|
||||||
@@ -87,9 +219,12 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
const primaryLight = theme.palette.primary.light;
|
const primaryLight = theme.palette.primary.light;
|
||||||
const primaryDark = theme.palette.primary.dark;
|
const primaryDark = theme.palette.primary.dark;
|
||||||
const paperBg = theme.palette.background.paper;
|
const paperBg = theme.palette.background.paper;
|
||||||
|
const completedTaskCount = completedTasks.length;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPlan(null);
|
setIrrigationPlan(null);
|
||||||
|
setFertilizerPlan(null);
|
||||||
|
setCompletedTasks([]);
|
||||||
setWaterBalance(null);
|
setWaterBalance(null);
|
||||||
setRequestError(null);
|
setRequestError(null);
|
||||||
setSelectedCrop(null);
|
setSelectedCrop(null);
|
||||||
@@ -132,7 +267,9 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!selectedCrop || !farmUuid) return;
|
if (!selectedCrop || !farmUuid) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setPlan(null);
|
setIrrigationPlan(null);
|
||||||
|
setFertilizerPlan(null);
|
||||||
|
setCompletedTasks([]);
|
||||||
setWaterBalance(null);
|
setWaterBalance(null);
|
||||||
setRequestError(null);
|
setRequestError(null);
|
||||||
setStatusMessage(t("generating"));
|
setStatusMessage(t("generating"));
|
||||||
@@ -170,7 +307,8 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
throw new Error(taskStatus.error ?? t("errors.generateFailed"));
|
throw new Error(taskStatus.error ?? t("errors.generateFailed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlan(taskStatus.result.plan);
|
setIrrigationPlan(taskStatus.result.plan);
|
||||||
|
setFertilizerPlan(MOCK_FERTILIZER_PLAN);
|
||||||
setWaterBalance(taskStatus.result.water_balance ?? null);
|
setWaterBalance(taskStatus.result.water_balance ?? null);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -180,10 +318,13 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
throw new Error(t("errors.generateFailed"));
|
throw new Error(t("errors.generateFailed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlan(recommendation.plan);
|
setIrrigationPlan(recommendation.plan);
|
||||||
|
setFertilizerPlan(MOCK_FERTILIZER_PLAN);
|
||||||
setWaterBalance(recommendation.water_balance ?? null);
|
setWaterBalance(recommendation.water_balance ?? null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setPlan(null);
|
setIrrigationPlan(null);
|
||||||
|
setFertilizerPlan(null);
|
||||||
|
setCompletedTasks([]);
|
||||||
setWaterBalance(null);
|
setWaterBalance(null);
|
||||||
setRequestError(getErrorMessage(error, t("errors.generateFailed")));
|
setRequestError(getErrorMessage(error, t("errors.generateFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -193,11 +334,18 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const moistureLevelValue =
|
const moistureLevelValue =
|
||||||
typeof plan?.moistureLevel === "number"
|
typeof irrigationPlan?.moistureLevel === "number"
|
||||||
? plan.moistureLevel
|
? irrigationPlan.moistureLevel
|
||||||
: Number(plan?.moistureLevel);
|
: Number(irrigationPlan?.moistureLevel);
|
||||||
const hasNumericMoistureLevel = Number.isFinite(moistureLevelValue);
|
const hasNumericMoistureLevel = Number.isFinite(moistureLevelValue);
|
||||||
const nextWaterBalanceDay = waterBalance?.daily?.[0];
|
const nextWaterBalanceDay = waterBalance?.daily?.[0];
|
||||||
|
const toggleCompletedTask = (taskId: number) => {
|
||||||
|
setCompletedTasks((prev) =>
|
||||||
|
prev.includes(taskId)
|
||||||
|
? prev.filter((id) => id !== taskId)
|
||||||
|
: [...prev, taskId],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCropSelect = (crop: CropOption) => {
|
const handleCropSelect = (crop: CropOption) => {
|
||||||
setSelectedCrop((prev) => {
|
setSelectedCrop((prev) => {
|
||||||
@@ -380,8 +528,35 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(_, value) => setActiveTab(value)}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
variant="fullWidth"
|
||||||
|
className="mb-6"
|
||||||
|
sx={{
|
||||||
|
bgcolor: alpha(primaryMain, 0.04),
|
||||||
|
borderRadius: "18px",
|
||||||
|
minHeight: 56,
|
||||||
|
"& .MuiTabs-indicator": {
|
||||||
|
height: 3,
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
label="Irrigation Plan"
|
||||||
|
sx={{ minHeight: 56, fontWeight: 600, textTransform: "none" }}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
label="Fertilizer Plan"
|
||||||
|
sx={{ minHeight: 56, fontWeight: 600, textTransform: "none" }}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
{/* 5) Result Card (after click) */}
|
{/* 5) Result Card (after click) */}
|
||||||
{plan && (
|
{activeTab === 0 && irrigationPlan && (
|
||||||
<Box className="mb-6 animate-fade-in">
|
<Box className="mb-6 animate-fade-in">
|
||||||
<Card
|
<Card
|
||||||
elevation={0}
|
elevation={0}
|
||||||
@@ -470,7 +645,7 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
color="primary.main"
|
color="primary.main"
|
||||||
className="mbe-1"
|
className="mbe-1"
|
||||||
>
|
>
|
||||||
{String(plan.moistureLevel)}
|
{String(irrigationPlan.moistureLevel)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{t("result.moistureLevel")}
|
{t("result.moistureLevel")}
|
||||||
@@ -483,17 +658,17 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
<ResultRow
|
<ResultRow
|
||||||
icon="tabler-calendar-week"
|
icon="tabler-calendar-week"
|
||||||
label={t("result.frequency")}
|
label={t("result.frequency")}
|
||||||
value={`${plan.frequencyPerWeek} ${t("result.timesPerWeek")}`}
|
value={`${irrigationPlan.frequencyPerWeek} ${t("result.timesPerWeek")}`}
|
||||||
/>
|
/>
|
||||||
<ResultRow
|
<ResultRow
|
||||||
icon="tabler-clock"
|
icon="tabler-clock"
|
||||||
label={t("result.duration")}
|
label={t("result.duration")}
|
||||||
value={`${plan.durationMinutes} ${t("result.minutes")}`}
|
value={`${irrigationPlan.durationMinutes} ${t("result.minutes")}`}
|
||||||
/>
|
/>
|
||||||
<ResultRow
|
<ResultRow
|
||||||
icon="tabler-sunrise"
|
icon="tabler-sunrise"
|
||||||
label={t("result.bestTime")}
|
label={t("result.bestTime")}
|
||||||
value={plan.bestTimeOfDay}
|
value={irrigationPlan.bestTimeOfDay}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -535,7 +710,7 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{plan.warning && (
|
{irrigationPlan.warning && (
|
||||||
<Box
|
<Box
|
||||||
className="mt-4 p-4 rounded-2xl"
|
className="mt-4 p-4 rounded-2xl"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -556,7 +731,7 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
{t("result.smartWarning")}
|
{t("result.smartWarning")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{plan.warning}
|
{irrigationPlan.warning}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -567,6 +742,366 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeTab === 1 && fertilizerPlan && (
|
||||||
|
<Box className="mt-6 space-y-6 animate-fade-in">
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
borderRadius: "24px",
|
||||||
|
background: `linear-gradient(160deg, ${paperBg} 0%, ${alpha(primaryMain, 0.05)} 100%)`,
|
||||||
|
boxShadow: `0 8px 32px ${alpha(primaryMain, 0.12)}, 0 2px 8px rgba(0,0,0,0.05)`,
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.14)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-6 space-y-6">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" fontWeight={700} className="mb-1">
|
||||||
|
Visual Summary
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{fertilizerPlan.status} for {fertilizerPlan.crop}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="space-y-4">
|
||||||
|
{fertilizerPlan.alerts.map((alert) => (
|
||||||
|
<Alert
|
||||||
|
key={`${alert.severity}-${alert.title}`}
|
||||||
|
severity={alert.severity}
|
||||||
|
className="mb-4 rounded-xl"
|
||||||
|
sx={{ alignItems: "flex-start" }}
|
||||||
|
>
|
||||||
|
<AlertTitle>{alert.title}</AlertTitle>
|
||||||
|
{alert.message}
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
borderRadius: "20px",
|
||||||
|
background: `linear-gradient(145deg, ${alpha(primaryMain, 0.05)} 0%, ${paperBg} 100%)`,
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.12)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4 sm:p-5">
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
fontWeight={700}
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
Nutrient Levels
|
||||||
|
</Typography>
|
||||||
|
<Box className="h-[300px] w-full">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={fertilizerPlan.nutrients}
|
||||||
|
margin={{ top: 8, right: 8, left: -12, bottom: 8 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
stroke={alpha(primaryMain, 0.15)}
|
||||||
|
/>
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
tick={{ fill: theme.palette.text.secondary, fontSize: 12 }}
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fill: theme.palette.text.secondary, fontSize: 12 }}
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
/>
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Bar
|
||||||
|
dataKey="current"
|
||||||
|
name="Current"
|
||||||
|
fill="#ff9800"
|
||||||
|
radius={[8, 8, 0, 0]}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
dataKey="target"
|
||||||
|
name="Target"
|
||||||
|
fill={primaryMain}
|
||||||
|
radius={[8, 8, 0, 0]}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
className="mt-6"
|
||||||
|
sx={{
|
||||||
|
borderRadius: "20px",
|
||||||
|
background: `linear-gradient(145deg, ${alpha(primaryMain, 0.05)} 0%, ${paperBg} 100%)`,
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.12)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4 sm:p-5">
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
fontWeight={700}
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
Fertilization Schedule
|
||||||
|
</Typography>
|
||||||
|
<Timeline
|
||||||
|
className="mt-0"
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
m: 0,
|
||||||
|
"& .MuiTimelineItem-root:before": {
|
||||||
|
flex: 0,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fertilizerPlan.stages.map((stage, index) => (
|
||||||
|
<TimelineItem key={stage.id}>
|
||||||
|
<TimelineSeparator>
|
||||||
|
<TimelineDot
|
||||||
|
color={stage.completed ? "success" : "grey"}
|
||||||
|
variant={stage.completed ? "filled" : "outlined"}
|
||||||
|
/>
|
||||||
|
{index < fertilizerPlan.stages.length - 1 && (
|
||||||
|
<TimelineConnector
|
||||||
|
sx={{
|
||||||
|
bgcolor: alpha(
|
||||||
|
stage.completed
|
||||||
|
? theme.palette.success.main
|
||||||
|
: theme.palette.grey[400],
|
||||||
|
0.4,
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TimelineSeparator>
|
||||||
|
<TimelineContent className="pb-6">
|
||||||
|
<Box
|
||||||
|
className="rounded-2xl p-4"
|
||||||
|
sx={{
|
||||||
|
bgcolor: alpha(primaryMain, 0.04),
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.08)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle1" fontWeight={700}>
|
||||||
|
{stage.phase}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
{stage.time}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.primary">
|
||||||
|
{stage.summary}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</TimelineContent>
|
||||||
|
</TimelineItem>
|
||||||
|
))}
|
||||||
|
</Timeline>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
className="mt-6"
|
||||||
|
sx={{
|
||||||
|
borderRadius: "20px",
|
||||||
|
background: `linear-gradient(145deg, ${alpha(primaryMain, 0.05)} 0%, ${paperBg} 100%)`,
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.12)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4 sm:p-5">
|
||||||
|
<Typography variant="h6" fontWeight={700} className="mb-4">
|
||||||
|
Current Stage Recommendations
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
{fertilizerPlan.recommendedFertilizers.map((item) => {
|
||||||
|
const isWaterBased =
|
||||||
|
item.method.toLowerCase().includes("fertigation") ||
|
||||||
|
item.method.toLowerCase().includes("foliar");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={item.id}
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: "18px",
|
||||||
|
background: `linear-gradient(145deg, ${paperBg} 0%, ${alpha(primaryMain, 0.05)} 100%)`,
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.1)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<Box className="flex items-start gap-3">
|
||||||
|
<Box
|
||||||
|
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl"
|
||||||
|
sx={{
|
||||||
|
background: alpha(primaryMain, 0.1),
|
||||||
|
color: primaryMain,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isWaterBased ? (
|
||||||
|
<Droplets size={18} />
|
||||||
|
) : (
|
||||||
|
<Sprout size={18} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="min-w-0 flex-1 space-y-3">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" fontWeight={700}>
|
||||||
|
{item.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Type: {item.type}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="grid grid-cols-2 gap-3">
|
||||||
|
<Box
|
||||||
|
className="rounded-xl p-3"
|
||||||
|
sx={{ bgcolor: alpha(primaryMain, 0.05) }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
>
|
||||||
|
Dosage
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" fontWeight={600}>
|
||||||
|
{item.dosage}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
className="rounded-xl p-3"
|
||||||
|
sx={{ bgcolor: alpha(primaryMain, 0.05) }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
>
|
||||||
|
Method
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" fontWeight={600}>
|
||||||
|
{item.method}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
className="mt-6 rounded-2xl bg-gray-50 p-4 sm:p-5"
|
||||||
|
sx={{
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.08)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
fontWeight={700}
|
||||||
|
className="mb-3"
|
||||||
|
>
|
||||||
|
Action List
|
||||||
|
</Typography>
|
||||||
|
<FormGroup className="gap-2">
|
||||||
|
{fertilizerPlan.recommendedFertilizers.map((item) => {
|
||||||
|
const isCompleted = completedTasks.includes(item.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
key={`task-${item.id}`}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={isCompleted}
|
||||||
|
onChange={() => toggleCompletedTask(item.id)}
|
||||||
|
sx={{ color: primaryMain }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className="m-0 items-start rounded-xl px-2 py-1"
|
||||||
|
label={
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className={isCompleted ? "line-through text-gray-400" : ""}
|
||||||
|
sx={{
|
||||||
|
color: isCompleted
|
||||||
|
? theme.palette.grey[400]
|
||||||
|
: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`Apply ${item.dosage} of ${item.name}`}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</FormGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
className="mt-6 rounded-2xl p-4 sm:p-5"
|
||||||
|
sx={{
|
||||||
|
background: `linear-gradient(145deg, ${alpha(primaryMain, 0.06)} 0%, ${alpha(primaryLight, 0.08)} 100%)`,
|
||||||
|
border: `1px solid ${alpha(primaryMain, 0.12)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
Next action required in 14 days. {completedTaskCount} of{" "}
|
||||||
|
{fertilizerPlan.recommendedFertilizers.length} tasks completed.
|
||||||
|
</Typography>
|
||||||
|
<Box className="flex flex-col gap-3 sm:flex-row sm:justify-end">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<Save size={18} />}
|
||||||
|
className="rounded-xl px-4 py-2.5"
|
||||||
|
>
|
||||||
|
Save Plan
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<Download size={18} />}
|
||||||
|
className="rounded-xl px-4 py-2.5"
|
||||||
|
>
|
||||||
|
Download PDF
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<CalendarDays size={18} />}
|
||||||
|
className="rounded-xl px-4 py-2.5"
|
||||||
|
>
|
||||||
|
Add to Calendar
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Loading state */}
|
{/* Loading state */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
Reference in New Issue
Block a user