UPDATE
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user