This commit is contained in:
2026-04-28 05:37:09 +03:30
parent f4e60f51a6
commit 2703e6d85e
3 changed files with 863 additions and 17 deletions
@@ -142,6 +142,31 @@ interface ApiResponse<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> {
const res = await promise;
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 Paper from "@mui/material/Paper";
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 StepContent from "@mui/material/StepContent";
import StepLabel from "@mui/material/StepLabel";
@@ -28,6 +36,8 @@ import {
type CropOption,
type FertilizationAlternativeRecommendation,
type FertilizationNutrientItem,
type FertilizationRecommendationHistoryItem,
type FertilizationRecommendationHistoryPagination,
type FertilizationRecommendationResult,
type GrowthStage,
} from "@/libs/api/services/fertilizationRecommendationService";
@@ -93,6 +103,29 @@ const formatUnitLabel = (unit: string) => {
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() {
const t = useTranslations("fertilization");
const theme = useTheme();
@@ -116,6 +149,24 @@ export default function SmartFertilizationRecommendation() {
const [statusMessage, setStatusMessage] = useState<string | null>(null);
const [reasoningExpanded, setReasoningExpanded] = useState(false);
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({
isOpen: false,
title: "",
@@ -170,6 +221,33 @@ export default function SmartFertilizationRecommendation() {
.finally(() => setConfigLoading(false));
}, [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 () => {
if (!selectedCrop || !growthStage || !farmUuid) return;
@@ -295,6 +373,30 @@ export default function SmartFertilizationRecommendation() {
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 (
<Box
className="min-bs-screen"
@@ -997,10 +1099,164 @@ export default function SmartFertilizationRecommendation() {
{requestError}
</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
elevation={0}
className="mb-6 animate-fade-in"
@@ -8,7 +8,37 @@ import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
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 {
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 type {
CropOption,
@@ -65,6 +95,105 @@ const getErrorMessage = (error: unknown, fallback: string) =>
? error.message
: 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() {
const t = useTranslations("irrigation");
const theme = useTheme();
@@ -78,7 +207,10 @@ export default function SmartIrrigationRecommendation() {
const [selectedGrowthStage, setSelectedGrowthStage] = useState<string | 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 [loading, setLoading] = useState(false);
const [requestError, setRequestError] = useState<string | null>(null);
@@ -87,9 +219,12 @@ export default function SmartIrrigationRecommendation() {
const primaryLight = theme.palette.primary.light;
const primaryDark = theme.palette.primary.dark;
const paperBg = theme.palette.background.paper;
const completedTaskCount = completedTasks.length;
useEffect(() => {
setPlan(null);
setIrrigationPlan(null);
setFertilizerPlan(null);
setCompletedTasks([]);
setWaterBalance(null);
setRequestError(null);
setSelectedCrop(null);
@@ -132,7 +267,9 @@ export default function SmartIrrigationRecommendation() {
const handleGenerate = async () => {
if (!selectedCrop || !farmUuid) return;
setLoading(true);
setPlan(null);
setIrrigationPlan(null);
setFertilizerPlan(null);
setCompletedTasks([]);
setWaterBalance(null);
setRequestError(null);
setStatusMessage(t("generating"));
@@ -170,7 +307,8 @@ export default function SmartIrrigationRecommendation() {
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);
return;
@@ -180,10 +318,13 @@ export default function SmartIrrigationRecommendation() {
throw new Error(t("errors.generateFailed"));
}
setPlan(recommendation.plan);
setIrrigationPlan(recommendation.plan);
setFertilizerPlan(MOCK_FERTILIZER_PLAN);
setWaterBalance(recommendation.water_balance ?? null);
} catch (error) {
setPlan(null);
setIrrigationPlan(null);
setFertilizerPlan(null);
setCompletedTasks([]);
setWaterBalance(null);
setRequestError(getErrorMessage(error, t("errors.generateFailed")));
} finally {
@@ -193,11 +334,18 @@ export default function SmartIrrigationRecommendation() {
};
const moistureLevelValue =
typeof plan?.moistureLevel === "number"
? plan.moistureLevel
: Number(plan?.moistureLevel);
typeof irrigationPlan?.moistureLevel === "number"
? irrigationPlan.moistureLevel
: Number(irrigationPlan?.moistureLevel);
const hasNumericMoistureLevel = Number.isFinite(moistureLevelValue);
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) => {
setSelectedCrop((prev) => {
@@ -380,8 +528,35 @@ export default function SmartIrrigationRecommendation() {
</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) */}
{plan && (
{activeTab === 0 && irrigationPlan && (
<Box className="mb-6 animate-fade-in">
<Card
elevation={0}
@@ -470,7 +645,7 @@ export default function SmartIrrigationRecommendation() {
color="primary.main"
className="mbe-1"
>
{String(plan.moistureLevel)}
{String(irrigationPlan.moistureLevel)}
</Typography>
<Typography variant="caption" color="text.secondary">
{t("result.moistureLevel")}
@@ -483,17 +658,17 @@ export default function SmartIrrigationRecommendation() {
<ResultRow
icon="tabler-calendar-week"
label={t("result.frequency")}
value={`${plan.frequencyPerWeek} ${t("result.timesPerWeek")}`}
value={`${irrigationPlan.frequencyPerWeek} ${t("result.timesPerWeek")}`}
/>
<ResultRow
icon="tabler-clock"
label={t("result.duration")}
value={`${plan.durationMinutes} ${t("result.minutes")}`}
value={`${irrigationPlan.durationMinutes} ${t("result.minutes")}`}
/>
<ResultRow
icon="tabler-sunrise"
label={t("result.bestTime")}
value={plan.bestTimeOfDay}
value={irrigationPlan.bestTimeOfDay}
/>
</Box>
@@ -535,7 +710,7 @@ export default function SmartIrrigationRecommendation() {
</Box>
)}
{plan.warning && (
{irrigationPlan.warning && (
<Box
className="mt-4 p-4 rounded-2xl"
sx={{
@@ -556,7 +731,7 @@ export default function SmartIrrigationRecommendation() {
{t("result.smartWarning")}
</Typography>
<Typography variant="body2" color="text.secondary">
{plan.warning}
{irrigationPlan.warning}
</Typography>
</Box>
</Box>
@@ -567,6 +742,366 @@ export default function SmartIrrigationRecommendation() {
</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 && (
<Card