diff --git a/docs/yield_harvest_ai_integration.md b/docs/yield_harvest_ai_integration.md new file mode 100644 index 0000000..1dffd48 --- /dev/null +++ b/docs/yield_harvest_ai_integration.md @@ -0,0 +1,40 @@ +# Yield & Harvest AI Integration + +این سند خلاصه قرارداد فرانت برای ماژول `yield-harvest` است. + +## اصل اصلی + +برای تمام endpointهای farm-based در این ماژول، فرانت فقط باید `farm_uuid` را ارسال کند. + +فرانت **نباید** این فیلدها را بفرستد: + +- `plant_name` +- `crop_name` +- هر context دیگری که backend از مزرعه استخراج می کند + +`plant_name` فقط در backend از روی مزرعه استخراج می شود و سپس backend آن را برای AI ارسال می کند. + +## endpointهای متصل شده در فرانت + +- `POST /api/yield-harvest/current-farm-chart/` +- `POST /api/yield-harvest/growth/` +- `GET /api/yield-harvest/growth/{task_id}/status/` +- `POST /api/yield-harvest/harvest-prediction/` +- `POST /api/yield-harvest/yield-prediction/` +- `GET /api/yield-harvest/yield-harvest-summary/` + +## وضعیت فعلی فرانت + +فایل `src/libs/api/services/yieldHarvestService.ts` فقط با `farm_uuid` به endpointهای farm-based درخواست می زند. + +نمونه payload ارسالی از فرانت: + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111" +} +``` + +## نکته مهم + +حتی اگر responseهای backend شامل `plant_name` باشند، این فیلد فقط برای نمایش یا logging است و از سمت فرانت در request ارسال نمی شود. diff --git a/src/app/(dashboard)/(private)/yield-harvest/BACKEND_REQUIREMENTS.md b/src/app/(dashboard)/(private)/yield-harvest/BACKEND_REQUIREMENTS.md new file mode 100644 index 0000000..e5b72ee --- /dev/null +++ b/src/app/(dashboard)/(private)/yield-harvest/BACKEND_REQUIREMENTS.md @@ -0,0 +1,709 @@ +# Yield & Harvest Backend Requirements + +این سند مشخص می‌کند صفحه `yield-harvest` برای کامل شدن تمام کارت‌ها چه داده‌ای از بک‌اند نیاز دارد، هر فیلد دقیقاً کجا مصرف می‌شود، و چه بخش‌هایی هنوز در فرانت mock هستند. + +## هدف + +در حال حاضر صفحه `yield-harvest` از این فایل‌ها ساخته می‌شود: + +- `src/views/dashboards/farm/PlantProductionPage.tsx` +- `src/views/dashboards/farm/YieldHarvestPageWrapper.tsx` +- `src/views/dashboards/farm/YieldSeasonHighlightsCard.tsx` +- `src/views/dashboards/farm/FarmOverviewKPIs.tsx` +- `src/views/dashboards/farm/HarvestPredictionCard.tsx` +- `src/views/dashboards/farm/HarvestReadinessZonesCard.tsx` +- `src/views/dashboards/farm/YieldQualityBandsCard.tsx` +- `src/views/dashboards/farm/HarvestOperationsCard.tsx` +- `src/views/dashboards/farm/YieldPredictionChart.tsx` + +هدف این سند این است که تیم بک‌اند دقیقاً بداند: + +- هر کارت چه payloadی می‌خواهد +- کدام فیلدها اجباری هستند +- کدام مقادیر باید `number` باشند و کدام‌ها `string` +- چه endpointی بهتر است این اطلاعات را برگرداند + +--- + +## وضعیت فعلی فرانت + +### داده‌هایی که همین الان از API خوانده می‌شوند + +فرانت الان از `src/libs/api/services/yieldHarvestService.ts` این endpoint را صدا می‌زند: + +`GET /api/yield-harvest/summary/?farm_uuid=` + +و فقط این سه بخش را map می‌کند: + +- `yield_prediction` +- `harvest_prediction_card` +- `yield_prediction_chart` + +### داده‌هایی که هنوز mock هستند + +این کارت‌ها هنوز با داده mock رندر می‌شوند و برای production باید از بک‌اند بیایند: + +- `YieldSeasonHighlightsCard` +- `HarvestReadinessZonesCard` +- `YieldQualityBandsCard` +- `HarvestOperationsCard` + +### نکته مهم درباره Plant Simulator + +کامپوننت `src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx` فعلاً self-contained است و برای نمایش فعلی به API نیاز ندارد. +پس اگر هدف فعلی فقط کامل شدن کارت‌های اطلاعاتی صفحه است، برای simulator نیازی به endpoint جدید نیست. + +--- + +## پیشنهاد اصلی + +بهترین راه این است که همان endpoint فعلی صفحه توسعه پیدا کند و تمام data blockهای موردنیاز را یکجا برگرداند: + +`GET /api/yield-harvest/summary/` + +### query params + +- `farm_uuid` - اجباری + +### query params پیشنهادی برای آینده + +- `season_year` - برای انتخاب فصل یا سال زراعی +- `crop_name` - اگر یک مزرعه چند محصول داشته باشد +- `lang` - اگر بخواهید backend متن‌های آماده UI را چندزبانه برگرداند + +--- + +## payload کامل پیشنهادی + +نمونه response کامل که تمام کارت‌های فعلی صفحه را پوشش می‌دهد: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "season_highlights_card": { + "title": "اتاق فرمان برداشت این فصل", + "subtitle": "خلاصه سریع وضعیت عملکرد، کیفیت و بهترین پنجره فروش برای مزرعه انتخاب شده.", + "seasonLabel": "فصل 1404", + "badges": ["کیفیت ممتاز", "آماده بسته بندی", "ریسک پایین"], + "spotlight": { + "title": "پنجره طلایی فروش", + "value": "3 روز اول بعد از برداشت", + "caption": "در این بازه برآورد قیمت فروش حدود 8٪ بهتر از میانگین هفتگی است." + }, + "metrics": [ + { + "label": "سطح قابل برداشت", + "value": "18.6 هکتار", + "caption": "4 قطعه در اولویت نخست قرار دارند.", + "avatarIcon": "tabler-map-2", + "avatarColor": "success" + }, + { + "label": "گرید ممتاز", + "value": "46%", + "caption": "بالاترین سهم کیفیت مربوط به قطعه A2 است.", + "avatarIcon": "tabler-rosette-discount-check", + "avatarColor": "warning" + }, + { + "label": "درآمد هدف", + "value": "1.84 میلیارد", + "caption": "با فرض فروش در بازه پیشنهادی مدل.", + "avatarIcon": "tabler-cash-banknote", + "avatarColor": "primary" + } + ] + }, + "yield_prediction": { + "kpis": [ + { + "id": "predicted-yield", + "title": "عملکرد پیش بینی شده", + "subtitle": "فصل جاری", + "stats": "42.8 تن", + "avatarColor": "primary", + "avatarIcon": "tabler-chart-arcs", + "chipText": "+12%", + "chipColor": "success" + }, + { + "id": "harvest-readiness", + "title": "آمادگی برداشت", + "subtitle": "میانگین مزرعه", + "stats": "84%", + "avatarColor": "success", + "avatarIcon": "tabler-plant-2", + "chipText": "روی برنامه", + "chipColor": "success" + }, + { + "id": "quality-score", + "title": "امتیاز کیفیت", + "subtitle": "برآورد هوش مصنوعی", + "stats": "91/100", + "avatarColor": "info", + "avatarIcon": "tabler-stars", + "chipText": "+4 واحد", + "chipColor": "success" + }, + { + "id": "loss-risk", + "title": "ریسک افت محصول", + "subtitle": "آب و هوا و آفات", + "stats": "6.5%", + "avatarColor": "warning", + "avatarIcon": "tabler-alert-triangle", + "chipText": "پایین", + "chipColor": "warning" + } + ] + }, + "harvest_prediction_card": { + "dateFormatted": "28 شهریور", + "daysUntil": 18, + "description": "با توجه به روند رشد، الگوی آبیاری و وضعیت دمایی اخیر، این مزرعه در هفته آخر شهریور به نقطه ایده آل برداشت می رسد." + }, + "harvest_readiness_zones": { + "averageReadiness": "84%", + "blocks": [ + { + "name": "قطعه A1", + "cultivar": "گندم سیروان", + "readiness": 92, + "harvestDate": "26 شهریور", + "expectedYield": "12.4 تن", + "moisture": "11.8%" + }, + { + "name": "قطعه A2", + "cultivar": "گندم پیشگام", + "readiness": 87, + "harvestDate": "27 شهریور", + "expectedYield": "10.1 تن", + "moisture": "12.3%" + } + ] + }, + "yield_quality_bands": { + "bands": [ + { + "label": "گرید ممتاز", + "share": 46, + "volume": "19.7 تن", + "premium": "+18% قیمت", + "color": "#2e7d32" + }, + { + "label": "گرید درجه یک", + "share": 34, + "volume": "14.5 تن", + "premium": "+9% قیمت", + "color": "#0288d1" + }, + { + "label": "گرید فرآوری", + "share": 20, + "volume": "8.6 تن", + "premium": "فروش پایه", + "color": "#ed6c02" + } + ], + "stats": [ + { "label": "میانگین بریکس", "value": "14.8" }, + { "label": "یکنواختی دانه", "value": "89%" }, + { "label": "ضایعات قابل انتظار", "value": "2.1%" }, + { "label": "پتانسیل صادرات", "value": "بالا" } + ] + }, + "harvest_operations_card": { + "summary": "اگر برداشت از قطعات A1 و A2 در دو شیفت اول انجام شود، کیفیت ممتاز حفظ می شود و فشار روی مرحله سورتینگ متعادل می ماند.", + "steps": [ + { + "title": "برداشت قطعات اولویت دار", + "note": "تمرکز ابتدا روی A1 و سپس A2 باشد تا گرید ممتاز در دمای پایین صبح جمع آوری شود.", + "status": "امروز", + "statusColor": "success" + }, + { + "title": "سورت و تفکیک بر اساس کیفیت", + "note": "محصول ممتاز از جریان فرآوری جدا شود تا فروش با قیمت پریمیوم حفظ شود.", + "status": "بعد از برداشت", + "statusColor": "primary" + }, + { + "title": "انتقال سریع به انبار خنک", + "note": "انتقال نهایی حداکثر تا 6 ساعت پس از برداشت انجام شود.", + "status": "ضروری", + "statusColor": "warning" + } + ], + "outputs": [ + { "label": "شیفت پیشنهادی", "value": "2 شیفت" }, + { "label": "ظرفیت سورتینگ", "value": "15 تن/روز" }, + { "label": "نیروی مورد نیاز", "value": "12 نفر" }, + { "label": "مدت تا ارسال", "value": "6 ساعت" } + ] + }, + "yield_prediction_chart": { + "categories": ["فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان"], + "series": [ + { + "name": "سال قبل", + "data": [9, 11, 13, 16, 19, 24, 28, 31] + }, + { + "name": "سال جاری", + "data": [10, 12, 15, 18, 23, 29, 34, 39] + } + ], + "summary": [ + { + "title": "بیشترین خروجی پیش بینی شده", + "subtitle": "بهترین ماه برداشت", + "amount": "39 تن", + "avatarColor": "success", + "avatarIcon": "tabler-trending-up" + }, + { + "title": "رشد این فصل", + "subtitle": "نسبت به سال قبل", + "amount": "+11.2 تن", + "avatarColor": "primary", + "avatarIcon": "tabler-chart-line" + } + ] + } + } +} +``` + +--- + +## توضیح کامل هر کارت + +## 1) Season Highlights Card + +کامپوننت: `src/views/dashboards/farm/YieldSeasonHighlightsCard.tsx` + +### حداقل داده لازم برای رندر + +- `title` +- `subtitle` +- `spotlight` +- `metrics[]` + +اگر `title` خالی باشد یا `spotlight` وجود نداشته باشد یا `metrics` خالی باشد، کارت رندر نمی‌شود. + +### فیلدها + +- `title`: تیتر اصلی کارت +- `subtitle`: توضیح متنی زیر تیتر +- `seasonLabel`: چیپ فصل، مثل `فصل 1404` +- `badges[]`: لیبل‌های کوتاه وضعیت +- `spotlight.title`: عنوان بخش برجسته +- `spotlight.value`: مقدار اصلی بخش برجسته +- `spotlight.caption`: توضیح تکمیلی +- `metrics[]`: آرایه کارت‌های کوچک پایین + +### ساختار `metrics[]` + +هر آیتم این فیلدها را نیاز دارد: + +- `label`: عنوان metric +- `value`: مقدار اصلی metric +- `caption`: توضیح کوتاه زیر metric +- `avatarIcon`: نام icon از Tabler +- `avatarColor`: یکی از این مقادیر: + - `primary` + - `success` + - `info` + - `warning` + +### نکته بک‌اند + +این کارت فعلاً به API وصل نیست و باید یا: + +- به همین summary endpoint اضافه شود +- یا یک endpoint جدا برای page header/hero داشته باشد + +پیشنهاد بهتر: در همان `summary` برگردد. + +--- + +## 2) KPI Row + +کامپوننت: `src/views/dashboards/farm/FarmOverviewKPIs.tsx` + +### ساختار موردنیاز + +```json +{ + "yield_prediction": { + "kpis": [ + { + "id": "predicted-yield", + "title": "عملکرد پیش بینی شده", + "subtitle": "فصل جاری", + "stats": "42.8 تن", + "avatarColor": "primary", + "avatarIcon": "tabler-chart-arcs", + "chipText": "+12%", + "chipColor": "success" + } + ] + } +} +``` + +### فیلدهای هر KPI + +- `id` - اجباری و یکتا +- `title` - اجباری +- `subtitle` - اجباری +- `stats` - اجباری +- `avatarColor` - اختیاری +- `avatarIcon` - اختیاری +- `chipText` - اختیاری +- `chipColor` - اختیاری + +### مقدارهای مجاز + +- `avatarColor` بهتر است یکی از این‌ها باشد: + - `primary` + - `secondary` + - `success` + - `info` + - `warning` +- `chipColor` بهتر است یکی از این‌ها باشد: + - `success` + - `warning` + +### نکته مهم + +فرانت برای نمایش کامل این ردیف، `yield_prediction.kpis[]` می‌خواهد. +اگر بک‌اند فقط یک object تکی برگرداند، فقط یک کارت KPI نمایش داده می‌شود. + +--- + +## 3) Harvest Prediction Card + +کامپوننت: `src/views/dashboards/farm/HarvestPredictionCard.tsx` + +### ساختار موردنیاز + +```json +{ + "harvest_prediction_card": { + "dateFormatted": "28 شهریور", + "daysUntil": 18, + "description": "..." + } +} +``` + +### فیلدها + +- `dateFormatted` - تاریخ برداشت به صورت آماده نمایش +- `daysUntil` - عدد با type `number` +- `description` - توضیح متنی + +### رفتار UI + +- اگر `daysUntil > 0` باشد، چیپ تعداد روز باقی‌مانده نمایش داده می‌شود +- اگر `daysUntil` صفر یا منفی باشد، چیپ نمایش داده نمی‌شود + +### نکته بک‌اند + +- `daysUntil` حتماً `number` باشد، نه string +- `dateFormatted` چون مستقیم در UI چاپ می‌شود بهتر است آماده نمایش باشد + +--- + +## 4) Harvest Readiness Zones Card + +کامپوننت: `src/views/dashboards/farm/HarvestReadinessZonesCard.tsx` + +### ساختار موردنیاز + +```json +{ + "harvest_readiness_zones": { + "averageReadiness": "84%", + "blocks": [ + { + "name": "قطعه A1", + "cultivar": "گندم سیروان", + "readiness": 92, + "harvestDate": "26 شهریور", + "expectedYield": "12.4 تن", + "moisture": "11.8%" + } + ] + } +} +``` + +### فیلدهای سطح کارت + +- `averageReadiness` - مقدار آماده نمایش برای چیپ بالای کارت +- `blocks[]` - آرایه قطعات مزرعه + +### فیلدهای هر block + +- `name` - نام قطعه +- `cultivar` - رقم یا cultivar +- `readiness` - درصد آمادگی با type `number` +- `harvestDate` - تاریخ پیشنهادی برداشت +- `expectedYield` - عملکرد پیش‌بینی‌شده به صورت string +- `moisture` - رطوبت محصول/دانه به صورت string + +### نکات مهم + +- اگر `blocks.length === 0` باشد کارت اصلاً رندر نمی‌شود +- `readiness` باید بین `0..100` باشد چون در progress bar استفاده می‌شود +- رنگ چیپ تاریخ برداشت از روی `readiness` در فرانت تعیین می‌شود: + - `>= 85` -> `success` + - `>= 70` -> `warning` + - کمتر از `70` -> `info` + +--- + +## 5) Yield Quality Bands Card + +کامپوننت: `src/views/dashboards/farm/YieldQualityBandsCard.tsx` + +### ساختار موردنیاز + +```json +{ + "yield_quality_bands": { + "bands": [ + { + "label": "گرید ممتاز", + "share": 46, + "volume": "19.7 تن", + "premium": "+18% قیمت", + "color": "#2e7d32" + } + ], + "stats": [ + { + "label": "میانگین بریکس", + "value": "14.8" + } + ] + } +} +``` + +### فیلدهای `bands[]` + +- `label` - نام گرید کیفیت +- `share` - درصد سهم از کل برداشت با type `number` +- `volume` - حجم محصول این گرید +- `premium` - اثر قیمتی یا برچسب تجاری +- `color` - رنگ نوار، ترجیحاً hex + +### فیلدهای `stats[]` + +- `label` +- `value` + +### نکات مهم + +- اگر `bands.length === 0` باشد کارت رندر نمی‌شود +- `share` باید `number` باشد و بهتر است در بازه `0..100` باشد +- `color` باید color-valid باشد چون مستقیم برای style استفاده می‌شود + +--- + +## 6) Harvest Operations Card + +کامپوننت: `src/views/dashboards/farm/HarvestOperationsCard.tsx` + +### ساختار موردنیاز + +```json +{ + "harvest_operations_card": { + "summary": "....", + "steps": [ + { + "title": "برداشت قطعات اولویت دار", + "note": "....", + "status": "امروز", + "statusColor": "success" + } + ], + "outputs": [ + { + "label": "شیفت پیشنهادی", + "value": "2 شیفت" + } + ] + } +} +``` + +### فیلدها + +- `summary` - متن خلاصه بالای کارت +- `steps[]` - لیست مراحل عملیاتی +- `outputs[]` - خروجی‌های خلاصه پایین کارت + +### فیلدهای هر step + +- `title` +- `note` +- `status` +- `statusColor` + +### مقدارهای مجاز `statusColor` + +- `primary` +- `success` +- `info` +- `warning` + +### فیلدهای هر output + +- `label` +- `value` + +### نکات مهم + +- اگر `steps.length === 0` باشد کارت رندر نمی‌شود +- `summary` اختیاری است ولی بهتر است همیشه مقدار داشته باشد + +--- + +## 7) Yield Prediction Chart + +کامپوننت: `src/views/dashboards/farm/YieldPredictionChart.tsx` + +### ساختار موردنیاز + +```json +{ + "yield_prediction_chart": { + "categories": ["فروردین", "اردیبهشت", "خرداد"], + "series": [ + { + "name": "سال قبل", + "data": [9, 11, 13] + }, + { + "name": "سال جاری", + "data": [10, 12, 15] + } + ], + "summary": [ + { + "title": "بیشترین خروجی پیش بینی شده", + "subtitle": "بهترین ماه برداشت", + "amount": "39 تن", + "avatarColor": "success", + "avatarIcon": "tabler-trending-up" + } + ] + } +} +``` + +### فیلدهای chart + +- `categories[]` - labelهای محور X +- `series[]` - سری‌های نمودار +- `summary[]` - لیست خلاصه پایین نمودار + +### فیلدهای هر series + +- `name` +- `data[]` - فقط `number` + +### فیلدهای هر summary item + +- `title` +- `subtitle` +- `amount` +- `avatarColor` +- `avatarIcon` + +### نکات مهم + +- اگر `series.length === 0` باشد خود chart رندر نمی‌شود +- `categories.length` باید با طول `data[]` هر series سازگار باشد +- مقادیر `data[]` حتماً عددی باشند، نه string + +--- + +## قوانین مشترک برای بک‌اند + +برای اینکه این صفحه بدون mapping پیچیده و bug کار کند، این قواعد را رعایت کنید: + +- همه درصدها و مقادیری که در chart یا progress bar استفاده می‌شوند باید `number` باشند +- همه متن‌های آماده نمایش مثل `12.4 تن`، `26 شهریور`، `11.8%` می‌توانند `string` باشند +- color fieldها باید مقدار معتبر UI داشته باشند +- icon fieldها باید از icon nameهای معتبر Tabler باشند +- اگر API wrapper دارید، ساختار `code/msg/data` مشکلی ندارد +- همه timestampها در صورت نیاز آینده بهتر است ISO 8601 باشند +- داده‌ها باید بر اساس `farm_uuid` فیلتر شوند + +--- + +## حداقل داده‌ای که برای حذف کامل mockها لازم است + +برای اینکه این صفحه دیگر به هیچ mockی وابسته نباشد، بک‌اند باید این 7 بلوک را برگرداند: + +- `season_highlights_card` +- `yield_prediction` +- `harvest_prediction_card` +- `harvest_readiness_zones` +- `yield_quality_bands` +- `harvest_operations_card` +- `yield_prediction_chart` + +اگر فقط این سه بخش برگردند: + +- `yield_prediction` +- `harvest_prediction_card` +- `yield_prediction_chart` + +باز هم صفحه ناقص می‌ماند چون سه کارت تحلیلی و کارت hero هنوز از mock استفاده می‌کنند. + +--- + +## نکات اتصال فرانت + +بعد از آماده شدن بک‌اند، فرانت باید این mappingها را کامل کند: + +- `season_highlights_card` -> `YieldSeasonHighlightsCard` +- `yield_prediction.kpis` -> `FarmOverviewKPIs` +- `harvest_prediction_card` -> `HarvestPredictionCard` +- `harvest_readiness_zones` -> `HarvestReadinessZonesCard` +- `yield_quality_bands` -> `YieldQualityBandsCard` +- `harvest_operations_card` -> `HarvestOperationsCard` +- `yield_prediction_chart` -> `YieldPredictionChart` + +### نکته فنی مهم + +در `src/libs/api/services/yieldHarvestService.ts` الان `yield_prediction` به شکل یک کارت تکی normalize می‌شود. +اگر بک‌اند `yield_prediction.kpis[]` برگرداند، بهتر است این service به‌روزرسانی شود تا کل آرایه KPIها بدون wrap اضافه عبور کند. + +--- + +## جمع بندی نهایی + +اگر بک‌اند یک summary کامل برای صفحه `yield-harvest` برگرداند، فرانت می‌تواند تمام کارت‌های صفحه را بدون mock و بدون محاسبه‌ی اضافه سمت کلاینت رندر کند. + +بهترین قرارداد برای این صفحه: + +- یک endpoint summary +- یک payload شامل 7 بلوک اصلی +- typeهای عددی برای chart/progress +- stringهای آماده نمایش برای متن‌ها، تاریخ‌ها و واحدها + +این ساختار هم برای وضعیت فعلی فرانت کافی است، هم برای توسعه بعدی صفحه. diff --git a/src/libs/api/services/yieldHarvestService.ts b/src/libs/api/services/yieldHarvestService.ts index 4ac8f42..3d04ffe 100644 --- a/src/libs/api/services/yieldHarvestService.ts +++ b/src/libs/api/services/yieldHarvestService.ts @@ -2,10 +2,108 @@ import { apiClient } from '../client' const PREFIX = '/api/yield-harvest' +type GenericRecord = Record +type StatusColor = 'primary' | 'success' | 'info' | 'warning' + export interface YieldHarvestSummary { - yield_prediction?: Record - yieldPredictionChart?: Record - harvestPredictionCard?: Record + seasonHighlightsCard?: GenericRecord + yield_prediction?: GenericRecord + yieldPredictionChart?: GenericRecord + harvestPredictionCard?: GenericRecord + harvestReadinessZones?: GenericRecord + yieldQualityBands?: GenericRecord + harvestOperationsCard?: GenericRecord + raw?: GenericRecord +} + +export interface CurrentFarmChartResponse { + farm_uuid?: string | null + plant_name?: string | null + engine?: string | null + model_name?: string | null + scenario_id?: number | null + simulation_warning?: string | null + categories?: string[] + series?: Array<{ + name: string + key?: string + data: number[] + }> + summary?: Array<{ + title: string + subtitle?: string + amount: number + unit?: string + avatarColor?: string + avatarIcon?: string + }> + current_state?: GenericRecord + metrics?: GenericRecord + daily_output?: Array +} + +export interface GrowthStartResponse { + task_id: string + status_url?: string + plant_name?: string | null +} + +export interface GrowthStatusResponse { + task_id: string + status: 'PENDING' | 'PROGRESS' | 'SUCCESS' | 'FAILURE' + message?: string + progress?: { + current?: number + total?: number + percent?: number + } + result?: GenericRecord + error?: string +} + +export interface HarvestPredictionResponse { + date?: string + dateFormatted?: string + daysUntil?: number + description?: string + optimalWindowStart?: string + optimalWindowEnd?: string + gddDetails?: GenericRecord +} + +export interface YieldPredictionResponse { + farm_uuid?: string + plant_name?: string | null + predictedYieldTons?: number + predictedYieldRaw?: number + predicted_yield_tons?: number + predicted_yield_raw?: number + unit?: string + sourceUnit?: string + source_unit?: string + simulationEngine?: string | null + simulationModel?: string | null + scenarioId?: number | null + simulation_engine?: string | null + simulation_model?: string | null + scenario_id?: number | null + simulationWarning?: string | null + simulation_warning?: string | null + supportingMetrics?: GenericRecord + supporting_metrics?: GenericRecord +} + +export interface YieldHarvestDashboardData { + summary: YieldHarvestSummary + currentFarmChart?: CurrentFarmChartResponse + harvestPrediction?: HarvestPredictionResponse + yieldPrediction?: YieldPredictionResponse +} + +interface SummaryOptions { + seasonYear?: number + cropName?: string + includeNarrative?: boolean } interface ApiResponse { @@ -18,29 +116,857 @@ interface ApiResponse { function extract(res: ApiResponse | T): T { if (res && typeof res === 'object') { if ('data' in res) return (res as ApiResponse).data - if ('result' in res) return (res as ApiResponse).result as T + if ('result' in res) return (res as unknown as ApiResponse).result as T } return res as T } -function toKpiCard(card?: Record): Record { - if (!card || typeof card !== 'object') return {} +function asRecord(value: unknown): GenericRecord | undefined { + return value && typeof value === 'object' && !Array.isArray(value) + ? (value as GenericRecord) + : undefined +} - return { kpis: [card] } +function asArray(value: unknown): T[] { + return Array.isArray(value) ? (value as T[]) : [] +} + +function toNumber(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) return value + if (typeof value === 'string') { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : null + } + + return null +} + +function buildFarmPayload(farmUuid: string) { + return { farm_uuid: farmUuid } +} + +function buildSummaryQuery(farmUuid: string, options?: SummaryOptions) { + const params = new URLSearchParams({ farm_uuid: farmUuid }) + + if (options?.seasonYear !== undefined) { + params.set('season_year', String(options.seasonYear)) + } + + if (options?.cropName) { + params.set('crop_name', options.cropName) + } + + if (options?.includeNarrative !== undefined) { + params.set('include_narrative', String(options.includeNarrative)) + } + + return params.toString() +} + +function formatNumber(value: number, maximumFractionDigits = 1) { + return new Intl.NumberFormat('fa-IR', { maximumFractionDigits }).format(value) +} + +function formatPercent(value: unknown, maximumFractionDigits = 0) { + const amount = toNumber(value) + if (amount === null) return '-' + + return `${formatNumber(amount, maximumFractionDigits)}%` +} + +function formatCurrency(value: unknown) { + const amount = toNumber(value) + if (amount === null) return '-' + + return `${formatNumber(amount, 0)} ریال` +} + +function formatDaysUntil(value: unknown) { + const amount = toNumber(value) + if (amount === null) return 'نامشخص' + if (amount <= 0) return 'آماده برداشت' + + return `${formatNumber(amount, 0)} روز مانده` +} + +function getPredictedYieldTons(value: unknown): number | null { + const record = asRecord(value) + if (!record) return toNumber(value) + + const direct = + toNumber(record.predictedYieldTons) ?? + toNumber(record.predicted_yield_tons) ?? + toNumber(record.total_predicted_yield) + + if (direct !== null) return direct + + const rawValue = + toNumber(record.predictedYieldRaw) ?? toNumber(record.predicted_yield_raw) + const sourceUnit = String(record.sourceUnit ?? record.source_unit ?? '') + const displayUnit = String(record.unit ?? record.yield_unit ?? '') + + if ( + rawValue !== null && + sourceUnit.toLowerCase().includes('kg') && + (displayUnit.includes('تن') || displayUnit.toLowerCase().includes('ton')) + ) { + return rawValue / 1000 + } + + return null +} + +function getStatusColor(value: unknown): StatusColor { + const normalized = String(value ?? '').toLowerCase() + + if ( + normalized.includes('success') || + normalized.includes('complete') || + normalized.includes('today') || + normalized.includes('ready') || + normalized.includes('امروز') || + normalized.includes('آماده') + ) { + return 'success' + } + + if ( + normalized.includes('warning') || + normalized.includes('urgent') || + normalized.includes('ضروری') || + normalized.includes('هشدار') + ) { + return 'warning' + } + + if ( + normalized.includes('info') || + normalized.includes('planned') || + normalized.includes('برنامه') + ) { + return 'info' + } + + return 'primary' +} + +function toTimestampLabel(timestamp: unknown) { + const numericTimestamp = toNumber(timestamp) + if (numericTimestamp === null) return String(timestamp ?? '') + + return new Date(numericTimestamp).toLocaleDateString('fa-IR', { + month: 'short', + day: 'numeric', + }) +} + +function normalizeSeriesChart(chartData?: GenericRecord) { + if (!chartData) return {} + + const directCategories = asArray(chartData.categories) + const directSeries = asArray(chartData.series) + + if ( + directCategories.length > 0 && + directSeries.every((item) => Array.isArray(item.data)) + ) { + return { + categories: directCategories, + series: directSeries.map((item) => ({ + name: String(item.name ?? 'Series'), + data: asArray(item.data).map((point) => toNumber(point) ?? 0), + })), + } + } + + if (directSeries.length === 0) return {} + + let derivedCategories: string[] = [] + + const normalizedSeries = directSeries.map((item) => { + const points = asArray(item.data) + + if (points.length > 0 && Array.isArray(points[0])) { + const tuplePoints = points as Array<[unknown, unknown]> + + if (derivedCategories.length === 0) { + derivedCategories = tuplePoints.map(([timestamp]) => + toTimestampLabel(timestamp), + ) + } + + return { + name: String(item.name ?? 'Series'), + data: tuplePoints.map(([, value]) => toNumber(value) ?? 0), + } + } + + return { + name: String(item.name ?? 'Series'), + data: points.map((point) => toNumber(point) ?? 0), + } + }) + + return { + categories: derivedCategories, + series: normalizedSeries, + } +} + +function normalizeSeasonHighlightsCard( + raw: GenericRecord | undefined, + summaryData: GenericRecord, +) { + if (!raw) return undefined + + if (Array.isArray(raw.metrics) && asRecord(raw.spotlight)) { + return raw + } + + const title = String(raw.title ?? 'اتاق فرمان برداشت این فصل') + const subtitle = String( + raw.subtitle ?? + 'خلاصه سریع وضعیت عملکرد، کیفیت و بهترین بازه برداشت برای مزرعه انتخاب شده.', + ) + const seasonPredictedYield = getPredictedYieldTons(raw) + const summaryPredictedYield = getPredictedYieldTons( + asRecord(summaryData.yield_prediction), + ) + const totalPredictedYield = + summaryPredictedYield !== null && + (seasonPredictedYield === null || seasonPredictedYield === 0) + ? summaryPredictedYield + : seasonPredictedYield + const yieldUnit = String(raw.yield_unit ?? 'تن') + const daysUntilHarvest = + toNumber(raw.days_until_harvest) ?? + toNumber(asRecord(summaryData.harvest_prediction_card)?.days_until) + const targetHarvestDate = String( + raw.target_harvest_date ?? + asRecord(summaryData.harvest_prediction_card)?.harvest_date_formatted ?? + 'نامشخص', + ) + const averageReadiness = + toNumber(raw.average_readiness) ?? + toNumber(asRecord(summaryData.harvest_readiness_zones)?.averageReadiness) + const primaryQualityGrade = String(raw.primary_quality_grade ?? '') + const estimatedRevenue = raw.estimated_revenue + const soilType = String(raw.soil_type ?? '') + + const badges = [primaryQualityGrade, soilType, averageReadiness !== null ? `آمادگی ${formatPercent(averageReadiness)}` : ''] + .filter(Boolean) + + return { + title, + subtitle, + seasonLabel: raw.seasonLabel ?? (raw.season_year ? `فصل ${raw.season_year}` : ''), + badges, + spotlight: { + title: 'تاریخ هدف برداشت', + value: targetHarvestDate, + caption: + daysUntilHarvest === null + ? 'زمان برداشت از داده های فعلی مزرعه برآورد می شود.' + : `${formatNumber(daysUntilHarvest, 0)} روز تا برداشت باقی مانده است.`, + }, + metrics: [ + totalPredictedYield !== null + ? { + label: 'عملکرد پیش بینی شده', + value: `${formatNumber(totalPredictedYield)} ${yieldUnit}`, + caption: 'بر اساس خلاصه پیش بینی عملکرد این مزرعه.', + avatarIcon: 'tabler-chart-arcs', + avatarColor: 'primary', + } + : null, + averageReadiness !== null + ? { + label: 'میانگین آمادگی برداشت', + value: formatPercent(averageReadiness), + caption: 'برآورد میانگین readiness در سطح مزرعه.', + avatarIcon: 'tabler-plant-2', + avatarColor: 'success', + } + : null, + estimatedRevenue !== undefined + ? { + label: 'درآمد تخمینی', + value: formatCurrency(estimatedRevenue), + caption: 'برآورد درآمد بر اساس داده های فعلی summary.', + avatarIcon: 'tabler-cash-banknote', + avatarColor: 'warning', + } + : null, + ].filter(Boolean), + } +} + +function normalizeYieldPrediction( + raw: GenericRecord | undefined, + summaryData: GenericRecord, +) { + if (!raw) return undefined + + if (Array.isArray(raw.kpis)) return raw + + const kpis: GenericRecord[] = [] + const predictedYield = getPredictedYieldTons(raw) + const yieldUnit = String(raw.unit ?? 'تن') + const averageReadiness = + toNumber(asRecord(summaryData.harvest_readiness_zones)?.averageReadiness) ?? + toNumber(asRecord(summaryData.season_highlights_card)?.average_readiness) + const qualityScore = toNumber( + asRecord(summaryData.yield_quality_bands)?.quality_score, + ) + const daysUntilHarvest = + toNumber(asRecord(summaryData.harvest_prediction_card)?.days_until) ?? + toNumber(asRecord(summaryData.season_highlights_card)?.days_until_harvest) + const estimatedRevenue = toNumber( + asRecord(summaryData.season_highlights_card)?.estimated_revenue, + ) + + if (predictedYield !== null) { + kpis.push({ + id: 'predicted-yield', + title: 'عملکرد پیش بینی شده', + subtitle: 'پیش بینی عملکرد این مزرعه', + stats: `${formatNumber(predictedYield)} ${yieldUnit}`, + avatarColor: 'primary', + avatarIcon: 'tabler-chart-arcs', + chipText: raw.simulation_warning ? 'نیازمند بررسی' : 'به روز', + chipColor: raw.simulation_warning ? 'warning' : 'success', + }) + } + + if (averageReadiness !== null) { + kpis.push({ + id: 'harvest-readiness', + title: 'آمادگی برداشت', + subtitle: 'میانگین کل مزرعه', + stats: formatPercent(averageReadiness), + avatarColor: 'success', + avatarIcon: 'tabler-plant-2', + chipText: averageReadiness >= 80 ? 'روی برنامه' : 'در حال پایش', + chipColor: averageReadiness >= 80 ? 'success' : 'warning', + }) + } + + if (qualityScore !== null) { + kpis.push({ + id: 'quality-score', + title: 'امتیاز کیفیت', + subtitle: 'برآورد کیفیت محصول', + stats: formatNumber(qualityScore), + avatarColor: 'info', + avatarIcon: 'tabler-stars', + chipText: qualityScore >= 80 ? 'مطلوب' : 'متوسط', + chipColor: qualityScore >= 80 ? 'success' : 'warning', + }) + } + + if (daysUntilHarvest !== null) { + kpis.push({ + id: 'days-until-harvest', + title: 'روز تا برداشت', + subtitle: 'زمان باقیمانده تا پنجره برداشت', + stats: formatNumber(daysUntilHarvest, 0), + avatarColor: 'warning', + avatarIcon: 'tabler-calendar-event', + chipText: daysUntilHarvest <= 7 ? 'نزدیک' : 'برنامه ریزی', + chipColor: daysUntilHarvest <= 7 ? 'warning' : 'success', + }) + } else if (estimatedRevenue !== null) { + kpis.push({ + id: 'estimated-revenue', + title: 'درآمد تخمینی', + subtitle: 'بر پایه summary این مزرعه', + stats: formatCurrency(estimatedRevenue), + avatarColor: 'warning', + avatarIcon: 'tabler-cash-banknote', + chipText: 'تخمینی', + chipColor: 'warning', + }) + } + + return { ...raw, kpis } +} + +function normalizeHarvestPredictionCard(raw?: GenericRecord) { + if (!raw) return undefined + + return { + ...raw, + dateFormatted: + raw.dateFormatted ?? raw.harvest_date_formatted ?? raw.dateFormatted ?? raw.date, + daysUntil: toNumber(raw.daysUntil ?? raw.days_until) ?? 0, + description: raw.description ?? raw.explanation ?? '', + } +} + +function normalizeHarvestReadinessZones(raw?: GenericRecord) { + if (!raw) return undefined + + if (Array.isArray(raw.blocks)) return raw + + const blocks = asArray(raw.zones).map((zone, index) => ({ + name: String(zone.zoneLabel ?? zone.zoneId ?? `Zone ${index + 1}`), + cultivar: String(zone.status ?? raw.vegetationHealthClass ?? 'وضعیت مزرعه'), + readiness: toNumber(zone.readiness) ?? 0, + harvestDate: formatDaysUntil(zone.daysUntil), + expectedYield: + toNumber(zone.meanNdvi) !== null + ? `NDVI ${formatNumber(toNumber(zone.meanNdvi) ?? 0, 2)}` + : String(raw.summary ?? '-'), + moisture: + toNumber(raw.ndviTrend) !== null + ? `${toNumber(raw.ndviTrend)! >= 0 ? '+' : ''}${formatNumber(toNumber(raw.ndviTrend) ?? 0, 1)}%` + : String(raw.source ?? '-'), + })) + + return { + averageReadiness: + typeof raw.averageReadiness === 'string' + ? raw.averageReadiness + : formatPercent(raw.averageReadiness), + blocks, + } +} + +function normalizeYieldQualityBands(raw?: GenericRecord) { + if (!raw) return undefined + + if (Array.isArray(raw.bands)) return raw + + const palette = ['#2e7d32', '#0288d1', '#ed6c02', '#8e24aa'] + const bands = asArray(raw.grade_distribution).map((band, index) => ({ + label: String(band.label ?? band.grade ?? band.name ?? `گرید ${index + 1}`), + share: toNumber(band.share ?? band.percentage ?? band.percent) ?? 0, + volume: + band.volume !== undefined + ? String(band.volume) + : band.amount !== undefined + ? String(band.amount) + : '-', + premium: String(band.premium ?? band.note ?? raw.primary_quality_grade ?? 'برآورد کیفیت'), + color: String(band.color ?? palette[index % palette.length]), + })) + + const proteinContent = asRecord(raw.protein_content) + const moisturePercentage = asRecord(raw.moisture_percentage) + const stats = [ + proteinContent + ? { + label: 'درصد پروتئین', + value: String( + proteinContent.value ?? proteinContent.current ?? proteinContent.percent ?? '-', + ), + } + : null, + moisturePercentage + ? { + label: 'درصد رطوبت', + value: String( + moisturePercentage.value ?? + moisturePercentage.current ?? + moisturePercentage.percent ?? + '-', + ), + } + : null, + raw.quality_score !== undefined + ? { + label: 'امتیاز کیفیت', + value: formatNumber(toNumber(raw.quality_score) ?? 0), + } + : null, + raw.primary_quality_grade + ? { + label: 'گرید غالب', + value: String(raw.primary_quality_grade), + } + : null, + ].filter(Boolean) + + return { + bands, + stats, + } +} + +function normalizeHarvestOperationsCard(raw?: GenericRecord) { + if (!raw) return undefined + + const steps = asArray(raw.steps).map((step) => ({ + title: String(step.title ?? step.key ?? 'گام عملیاتی'), + note: String(step.note ?? ''), + status: String(step.status ?? step.phase_name ?? step.stage_label ?? 'برنامه ریزی'), + statusColor: getStatusColor(step.status), + })) + + const outputs = [ + raw.phase_name + ? { label: 'فاز رشد', value: String(raw.phase_name) } + : null, + raw.days_until_harvest !== undefined + ? { + label: 'روز تا برداشت', + value: formatNumber(toNumber(raw.days_until_harvest) ?? 0, 0), + } + : null, + raw.current_dvs !== undefined + ? { + label: 'DVS فعلی', + value: formatNumber(toNumber(raw.current_dvs) ?? 0, 2), + } + : null, + raw.rules_source + ? { label: 'منبع قواعد', value: String(raw.rules_source) } + : null, + ].filter(Boolean) + + return { + summary: String(raw.summary ?? ''), + steps, + outputs, + } +} + +function normalizeYieldPredictionChart( + raw: GenericRecord | undefined, + summaryData: GenericRecord, +) { + if (!raw) return undefined + + const chart = normalizeSeriesChart(raw) + const chartMeta = asRecord(raw.meta) + const predictedYield = + toNumber(asRecord(summaryData.yield_prediction)?.predicted_yield_tons) ?? + toNumber(asRecord(summaryData.yield_prediction)?.predictedYieldTons) + const unit = String( + asRecord(summaryData.yield_prediction)?.unit ?? chartMeta?.unit ?? 'تن', + ) + const engine = String(chartMeta?.simulation_engine ?? '') + + return { + ...chart, + summary: Array.isArray(raw.summary) + ? raw.summary + : [ + predictedYield !== null + ? { + title: 'عملکرد پیش بینی شده', + subtitle: 'جمع بندی مدل عملکرد', + amount: `${formatNumber(predictedYield)} ${unit}`, + avatarColor: 'success', + avatarIcon: 'tabler-trending-up', + } + : null, + engine + ? { + title: 'موتور شبیه سازی', + subtitle: 'منبع تحلیل نمودار', + amount: engine, + avatarColor: 'primary', + avatarIcon: 'tabler-chart-line', + } + : null, + ].filter(Boolean), + } +} + +function normalizeSummaryData(data: GenericRecord): YieldHarvestSummary { + const seasonHighlightsCard = normalizeSeasonHighlightsCard( + asRecord(data.season_highlights_card ?? data.seasonHighlightsCard), + data, + ) + const yieldPrediction = normalizeYieldPrediction( + asRecord(data.yield_prediction ?? data.yieldPrediction), + data, + ) + const harvestPredictionCard = normalizeHarvestPredictionCard( + asRecord(data.harvest_prediction_card ?? data.harvestPredictionCard), + ) + const harvestReadinessZones = normalizeHarvestReadinessZones( + asRecord(data.harvest_readiness_zones ?? data.harvestReadinessZones), + ) + const yieldQualityBands = normalizeYieldQualityBands( + asRecord(data.yield_quality_bands ?? data.yieldQualityBands), + ) + const harvestOperationsCard = normalizeHarvestOperationsCard( + asRecord(data.harvest_operations_card ?? data.harvestOperationsCard), + ) + const yieldPredictionChart = normalizeYieldPredictionChart( + asRecord(data.yield_prediction_chart ?? data.yieldPredictionChart), + data, + ) + + return { + seasonHighlightsCard, + yield_prediction: yieldPrediction, + harvestPredictionCard, + harvestReadinessZones, + yieldQualityBands, + harvestOperationsCard, + yieldPredictionChart, + raw: data, + } +} + +function enrichSummaryData( + summary: YieldHarvestSummary, + extras: { + currentFarmChart?: CurrentFarmChartResponse + harvestPrediction?: HarvestPredictionResponse + yieldPrediction?: YieldPredictionResponse + }, +): YieldHarvestSummary { + const raw = { ...(summary.raw ?? {}) } + + const baseSeason = asRecord( + raw.season_highlights_card ?? raw.seasonHighlightsCard, + ) + const baseYield = asRecord(raw.yield_prediction ?? raw.yieldPrediction) + const baseHarvest = asRecord( + raw.harvest_prediction_card ?? raw.harvestPredictionCard, + ) + const baseChart = asRecord( + raw.yield_prediction_chart ?? raw.yieldPredictionChart, + ) + + if (extras.yieldPrediction) { + const predictedYieldTons = getPredictedYieldTons(extras.yieldPrediction) + const predictedYieldRaw = + toNumber(extras.yieldPrediction.predictedYieldRaw) ?? + toNumber(extras.yieldPrediction.predicted_yield_raw) + const sourceUnit = String( + extras.yieldPrediction.sourceUnit ?? + extras.yieldPrediction.source_unit ?? + baseYield?.source_unit ?? + baseYield?.sourceUnit ?? + '', + ) + const simulationEngine = String( + extras.yieldPrediction.simulationEngine ?? + extras.yieldPrediction.simulation_engine ?? + baseYield?.simulation_engine ?? + baseYield?.simulationEngine ?? + '', + ) + const simulationModel = String( + extras.yieldPrediction.simulationModel ?? + extras.yieldPrediction.simulation_model ?? + baseYield?.simulation_model ?? + baseYield?.simulationModel ?? + '', + ) + const scenarioId = + extras.yieldPrediction.scenarioId ?? + extras.yieldPrediction.scenario_id ?? + baseYield?.scenario_id ?? + baseYield?.scenarioId + const simulationWarning = + extras.yieldPrediction.simulationWarning ?? + extras.yieldPrediction.simulation_warning ?? + baseYield?.simulation_warning ?? + baseYield?.simulationWarning + const supportingMetrics = + extras.yieldPrediction.supportingMetrics ?? + extras.yieldPrediction.supporting_metrics ?? + baseYield?.supporting_metrics ?? + baseYield?.supportingMetrics + + raw.yield_prediction = { + ...baseYield, + ...extras.yieldPrediction, + predicted_yield_tons: + predictedYieldTons ?? + baseYield?.predicted_yield_tons ?? + baseYield?.predictedYieldTons, + predicted_yield_raw: + predictedYieldRaw ?? + baseYield?.predicted_yield_raw ?? + baseYield?.predictedYieldRaw, + unit: extras.yieldPrediction.unit ?? baseYield?.unit, + source_unit: sourceUnit, + simulation_engine: simulationEngine, + simulation_model: simulationModel, + scenario_id: scenarioId, + simulation_warning: simulationWarning, + supporting_metrics: supportingMetrics, + } + } + + if (extras.harvestPrediction) { + raw.harvest_prediction_card = { + ...baseHarvest, + ...extras.harvestPrediction, + harvest_date: + extras.harvestPrediction.date ?? + baseHarvest?.harvest_date ?? + baseHarvest?.date, + harvest_date_formatted: + extras.harvestPrediction.dateFormatted ?? + baseHarvest?.harvest_date_formatted ?? + baseHarvest?.dateFormatted, + days_until: + extras.harvestPrediction.daysUntil ?? + baseHarvest?.days_until ?? + baseHarvest?.daysUntil, + optimal_window_start: + extras.harvestPrediction.optimalWindowStart ?? + baseHarvest?.optimal_window_start ?? + baseHarvest?.optimalWindowStart, + optimal_window_end: + extras.harvestPrediction.optimalWindowEnd ?? + baseHarvest?.optimal_window_end ?? + baseHarvest?.optimalWindowEnd, + description: + extras.harvestPrediction.description ?? baseHarvest?.description, + gddDetails: + extras.harvestPrediction.gddDetails ?? baseHarvest?.gddDetails, + } + } + + if (extras.currentFarmChart && !baseChart?.series) { + raw.yield_prediction_chart = { + categories: extras.currentFarmChart.categories ?? [], + series: extras.currentFarmChart.series ?? [], + summary: extras.currentFarmChart.summary ?? [], + meta: { + unit: 'leaf', + simulation_engine: extras.currentFarmChart.engine, + simulation_model: extras.currentFarmChart.model_name, + scenario_id: extras.currentFarmChart.scenario_id, + simulation_warning: extras.currentFarmChart.simulation_warning, + }, + } + } + + raw.season_highlights_card = { + ...baseSeason, + total_predicted_yield: + getPredictedYieldTons(extras.yieldPrediction) ?? + baseSeason?.total_predicted_yield, + yield_unit: extras.yieldPrediction?.unit ?? baseSeason?.yield_unit, + target_harvest_date: + extras.harvestPrediction?.dateFormatted ?? + baseSeason?.target_harvest_date, + days_until_harvest: + extras.harvestPrediction?.daysUntil ?? baseSeason?.days_until_harvest, + subtitle: + baseSeason?.subtitle ?? + extras.harvestPrediction?.description ?? + 'خلاصه سریع وضعیت عملکرد، کیفیت و بهترین بازه برداشت برای مزرعه انتخاب شده.', + } + + return normalizeSummaryData(raw) } export const yieldHarvestService = { - async getSummary(farmUuid: string): Promise { - const res = await apiClient.get> | Record>( - `${PREFIX}/summary/?farm_uuid=${encodeURIComponent(farmUuid)}` + async getCurrentFarmChart(farmUuid: string): Promise { + const response = await apiClient.post< + ApiResponse | CurrentFarmChartResponse + >(`${PREFIX}/current-farm-chart/`, buildFarmPayload(farmUuid)) + + return extract(response) + }, + + async startGrowth(farmUuid: string): Promise { + const response = await apiClient.post< + ApiResponse | GrowthStartResponse + >(`${PREFIX}/growth/`, buildFarmPayload(farmUuid)) + + return extract(response) + }, + + async getGrowthStatus( + taskId: string, + options?: { page?: number; pageSize?: number }, + ): Promise { + const params = new URLSearchParams() + + if (options?.page !== undefined) { + params.set('page', String(options.page)) + } + + if (options?.pageSize !== undefined) { + params.set('page_size', String(options.pageSize)) + } + + const query = params.toString() + const endpoint = `${PREFIX}/growth/${encodeURIComponent(taskId)}/status/${query ? `?${query}` : ''}` + const response = await apiClient.get< + ApiResponse | GrowthStatusResponse + >(endpoint) + + return extract(response) + }, + + async getHarvestPrediction( + farmUuid: string, + ): Promise { + const response = await apiClient.post< + ApiResponse | HarvestPredictionResponse + >(`${PREFIX}/harvest-prediction/`, buildFarmPayload(farmUuid)) + + return extract(response) + }, + + async getYieldPrediction(farmUuid: string): Promise { + const response = await apiClient.post< + ApiResponse | YieldPredictionResponse + >(`${PREFIX}/yield-prediction/`, buildFarmPayload(farmUuid)) + + return extract(response) + }, + + async getSummary( + farmUuid: string, + options?: SummaryOptions, + ): Promise { + const response = await apiClient.get | GenericRecord>( + `${PREFIX}/yield-harvest-summary/?${buildSummaryQuery(farmUuid, options)}`, ) - const data = extract(res) + const data = extract(response) + + return normalizeSummaryData(data) + }, + + async getDashboardData( + farmUuid: string, + options?: SummaryOptions, + ): Promise { + const [summaryResult, harvestResult, yieldResult, currentChartResult] = + await Promise.allSettled([ + this.getSummary(farmUuid, options), + this.getHarvestPrediction(farmUuid), + this.getYieldPrediction(farmUuid), + this.getCurrentFarmChart(farmUuid), + ]) + + const summary = + summaryResult.status === 'fulfilled' + ? summaryResult.value + : normalizeSummaryData({}) + const harvestPrediction = + harvestResult.status === 'fulfilled' ? harvestResult.value : undefined + const yieldPrediction = + yieldResult.status === 'fulfilled' ? yieldResult.value : undefined + const currentFarmChart = + currentChartResult.status === 'fulfilled' + ? currentChartResult.value + : undefined return { - yield_prediction: toKpiCard(data?.yield_prediction ?? data?.yield_prediction_card), - yieldPredictionChart: (data?.yieldPredictionChart ?? data?.yield_prediction_chart ?? {}) as Record, - harvestPredictionCard: (data?.harvestPredictionCard ?? data?.harvest_prediction_card ?? {}) as Record + summary: enrichSummaryData(summary, { + harvestPrediction, + yieldPrediction, + currentFarmChart, + }), + harvestPrediction, + yieldPrediction, + currentFarmChart, } }, } diff --git a/src/views/dashboards/farm/PlantProductionPage.tsx b/src/views/dashboards/farm/PlantProductionPage.tsx index 5df74fa..1986cc2 100644 --- a/src/views/dashboards/farm/PlantProductionPage.tsx +++ b/src/views/dashboards/farm/PlantProductionPage.tsx @@ -1,60 +1,94 @@ "use client"; +import { useCallback, useEffect, useState } from "react"; +import { useFarmHub } from "@/hooks/useFarmHub"; +import { yieldHarvestService } from "@/libs/api/services/yieldHarvestService"; +import type { + CurrentFarmChartResponse, + YieldHarvestSummary, +} from "@/libs/api/services/yieldHarvestService"; + import Grid from "@mui/material/Grid2"; +import Box from "@mui/material/Box"; +import CircularProgress from "@mui/material/CircularProgress"; import YieldHarvestPageWrapper from "@views/dashboards/farm/YieldHarvestPageWrapper"; import YieldSeasonHighlightsCard from "@views/dashboards/farm/YieldSeasonHighlightsCard"; import PlantSimulator from "@views/dashboards/farm/plantSimulator/PlantSimulator"; - -const mockSeasonHighlightsData = { - title: "اتاق فرمان برداشت این فصل", - subtitle: - "این بخش برای نمایش سریع وضعیت عملکرد، کیفیت و بهترین پنجره فروش طراحی شده است. داده ها فعلا ماک هستند تا ظاهر نهایی کارت ها و ریتم بصری صفحه بهتر دیده شود.", - seasonLabel: "فصل ۱۴۰۴", - badges: ["کیفیت ممتاز", "آماده بسته بندی", "ریسک پایین"], - spotlight: { - title: "پنجره طلایی فروش", - value: "۳ روز اول بعد از برداشت", - caption: "در این بازه، برآورد قیمت فروش حدود ۸٪ بهتر از میانگین هفتگی است.", - }, - metrics: [ - { - label: "سطح قابل برداشت", - value: "18.6 هکتار", - caption: "۴ قطعه در اولویت نخست قرار دارند.", - avatarIcon: "tabler-map-2", - avatarColor: "success", - }, - { - label: "گرید ممتاز", - value: "46%", - caption: "بالاترین سهم کیفیت مربوط به قطعه A2 است.", - avatarIcon: "tabler-rosette-discount-check", - avatarColor: "warning", - }, - { - label: "درآمد هدف", - value: "1.84 میلیارد", - caption: "با فرض فروش در بازه پیشنهادی مدل.", - avatarIcon: "tabler-cash-banknote", - avatarColor: "primary", - }, - ], -}; +import { mockYieldHarvestSummary } from "@views/dashboards/farm/yieldHarvestMockData"; const PlantProductionPage = () => { + const { farmHub } = useFarmHub(); + const farmUuid = farmHub?.farm_uuid; + const [summary, setSummary] = + useState(mockYieldHarvestSummary); + const [currentFarmChart, setCurrentFarmChart] = + useState(null); + const [loading, setLoading] = useState(true); + + const loadDashboard = useCallback(() => { + if (!farmUuid) { + setSummary(mockYieldHarvestSummary); + setCurrentFarmChart(null); + setLoading(false); + return Promise.resolve(); + } + + setLoading(true); + return yieldHarvestService + .getDashboardData(farmUuid) + .then((data) => { + setSummary(data.summary); + setCurrentFarmChart(data.currentFarmChart ?? null); + }) + .catch(() => { + setSummary(mockYieldHarvestSummary); + setCurrentFarmChart(null); + }) + .finally(() => setLoading(false)); + }, [farmUuid]); + + useEffect(() => { + loadDashboard(); + }, [loadDashboard]); + + if (loading) { + return ( + + + + ); + } + return ( - + ) ?? + (mockYieldHarvestSummary.seasonHighlightsCard as Record< + string, + unknown + >) + } + /> - + - + ); diff --git a/src/views/dashboards/farm/YieldHarvestPageWrapper.tsx b/src/views/dashboards/farm/YieldHarvestPageWrapper.tsx index 2a11706..e6be3ad 100644 --- a/src/views/dashboards/farm/YieldHarvestPageWrapper.tsx +++ b/src/views/dashboards/farm/YieldHarvestPageWrapper.tsx @@ -1,11 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; -import { useFarmHub } from "@/hooks/useFarmHub"; - import Grid from "@mui/material/Grid2"; import Box from "@mui/material/Box"; -import CircularProgress from "@mui/material/CircularProgress"; import HarvestPredictionCard from "@views/dashboards/farm/HarvestPredictionCard"; import YieldPredictionChart from "@views/dashboards/farm/YieldPredictionChart"; @@ -14,8 +10,8 @@ import HarvestReadinessZonesCard from "@views/dashboards/farm/HarvestReadinessZo import YieldQualityBandsCard from "@views/dashboards/farm/YieldQualityBandsCard"; import HarvestOperationsCard from "@views/dashboards/farm/HarvestOperationsCard"; -import { yieldHarvestService } from "@/libs/api/services/yieldHarvestService"; import type { YieldHarvestSummary } from "@/libs/api/services/yieldHarvestService"; +import { mockYieldHarvestSummary } from "@views/dashboards/farm/yieldHarvestMockData"; const cardSlotSx = { display: "flex", @@ -40,246 +36,32 @@ const chartCardSx = { minHeight: { xs: 360, lg: 440 }, }; -const mockYieldHarvestData: YieldHarvestSummary = { - yield_prediction: { - kpis: [ - { - id: "predicted-yield", - title: "عملکرد پیش بینی شده", - subtitle: "فصل جاری", - stats: "42.8 تن", - avatarColor: "primary", - avatarIcon: "tabler-chart-arcs", - chipText: "+12%", - chipColor: "success", - }, - { - id: "harvest-readiness", - title: "آمادگی برداشت", - subtitle: "میانگین مزرعه", - stats: "84%", - avatarColor: "success", - avatarIcon: "tabler-plant-2", - chipText: "روی برنامه", - chipColor: "success", - }, - { - id: "quality-score", - title: "امتیاز کیفیت", - subtitle: "برآورد هوش مصنوعی", - stats: "91/100", - avatarColor: "info", - avatarIcon: "tabler-stars", - chipText: "+4 واحد", - chipColor: "success", - }, - { - id: "loss-risk", - title: "ریسک افت محصول", - subtitle: "آب وهوا و آفات", - stats: "6.5%", - avatarColor: "warning", - avatarIcon: "tabler-alert-triangle", - chipText: "پایین", - chipColor: "success", - }, - ], - }, - harvestPredictionCard: { - dateFormatted: "۲۸ شهریور", - daysUntil: 18, - description: - "با توجه به روند رشد بوته، الگوی آبیاری و وضعیت دمایی اخیر، این مزرعه در هفته آخر شهریور به نقطه ایده آل برداشت می رسد.", - }, - yieldPredictionChart: { - categories: [ - "فروردین", - "اردیبهشت", - "خرداد", - "تیر", - "مرداد", - "شهریور", - "مهر", - "آبان", - ], - series: [ - { - name: "سال قبل", - data: [9, 11, 13, 16, 19, 24, 28, 31], - }, - { - name: "سال جاری", - data: [10, 12, 15, 18, 23, 29, 34, 39], - }, - ], - summary: [ - { - title: "بیشترین خروجی پیش بینی شده", - subtitle: "بهترین ماه برداشت", - amount: "39 تن", - avatarColor: "success", - avatarIcon: "tabler-trending-up", - }, - { - title: "رشد این فصل", - subtitle: "نسبت به سال قبل", - amount: "+11.2 تن", - avatarColor: "primary", - avatarIcon: "tabler-chart-line", - }, - ], - }, +const hasYieldKpis = (summary?: YieldHarvestSummary) => { + const kpis = (summary?.yield_prediction as Record | undefined) + ?.kpis; + + return Array.isArray(kpis) && kpis.length > 0; }; -const mockHarvestReadinessData = { - averageReadiness: "84%", - blocks: [ - { - name: "قطعه A1", - cultivar: "گندم سیروان", - readiness: 92, - harvestDate: "۲۶ شهریور", - expectedYield: "12.4 تن", - moisture: "11.8%", - }, - { - name: "قطعه A2", - cultivar: "گندم پیشگام", - readiness: 87, - harvestDate: "۲۷ شهریور", - expectedYield: "10.1 تن", - moisture: "12.3%", - }, - { - name: "قطعه B1", - cultivar: "گندم مهرگان", - readiness: 73, - harvestDate: "۳۰ شهریور", - expectedYield: "8.6 تن", - moisture: "13.7%", - }, - ], +const hasYieldChart = (summary?: YieldHarvestSummary) => { + const series = ( + summary?.yieldPredictionChart as Record | undefined + )?.series; + + return Array.isArray(series) && series.length > 0; }; -const mockYieldQualityBandsData = { - bands: [ - { - label: "گرید ممتاز", - share: 46, - volume: "19.7 تن", - premium: "+18% قیمت", - color: "#2e7d32", - }, - { - label: "گرید درجه یک", - share: 34, - volume: "14.5 تن", - premium: "+9% قیمت", - color: "#0288d1", - }, - { - label: "گرید فرآوری", - share: 20, - volume: "8.6 تن", - premium: "فروش پایه", - color: "#ed6c02", - }, - ], - stats: [ - { label: "میانگین بریکس", value: "14.8" }, - { label: "یکنواختی دانه", value: "89%" }, - { label: "ضایعات قابل انتظار", value: "2.1%" }, - { label: "پتانسیل صادرات", value: "بالا" }, - ], -}; +interface YieldHarvestPageWrapperProps { + data?: YieldHarvestSummary; +} -const mockHarvestOperationsData = { - summary: - "اگر برداشت از قطعات A1 و A2 در دو شیفت اول انجام شود، کیفیت ممتاز حفظ می شود و فشار روی مرحله سورتینگ نیز متعادل می ماند.", - steps: [ - { - title: "برداشت قطعات اولویت دار", - note: "تمرکز ابتدا روی A1 و سپس A2 باشد تا گرید ممتاز در دمای پایین صبح جمع آوری شود.", - status: "امروز", - statusColor: "success", - }, - { - title: "سورت و تفکیک بر اساس کیفیت", - note: "محصول ممتاز از جریان فرآوری جدا شود تا فروش با قیمت پریمیوم قابل حفظ باشد.", - status: "بعد از برداشت", - statusColor: "primary", - }, - { - title: "انتقال سریع به انبار خنک", - note: "برای جلوگیری از افت رطوبت و رنگ، انتقال نهایی حداکثر تا ۶ ساعت پس از برداشت انجام شود.", - status: "ضروری", - statusColor: "warning", - }, - ], - outputs: [ - { label: "شیفت پیشنهادی", value: "۲ شیفت" }, - { label: "ظرفیت سورتینگ", value: "15 تن/روز" }, - { label: "نیروی مورد نیاز", value: "12 نفر" }, - { label: "مدت تا ارسال", value: "6 ساعت" }, - ], -}; - -const hasRenderableData = (summary?: YieldHarvestSummary) => - Boolean( - summary?.yield_prediction || - summary?.harvestPredictionCard || - ((summary?.yieldPredictionChart?.series as unknown[])?.length ?? 0) > 0, - ); - -const hasYieldKpis = (summary?: YieldHarvestSummary) => - ((summary?.yield_prediction as Record | undefined)?.kpis as - | unknown[] - | undefined)?.length > 0; - -const hasYieldChart = (summary?: YieldHarvestSummary) => - ((summary?.yieldPredictionChart as Record | undefined) - ?.series as unknown[] | undefined)?.length > 0; - -const YieldHarvestPageWrapper = () => { - const { farmHub } = useFarmHub(); - const farmUuid = farmHub?.farm_uuid; - const [data, setData] = useState(mockYieldHarvestData); - const [loading, setLoading] = useState(true); - - useEffect(() => { - if (!farmUuid) { - setData(mockYieldHarvestData); - setLoading(false); - return; - } - - setLoading(true); - yieldHarvestService - .getSummary(farmUuid) - .then((summary) => - setData(hasRenderableData(summary) ? summary : mockYieldHarvestData), - ) - .catch(() => setData(mockYieldHarvestData)) - .finally(() => setLoading(false)); - }, [farmUuid]); - - if (loading) { - return ( - - - - ); - } +const YieldHarvestPageWrapper = ({ data }: YieldHarvestPageWrapperProps) => { + const summary = data ?? mockYieldHarvestSummary; return ( - {hasYieldKpis(data) && ( + {hasYieldKpis(summary) && ( { sx={{ width: "100%" }} > } + data={summary.yield_prediction as Record} /> )} + } + data={ + (summary.harvestPredictionCard as Record) ?? + (mockYieldHarvestSummary.harvestPredictionCard as Record< + string, + unknown + >) + } /> - + ) ?? + (mockYieldHarvestSummary.harvestReadinessZones as Record< + string, + unknown + >) + } + /> - + ) ?? + (mockYieldHarvestSummary.yieldQualityBands as Record< + string, + unknown + >) + } + /> - + ) ?? + (mockYieldHarvestSummary.harvestOperationsCard as Record< + string, + unknown + >) + } + /> - {hasYieldChart(data) && ( + + {hasYieldChart(summary) && ( } + data={summary.yieldPredictionChart as Record} /> diff --git a/src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx b/src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx index 35360c3..f594d23 100644 --- a/src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx +++ b/src/views/dashboards/farm/plantSimulator/PlantSimulator.tsx @@ -20,6 +20,13 @@ import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; import Grid from "@mui/material/Grid2"; import Button from "@mui/material/Button"; +import Chip from "@mui/material/Chip"; + +import { + yieldHarvestService, + type CurrentFarmChartResponse, + type GrowthStatusResponse, +} from "@/libs/api/services/yieldHarvestService"; ChartJS.register( LineElement, @@ -81,6 +88,12 @@ interface EnvironmentSettings { water: number; } +interface PlantSimulatorProps { + farmUuid?: string; + currentFarmChart?: CurrentFarmChartResponse; + onRefreshDashboard?: () => Promise | void; +} + // ─── Constants ─────────────────────────────────────────────────────────────── const MAX_HEIGHT = 280; @@ -721,6 +734,19 @@ const GrowthChart = memo(function GrowthChart({ ); }); +function getGrowthTaskColor(status?: GrowthStatusResponse["status"]) { + switch (status) { + case "SUCCESS": + return "success"; + case "FAILURE": + return "error"; + case "PROGRESS": + return "warning"; + default: + return "default"; + } +} + // ─── Main Component ─────────────────────────────────────────────────────────── const INIT_PLANT: PlantState = { @@ -733,7 +759,11 @@ const INIT_PLANT: PlantState = { yieldRate: 0, }; -export default function PlantSimulator() { +export default function PlantSimulator({ + farmUuid, + currentFarmChart, + onRefreshDashboard, +}: PlantSimulatorProps) { const [running, setRunning] = useState(false); const [speed, setSpeed] = useState(1.5); const [env, setEnv] = useState({ light: 75, water: 65 }); @@ -749,6 +779,10 @@ export default function PlantSimulator() { const historyIntervalRef = useRef | null>( null, ); + const [growthTaskId, setGrowthTaskId] = useState(null); + const [growthTaskStatus, setGrowthTaskStatus] = + useState(null); + const [growthTaskLoading, setGrowthTaskLoading] = useState(false); const plantRef = useRef(plant); plantRef.current = plant; @@ -935,8 +969,52 @@ export default function PlantSimulator() { }; }, [running]); + useEffect(() => { + if (!growthTaskId) return; + + let isMounted = true; + const pollStatus = async () => { + try { + const status = await yieldHarvestService.getGrowthStatus(growthTaskId); + + if (!isMounted) return; + setGrowthTaskStatus(status); + + if (status.status === "SUCCESS" || status.status === "FAILURE") { + setGrowthTaskLoading(false); + setGrowthTaskId(null); + if (status.status === "SUCCESS") { + await onRefreshDashboard?.(); + } + } + } catch { + if (!isMounted) return; + setGrowthTaskLoading(false); + setGrowthTaskStatus({ + task_id: growthTaskId, + status: "FAILURE", + error: "خطا در دریافت وضعیت شبیه سازی", + }); + setGrowthTaskId(null); + } + }; + + pollStatus(); + const pollInterval = setInterval(pollStatus, 4000); + + return () => { + isMounted = false; + clearInterval(pollInterval); + }; + }, [growthTaskId, onRefreshDashboard]); + const t = useTranslations("plantSimulator"); const isFinished = plant.height >= MAX_HEIGHT; + const backendSummaryItems = currentFarmChart?.summary ?? []; + const backendCurrentState = currentFarmChart?.current_state ?? {}; + const backendTaskPercent = Math.round( + growthTaskStatus?.progress?.percent ?? 0, + ); const statItems: { value: string | number; @@ -964,7 +1042,7 @@ export default function PlantSimulator() { {/* ── Plant visualization ── */} - + {statItems.map((item, idx) => ( - + {item.value} @@ -1004,7 +1082,7 @@ export default function PlantSimulator() { {/* ── Chart ── */} - + setRunning((r) => !r)} + onClick={async () => { + if (running) { + setRunning(false); + return; + } + + setRunning(true); + + if (!farmUuid || growthTaskLoading || growthTaskId) { + return; + } + + try { + setGrowthTaskLoading(true); + const task = await yieldHarvestService.startGrowth( + farmUuid, + ); + + setGrowthTaskStatus({ + task_id: task.task_id, + status: "PENDING", + message: "تسک شبیه سازی در صف قرار گرفت.", + }); + setGrowthTaskId(task.task_id); + } catch { + setGrowthTaskLoading(false); + setGrowthTaskStatus({ + task_id: "unavailable", + status: "FAILURE", + error: "شروع شبیه سازی در بک اند انجام نشد.", + }); + } + }} disabled={isFinished} fullWidth > @@ -1262,6 +1372,98 @@ export default function PlantSimulator() { + + {farmUuid && ( + +
+ + وضعیت اتصال AI + + +
+ + + گیاه فعال: {currentFarmChart?.plant_name ?? "نامشخص"} + + + + موتور: {currentFarmChart?.engine ?? "-"} / مدل:{" "} + {currentFarmChart?.model_name ?? "-"} + + + {growthTaskLoading && ( + + تسک شبیه سازی در حال ارسال به بک اند است... + + )} + + {growthTaskStatus?.progress && ( + + پیشرفت شبیه سازی: {backendTaskPercent}٪ + + )} + + {growthTaskStatus?.message && ( + + {growthTaskStatus.message} + + )} + + {growthTaskStatus?.error && ( + + {growthTaskStatus.error} + + )} + + {backendSummaryItems.length > 0 && ( + + {backendSummaryItems.slice(0, 2).map((item, index) => ( + + + {item.title} + + + {item.amount} + {item.unit ? ` ${item.unit}` : ""} + + {item.subtitle && ( + + {item.subtitle} + + )} + + ))} + + )} + + {Object.keys(backendCurrentState).length > 0 && ( + + آخرین وضعیت مزرعه: برگ {String( + backendCurrentState.leaf_count_estimate ?? "-", + )}، بیوماس {String( + backendCurrentState.biomass_weight ?? "-", + )}، رطوبت خاک {String( + backendCurrentState.soil_moisture_percent ?? "-", + )} + + )} +
+ )}
diff --git a/src/views/dashboards/farm/yieldHarvestMockData.ts b/src/views/dashboards/farm/yieldHarvestMockData.ts new file mode 100644 index 0000000..bb3dc0b --- /dev/null +++ b/src/views/dashboards/farm/yieldHarvestMockData.ts @@ -0,0 +1,212 @@ +import type { YieldHarvestSummary } from '@/libs/api/services/yieldHarvestService' + +export const mockYieldHarvestSummary: YieldHarvestSummary = { + seasonHighlightsCard: { + title: 'اتاق فرمان برداشت این فصل', + subtitle: + 'این بخش برای نمایش سریع وضعیت عملکرد، کیفیت و بهترین پنجره فروش طراحی شده است. داده ها فعلا ماک هستند تا ظاهر نهایی کارت ها و ریتم بصری صفحه بهتر دیده شود.', + seasonLabel: 'فصل ۱۴۰۴', + badges: ['کیفیت ممتاز', 'آماده بسته بندی', 'ریسک پایین'], + spotlight: { + title: 'پنجره طلایی فروش', + value: '۳ روز اول بعد از برداشت', + caption: + 'در این بازه، برآورد قیمت فروش حدود ۸٪ بهتر از میانگین هفتگی است.', + }, + metrics: [ + { + label: 'سطح قابل برداشت', + value: '18.6 هکتار', + caption: '۴ قطعه در اولویت نخست قرار دارند.', + avatarIcon: 'tabler-map-2', + avatarColor: 'success', + }, + { + label: 'گرید ممتاز', + value: '46%', + caption: 'بالاترین سهم کیفیت مربوط به قطعه A2 است.', + avatarIcon: 'tabler-rosette-discount-check', + avatarColor: 'warning', + }, + { + label: 'درآمد هدف', + value: '1.84 میلیارد', + caption: 'با فرض فروش در بازه پیشنهادی مدل.', + avatarIcon: 'tabler-cash-banknote', + avatarColor: 'primary', + }, + ], + }, + yield_prediction: { + kpis: [ + { + id: 'predicted-yield', + title: 'عملکرد پیش بینی شده', + subtitle: 'فصل جاری', + stats: '42.8 تن', + avatarColor: 'primary', + avatarIcon: 'tabler-chart-arcs', + chipText: '+12%', + chipColor: 'success', + }, + { + id: 'harvest-readiness', + title: 'آمادگی برداشت', + subtitle: 'میانگین مزرعه', + stats: '84%', + avatarColor: 'success', + avatarIcon: 'tabler-plant-2', + chipText: 'روی برنامه', + chipColor: 'success', + }, + { + id: 'quality-score', + title: 'امتیاز کیفیت', + subtitle: 'برآورد هوش مصنوعی', + stats: '91/100', + avatarColor: 'info', + avatarIcon: 'tabler-stars', + chipText: '+4 واحد', + chipColor: 'success', + }, + { + id: 'loss-risk', + title: 'ریسک افت محصول', + subtitle: 'آب و هوا و آفات', + stats: '6.5%', + avatarColor: 'warning', + avatarIcon: 'tabler-alert-triangle', + chipText: 'پایین', + chipColor: 'success', + }, + ], + }, + harvestPredictionCard: { + dateFormatted: '۲۸ شهریور', + daysUntil: 18, + description: + 'با توجه به روند رشد بوته، الگوی آبیاری و وضعیت دمایی اخیر، این مزرعه در هفته آخر شهریور به نقطه ایده آل برداشت می رسد.', + }, + harvestReadinessZones: { + averageReadiness: '84%', + blocks: [ + { + name: 'قطعه A1', + cultivar: 'گندم سیروان', + readiness: 92, + harvestDate: '۲۶ شهریور', + expectedYield: '12.4 تن', + moisture: '11.8%', + }, + { + name: 'قطعه A2', + cultivar: 'گندم پیشگام', + readiness: 87, + harvestDate: '۲۷ شهریور', + expectedYield: '10.1 تن', + moisture: '12.3%', + }, + { + name: 'قطعه B1', + cultivar: 'گندم مهرگان', + readiness: 73, + harvestDate: '۳۰ شهریور', + expectedYield: '8.6 تن', + moisture: '13.7%', + }, + ], + }, + yieldQualityBands: { + bands: [ + { + label: 'گرید ممتاز', + share: 46, + volume: '19.7 تن', + premium: '+18% قیمت', + color: '#2e7d32', + }, + { + label: 'گرید درجه یک', + share: 34, + volume: '14.5 تن', + premium: '+9% قیمت', + color: '#0288d1', + }, + { + label: 'گرید فرآوری', + share: 20, + volume: '8.6 تن', + premium: 'فروش پایه', + color: '#ed6c02', + }, + ], + stats: [ + { label: 'میانگین بریکس', value: '14.8' }, + { label: 'یکنواختی دانه', value: '89%' }, + { label: 'ضایعات قابل انتظار', value: '2.1%' }, + { label: 'پتانسیل صادرات', value: 'بالا' }, + ], + }, + harvestOperationsCard: { + summary: + 'اگر برداشت از قطعات A1 و A2 در دو شیفت اول انجام شود، کیفیت ممتاز حفظ می شود و فشار روی مرحله سورتینگ نیز متعادل می ماند.', + steps: [ + { + title: 'برداشت قطعات اولویت دار', + note: + 'تمرکز ابتدا روی A1 و سپس A2 باشد تا گرید ممتاز در دمای پایین صبح جمع آوری شود.', + status: 'امروز', + statusColor: 'success', + }, + { + title: 'سورت و تفکیک بر اساس کیفیت', + note: + 'محصول ممتاز از جریان فرآوری جدا شود تا فروش با قیمت پریمیوم قابل حفظ باشد.', + status: 'بعد از برداشت', + statusColor: 'primary', + }, + { + title: 'انتقال سریع به انبار خنک', + note: + 'برای جلوگیری از افت رطوبت و رنگ، انتقال نهایی حداکثر تا ۶ ساعت پس از برداشت انجام شود.', + status: 'ضروری', + statusColor: 'warning', + }, + ], + outputs: [ + { label: 'شیفت پیشنهادی', value: '۲ شیفت' }, + { label: 'ظرفیت سورتینگ', value: '15 تن/روز' }, + { label: 'نیروی مورد نیاز', value: '12 نفر' }, + { label: 'مدت تا ارسال', value: '6 ساعت' }, + ], + }, + yieldPredictionChart: { + categories: ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان'], + series: [ + { + name: 'سال قبل', + data: [9, 11, 13, 16, 19, 24, 28, 31], + }, + { + name: 'سال جاری', + data: [10, 12, 15, 18, 23, 29, 34, 39], + }, + ], + summary: [ + { + title: 'بیشترین خروجی پیش بینی شده', + subtitle: 'بهترین ماه برداشت', + amount: '39 تن', + avatarColor: 'success', + avatarIcon: 'tabler-trending-up', + }, + { + title: 'رشد این فصل', + subtitle: 'نسبت به سال قبل', + amount: '+11.2 تن', + avatarColor: 'primary', + avatarIcon: 'tabler-chart-line', + }, + ], + }, +}