diff --git a/FARMER_CALENDAR_BACKEND_REQUIREMENTS.md b/FARMER_CALENDAR_BACKEND_REQUIREMENTS.md new file mode 100644 index 0000000..c8fe883 --- /dev/null +++ b/FARMER_CALENDAR_BACKEND_REQUIREMENTS.md @@ -0,0 +1,381 @@ +# Farmer Calendar Backend Requirements + +این فایل قرارداد کامل داده‌ای برای صفحه `farmer-calendar` را توضیح می‌دهد؛ یعنی هر چیزی که فرانت برای نمایش، ساخت، ویرایش و حذف رویدادهای تقویم نیاز دارد باید از بک‌اند دریافت یا به بک‌اند ارسال کند. + +## Scope + +این مستند بر اساس این فایل‌ها تهیه شده است: + +- `src/views/dashboards/farm/FarmerCalendarPage.tsx` +- `src/views/dashboards/farm/FarmerCalendarEventModal.tsx` +- `src/views/dashboards/farm/FarmerCalendarEventDetails.tsx` +- `src/views/apps/calendar/Calendar.tsx` +- `src/redux-store/slices/calendar.ts` +- `src/libs/api/services/eventService.ts` + +## Frontend Features Covered + +بک‌اند باید از این قابلیت‌های صفحه پشتیبانی کند: + +- نمایش لیست رویدادهای تقویم +- فیلتر رویدادها بر اساس بازه زمانی و نوع تقویم +- نمایش جزییات رویداد +- ساخت رویداد جدید +- ویرایش رویداد +- حذف رویداد +- دریافت جداگانه‌ی لیست `tag`ها برای فرم ساخت/ویرایش +- پشتیبانی از drag/drop و resize رویدادها در تقویم + +## Domain Model + +هر رویداد تقویم باید حداقل ساختار زیر را داشته باشد: + +```ts +type CalendarEvent = { + id: string; + title: string; + description: string; + deadline?: number | null; + tags: string[]; + start: string; + end: string; + extendedProps?: Record; +}; +``` + +## Required Event Fields For Frontend + +فیلدهای زیر برای عملکرد درست فرانت لازم هستند: + +| Field | Type | Required | Used For | +|---|---|---:|---| +| `id` | `string` | yes | شناسایی رویداد برای edit/delete/update | +| `title` | `string` | yes | عنوان رویداد در تقویم، کارت‌ها، مودال جزییات | +| `description` | `string` | no but recommended | نمایش توضیح در کارت‌ها و جزییات | +| `start` | `ISO 8601 string` | yes | نمایش زمان شروع، تقویم، محاسبه امروز/این هفته | +| `end` | `ISO 8601 string` | yes | نمایش بازه زمانی، resize/drop | +| `tags` | `string[]` | yes | نمایش tag در جزییات، انتخاب مقدار در فرم | +| `deadline` | `number` | no | فعلا در state نگه‌داری می‌شود، بهتر است برگردد | +| `extendedProps` | `object` | no | برای توسعه آینده و سازگاری با FullCalendar | + +## Endpoints + +### 1) List Events + +برای لود اولیه‌ی صفحه و رفرش رویدادها. + +`GET /api/events` + +#### Query Params + +| Param | Type | Required | Description | +|---|---|---:|---| +| `start` | `string` | no | شروع بازه به فرمت ISO 8601 | +| `end` | `string` | no | پایان بازه به فرمت ISO 8601 | + +#### Success Response + +```json +{ + "events": [ + { + "id": "evt_101", + "title": "آبیاری بلوک شمالی", + "description": "کنترل فشار و مدت زمان آبیاری", + "deadline": 1734942600, + "tags": ["آبیاری"], + "start": "2025-02-24T06:30:00Z", + "end": "2025-02-24T08:00:00Z", + "extendedProps": {} + } + ] +} +``` + +#### Frontend Notes + +- اگر `events` خالی باشد باید `[]` برگردد، نه `null` +- تاریخ‌ها باید قابل parse شدن با `new Date(...)` و `parseISO(...)` باشند +- `start` و `end` برای همه رویدادها لازم‌اند + +### 2) Get Event Details + +برای سناریوهای آینده یا lazy loading جزییات. + +`GET /api/events/:id` + +#### Success Response + +```json +{ + "event": { + "id": "evt_101", + "title": "آبیاری بلوک شمالی", + "description": "کنترل فشار و مدت زمان آبیاری", + "deadline": 1734942600, + "tags": ["آبیاری"], + "start": "2025-02-24T06:30:00Z", + "end": "2025-02-24T08:00:00Z", + "extendedProps": {} + } +} +``` + +### 3) Create Event + +برای ساخت تسک/رویداد روزانه‌ی جدید. + +`POST /api/events` + +#### Request Body + +```json +{ + "title": "بازدید آفت در گلخانه", + "description": "بررسی وضعیت برگ‌ها و ثبت گزارش", + "deadline": 1734971400, + "tags": ["آفت"], + "start": "2025-02-24T14:00:00Z", + "end": "2025-02-24T15:00:00Z", + "extendedProps": {} +} +``` + +#### Required Create Fields + +- `title` +- `start` +- `end` + +#### Success Response + +```json +{ + "event": { + "id": "evt_102", + "title": "بازدید آفت در گلخانه", + "description": "بررسی وضعیت برگ‌ها و ثبت گزارش", + "deadline": 1734971400, + "tags": ["آفت"], + "start": "2025-02-24T14:00:00Z", + "end": "2025-02-24T15:00:00Z", + "extendedProps": {} + } +} +``` + +### 4) Update Event + +برای ویرایش دستی، drag/drop و resize. + +`PUT /api/events/:id` + +#### Request Body + +```json +{ + "title": "بازدید آفت در گلخانه", + "description": "اولویت بالا", + "deadline": 1734971400, + "tags": ["آفت", "فوری"], + "start": "2025-02-24T15:00:00Z", + "end": "2025-02-24T16:00:00Z", + "extendedProps": {} +} +``` + +#### Success Response + +```json +{ + "event": { + "id": "evt_102", + "title": "بازدید آفت در گلخانه", + "description": "اولویت بالا", + "deadline": 1734971400, + "tags": ["آفت", "فوری"], + "start": "2025-02-24T15:00:00Z", + "end": "2025-02-24T16:00:00Z", + "extendedProps": {} + } +} +``` + +#### Important Update Notes + +- این endpoint باید برای هر دو حالت `form edit` و `calendar drag/resize` جواب بدهد +- تغییر `start` و `end` باید سریع و idempotent باشد + +### 5) Delete Event + +`DELETE /api/events/:id` + +#### Success Response + +```json +{ + "success": true +} +``` + +## Separate Tags API + +طبق نیاز این صفحه، لیست `tag`ها نباید از `events` استخراج شود و باید از یک API جداگانه دریافت شود. + +### 6) List Event Tags + +`GET /api/events/tags` + +#### Purpose + +- پر کردن `select` مربوط به tag در فرم ساخت/ویرایش +- جلوگیری از وابستگی UI به استخراج `tags` از event list +- بهینه‌تر شدن لود مودال افزودن رویداد + +#### Success Response + +```json +{ + "tags": [ + { + "id": "tag_irrigation", + "label": "آبیاری", + "value": "آبیاری" + }, + { + "id": "tag_pest", + "label": "آفت", + "value": "آفت" + }, + { + "id": "tag_harvest", + "label": "برداشت", + "value": "برداشت" + } + ] +} +``` + +#### Minimum Accepted Alternative + +اگر نخواهید آبجکت کامل برگردانید، این ساختار هم برای فرانت کافی است: + +```json +{ + "tags": ["آبیاری", "آفت", "برداشت"] +} +``` + +#### Recommendation + +فرمت آبجکتی بهتر است، چون بعدا این قابلیت‌ها را ساده‌تر می‌کند: + +- مرتب‌سازی +- disabled state +- رنگ یا آیکن برای هر tag +- localization + +## Error Handling + +برای همه endpointها بهتر است خطاها ساختار ثابت داشته باشند: + +```json +{ + "code": "EVENT_VALIDATION_ERROR", + "message": "Invalid event payload", + "details": { + "start": "start is required" + } +} +``` + +فرانت فعلی حداقل به یک `message` قابل‌نمایش نیاز دارد. + +## Validation Rules Recommended + +- `title` نباید خالی باشد +- `start` باید تاریخ معتبر باشد +- `end` باید تاریخ معتبر باشد +- `end` نباید قبل از `start` باشد +- `tags` اگر موجود است باید آرایه‌ای از string باشد + +## Date/Time Requirements + +- فرمت ترجیحی: `ISO 8601 UTC`, مثال: `2025-02-24T06:30:00Z` +- timezone باید در پاسخ‌ها صریح و قابل پیش‌بینی باشد +- بک‌اند نباید تاریخ مبهم بدون timezone برگرداند + +## What Frontend Actually Renders Today + +این فیلدها همین الان در UI استفاده می‌شوند: + +- `title` +- `description` +- `start` +- `end` +- `tags[0]` + +این فیلدها فعلا بیشتر برای سازگاری یا توسعه بعدی نگه‌داری می‌شوند: + +- `deadline` +- `extendedProps` + +## Performance Recommendations + +- `GET /api/events` باید pagination یا date-range filtering را پشتیبانی کند، حتی اگر فعلا فرانت از آن به‌صورت کامل استفاده نکند +- `GET /api/events/tags` باید سبک و cacheable باشد +- پاسخ `GET /api/events/tags` بهتر است کوچک و بدون payload اضافی باشد + +## Final Backend Checklist + +- `GET /api/events` +- `GET /api/events/:id` +- `POST /api/events` +- `PUT /api/events/:id` +- `DELETE /api/events/:id` +- `GET /api/events/tags` +- پشتیبانی از `tags` به صورت آرایه +- بازگرداندن تاریخ‌ها با فرمت ISO 8601 + +## Suggested Stable Response Shapes + +اگر بخواهید قرارداد بک‌اند کاملا پایدار و توسعه‌پذیر باشد، این دو response shape پیشنهاد می‌شوند: + +### Events List + +```json +{ + "events": [ + { + "id": "evt_101", + "title": "آبیاری بلوک شمالی", + "description": "کنترل فشار و مدت زمان آبیاری", + "deadline": 1734942600, + "tags": ["آبیاری"], + "start": "2025-02-24T06:30:00Z", + "end": "2025-02-24T08:00:00Z", + "extendedProps": {} + } + ], + "meta": { + "total": 1 + } +} +``` + +### Tags List + +```json +{ + "tags": [ + { + "id": "tag_irrigation", + "label": "آبیاری", + "value": "آبیاری" + } + ], + "meta": { + "total": 1 + } +} +``` diff --git a/FARMER_TODOS_BACKEND_REQUIREMENTS.md b/FARMER_TODOS_BACKEND_REQUIREMENTS.md new file mode 100644 index 0000000..95e2cd1 --- /dev/null +++ b/FARMER_TODOS_BACKEND_REQUIREMENTS.md @@ -0,0 +1,451 @@ +# Farmer Todos Backend Requirements + +این فایل قرارداد کامل داده‌ای برای صفحه `farmer-todos` را توضیح می‌دهد؛ یعنی همه اطلاعاتی که فرانت باید از بک‌اند دریافت کند یا به بک‌اند ارسال کند تا صفحه تودولیست مزرعه به‌صورت کامل و قابل اتصال کار کند. + +## Scope + +این مستند بر اساس این فایل‌ها تهیه شده است: + +- `src/views/dashboards/farm/todos/FarmerTodoPage.tsx` +- `src/libs/api/services/todoService.ts` +- `src/libs/api/services/taskService.ts` + +## Frontend Features Covered + +بک‌اند باید از این قابلیت‌های صفحه پشتیبانی کند: + +- نمایش لیست تسک‌های مزرعه +- ثبت سریع تسک جدید با `عنوان + محل اجرا + روز + ساعت + اولویت` +- نمایش تسک‌ها بر اساس `همه / امروز / فوری / انجام شده` +- تغییر وضعیت تسک بین `باز` و `انجام شده` +- نمایش `note` و `tags` +- نمایش زمان‌بندی هر تسک با `scheduledDate` و `time` +- دریافت لیست محل‌های اجرا برای select فرم +- دریافت لیست tagها برای توسعه بعدی یا prefill + +## Page Data Model + +مدلی که این صفحه واقعا با آن کار می‌کند به این شکل است: + +```ts +type TaskPriority = "زیاد" | "متوسط" | "کم"; +type TaskStatus = "open" | "done"; + +type FarmerTodoTask = { + id: number | string; + title: string; + zone: string; + scheduledDate: string; + time: string; + priority: TaskPriority; + note: string; + tags: string[]; + status: TaskStatus; +}; +``` + +## Required Fields For Frontend + +| Field | Type | Required | Used For | +|---|---|---:|---| +| `id` | `number \| string` | yes | toggle/update/render key | +| `title` | `string` | yes | عنوان کارت تسک | +| `zone` | `string` | yes | نمایش محل اجرا و select فرم | +| `scheduledDate` | `string` | yes | فیلتر امروز، مرتب‌سازی، نمایش روز | +| `time` | `string` | yes | نمایش ساعت و مرتب‌سازی | +| `priority` | `"زیاد" \| "متوسط" \| "کم"` | yes | رنگ، آیکن، فیلتر فوری | +| `note` | `string` | no but recommended | متن توضیح داخل کارت | +| `tags` | `string[]` | yes | نمایش برچسب‌ها | +| `status` | `"open" \| "done"` | yes | نمایش/فیلتر و checkbox | + +## Date And Time Requirements + +- `scheduledDate` باید برای فرانت قابل parse باشد +- فرمت پیشنهادی برای `scheduledDate`: `YYYY-MM-DD` +- `time` بهتر است فرمت `HH:mm` داشته باشد +- چون datepicker صفحه شمسی است، فرانت می‌تواند تاریخ را شمسی انتخاب کند اما بهتر است به بک‌اند معادل میلادی استاندارد بفرستد +- پیشنهاد نهایی: بک‌اند و فرانت روی ذخیره `scheduledDate` به شکل میلادی `YYYY-MM-DD` توافق کنند + +## Recommended Task API + +### 1) List Farmer Todo Tasks + +`GET /api/farmer-todos` + +#### Query Params + +| Param | Type | Required | Description | +|---|---|---:|---| +| `status` | `open \| done` | no | فیلتر بر اساس وضعیت | +| `priority` | `high \| medium \| low` یا مقدار بومی | no | فیلتر اولویت | +| `date` | `YYYY-MM-DD` | no | فیلتر یک روز مشخص | +| `from` | `YYYY-MM-DD` | no | شروع بازه | +| `to` | `YYYY-MM-DD` | no | پایان بازه | +| `zone` | `string` | no | فیلتر بر اساس محل اجرا | +| `search` | `string` | no | جستجو در عنوان/یادداشت | + +#### Success Response + +```json +{ + "tasks": [ + { + "id": 101, + "title": "بررسی رطوبت ردیف شمالی", + "zone": "قطعه گندم - شمال مزرعه", + "scheduledDate": "2025-02-24", + "time": "06:30", + "priority": "زیاد", + "note": "اگر رطوبت کمتر از 28٪ بود، آبیاری دوباره بررسی شود.", + "tags": ["آبیاری", "صبح زود"], + "status": "open" + } + ], + "meta": { + "total": 1 + } +} +``` + +#### Frontend Notes + +- اگر `tasks` خالی باشد باید `[]` برگردد +- اگر بک‌اند enum داخلی دارد، بهتر است همینجا به مقادیر قابل‌استفاده فرانت map شود +- اگر بخواهید با `todoService` فعلی سازگار بمانید، یک adapter در frontend هم می‌تواند این mapping را انجام دهد + +### 2) Create New Farmer Todo Task + +`POST /api/farmer-todos` + +#### Request Body + +```json +{ + "title": "بازدید پمپ جنوب", + "zone": "قطعه گندم - شمال مزرعه", + "scheduledDate": "2025-02-24", + "time": "07:00", + "priority": "متوسط", + "note": "بعد از ثبت انجام، اگر موردی غیرعادی بود برای شیفت بعدی یادداشت بگذار.", + "tags": ["روزانه", "ثبت دستی"], + "status": "open" +} +``` + +#### Minimum Required Fields + +- `title` +- `zone` +- `scheduledDate` +- `time` +- `priority` + +#### Success Response + +```json +{ + "task": { + "id": 102, + "title": "بازدید پمپ جنوب", + "zone": "قطعه گندم - شمال مزرعه", + "scheduledDate": "2025-02-24", + "time": "07:00", + "priority": "متوسط", + "note": "بعد از ثبت انجام، اگر موردی غیرعادی بود برای شیفت بعدی یادداشت بگذار.", + "tags": ["روزانه", "ثبت دستی"], + "status": "open" + } +} +``` + +### 3) Update Task + +`PUT /api/farmer-todos/:id` + +#### Use Cases + +- تغییر وضعیت `open/done` +- ویرایش عنوان +- ویرایش زمان و روز +- ویرایش اولویت +- ویرایش محل اجرا +- ویرایش note و tags + +#### Request Body Example + +```json +{ + "status": "done" +} +``` + +یا: + +```json +{ + "title": "نمونه برداری خاک", + "zone": "گلخانه شماره 2", + "scheduledDate": "2025-02-24", + "time": "09:15", + "priority": "زیاد", + "note": "سه نقطه برداشت شود.", + "tags": ["خاک", "آزمایش"], + "status": "open" +} +``` + +#### Success Response + +```json +{ + "task": { + "id": 102, + "title": "نمونه برداری خاک", + "zone": "گلخانه شماره 2", + "scheduledDate": "2025-02-24", + "time": "09:15", + "priority": "زیاد", + "note": "سه نقطه برداشت شود.", + "tags": ["خاک", "آزمایش"], + "status": "open" + } +} +``` + +### 4) Delete Task + +اگرچه UI فعلی دکمه حذف ندارد، برای کامل شدن flow بهتر است endpoint حذف موجود باشد. + +`DELETE /api/farmer-todos/:id` + +#### Success Response + +```json +{ + "success": true +} +``` + +## Separate Zones API + +در UI فعلی `zone`ها hardcoded هستند. برای اتصال واقعی بهتر است از بک‌اند دریافت شوند. + +### 5) List Task Zones + +`GET /api/farmer-todos/zones` + +#### Success Response + +```json +{ + "zones": [ + { + "id": "zone_north_wheat", + "label": "قطعه گندم - شمال مزرعه", + "value": "قطعه گندم - شمال مزرعه" + }, + { + "id": "greenhouse_2", + "label": "گلخانه شماره 2", + "value": "گلخانه شماره 2" + }, + { + "id": "central_warehouse", + "label": "انبار مرکزی", + "value": "انبار مرکزی" + } + ] +} +``` + +#### Minimum Accepted Alternative + +```json +{ + "zones": [ + "قطعه گندم - شمال مزرعه", + "گلخانه شماره 2", + "انبار مرکزی" + ] +} +``` + +## Separate Tags API + +این صفحه `tags` را نمایش می‌دهد و برای create/edit هم می‌تواند به لیست آماده نیاز داشته باشد، بنابراین بهتر است API جداگانه داشته باشد. + +### 6) List Todo Tags + +`GET /api/farmer-todos/tags` + +#### Success Response + +```json +{ + "tags": [ + { + "id": "tag_irrigation", + "label": "آبیاری", + "value": "آبیاری" + }, + { + "id": "tag_daily", + "label": "روزانه", + "value": "روزانه" + } + ] +} +``` + +## Optional Summary API + +الان فرانت آمارهای زیر را از خود لیست taskها محاسبه می‌کند: + +- تعداد کارهای امروز +- تعداد انجام شده +- تعداد فوری +- درصد پیشرفت +- اولین تسک بعدی + +اگر بخواهید لود صفحه سبک‌تر شود، این API اختیاری مفید است: + +### 7) Farmer Todos Summary + +`GET /api/farmer-todos/summary` + +#### Success Response + +```json +{ + "todayTasksCount": 3, + "completedCount": 1, + "urgentCount": 2, + "progressValue": 25, + "nextTask": { + "id": 101, + "title": "بررسی رطوبت ردیف شمالی", + "zone": "قطعه گندم - شمال مزرعه", + "scheduledDate": "2025-02-24", + "time": "06:30", + "priority": "زیاد", + "note": "اگر رطوبت کمتر از 28٪ بود، آبیاری دوباره بررسی شود.", + "tags": ["آبیاری", "صبح زود"], + "status": "open" + } +} +``` + +این endpoint ضروری نیست، چون صفحه فعلی می‌تواند summary را از `GET /api/farmer-todos` بسازد. + +## Validation Rules Recommended + +- `title` نباید خالی باشد +- `zone` نباید خالی باشد +- `scheduledDate` باید تاریخ معتبر باشد +- `time` باید فرمت معتبر `HH:mm` داشته باشد +- `priority` باید یکی از `زیاد / متوسط / کم` یا enum معادل آن باشد +- `status` باید یکی از `open / done` باشد +- `tags` اگر ارسال می‌شود باید آرایه‌ای از string باشد + +## Error Handling + +ساختار پیشنهادی خطا: + +```json +{ + "code": "TASK_VALIDATION_ERROR", + "message": "Invalid farmer todo payload", + "details": { + "scheduledDate": "scheduledDate is required" + } +} +``` + +فرانت حداقل به یک `message` قابل‌نمایش نیاز دارد. + +## Mapping Notes If Reusing Existing Todo Service + +اگر بخواهید از `todoService` فعلی استفاده کنید، این mapping لازم می‌شود: + +| UI Field | Current `todoService` Field | +|---|---| +| `title` | `title` | +| `note` | `description` | +| `scheduledDate` | `startDate` یا `dueDate` | +| `time` | بهتر است field جداگانه داشته باشد؛ در `todoService` فعلی مستقیم وجود ندارد | +| `priority` | نیاز به mapping بین `high/medium/low` و `زیاد/متوسط/کم` | +| `status` | نیاز به mapping بین `pending/completed` و `open/done` | +| `tags` | `tags` | +| `zone` | field جداگانه لازم دارد؛ در `todoService` فعلی مستقیم وجود ندارد | + +### Important Note + +برای `farmer-todos` بهتر است endpoint/domain جداگانه داشته باشید، چون این صفحه به شکل صریح به `zone`, `scheduledDate`, `time` و اولویت محلی نیاز دارد و `todoService` عمومی فعلی دقیقاً این مدل را پوشش نمی‌دهد. + +## Final Backend Checklist + +- `GET /api/farmer-todos` +- `POST /api/farmer-todos` +- `PUT /api/farmer-todos/:id` +- `DELETE /api/farmer-todos/:id` +- `GET /api/farmer-todos/zones` +- `GET /api/farmer-todos/tags` +- پشتیبانی از `scheduledDate` +- پشتیبانی از `time` +- پشتیبانی از `zone` +- پشتیبانی از `priority` +- پشتیبانی از `status` +- پشتیبانی از `tags` + +## Suggested Stable Response Shapes + +### Tasks List + +```json +{ + "tasks": [ + { + "id": 101, + "title": "بررسی رطوبت ردیف شمالی", + "zone": "قطعه گندم - شمال مزرعه", + "scheduledDate": "2025-02-24", + "time": "06:30", + "priority": "زیاد", + "note": "اگر رطوبت کمتر از 28٪ بود، آبیاری دوباره بررسی شود.", + "tags": ["آبیاری", "صبح زود"], + "status": "open" + } + ], + "meta": { + "total": 1 + } +} +``` + +### Zones List + +```json +{ + "zones": [ + { + "id": "zone_north_wheat", + "label": "قطعه گندم - شمال مزرعه", + "value": "قطعه گندم - شمال مزرعه" + } + ] +} +``` + +### Tags List + +```json +{ + "tags": [ + { + "id": "tag_daily", + "label": "روزانه", + "value": "روزانه" + } + ] +} +``` diff --git a/messages/fa.json b/messages/fa.json index 443e654..e958371 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -873,5 +873,131 @@ "empty": "هنوز مکالمه‌ای ندارید", "chatLabel": "چت" } + }, + "farmerCalendar": { + "categories": { + "personal": "کارهای مزرعه", + "business": "عملیات", + "family": "تیم", + "holiday": "برداشت", + "etc": "پشتیبانی" + }, + "fallbacks": { + "untitledTask": "وظیفه بدون عنوان" + }, + "insights": { + "today": { + "label": "امروز", + "hint": "کارهای روی نقشه مزرعه" + }, + "thisWeek": { + "label": "این هفته", + "hint": "بازه های برنامه ریزی شده" + }, + "allDay": { + "label": "تمام روز", + "hint": "کارهای طولانی مدت" + } + }, + "sidebar": { + "planningPulse": "نبض برنامه ریزی", + "farmWeekAtGlance": "نمای سریع هفته مزرعه", + "navigator": { + "title": "مسیریاب", + "description": "به هر تاریخ بروید و روی بازه زمانی قابل مشاهده تمرکز کنید." + }, + "focusLanes": { + "title": "مسیرهای تمرکز", + "description": "رویدادها را بر اساس جریان کاری فیلتر کنید.", + "allVisible": "همه قابل مشاهده", + "activeCount": "{count} مورد فعال", + "clearFilters": "پاک کردن فیلترها", + "viewAll": "نمایش همه", + "categoryCount": "{label} ({count})" + }, + "upcomingActions": { + "title": "اقدام های پیش رو", + "description": "مهم ترین بازه های بعدی از تقویم شما.", + "empty": "هنوز رویداد آینده ای ندارید. با افزودن اقدام بعدی مزرعه شروع کنید." + } + }, + "hero": { + "badge": "مرکز برنامه ریزی کشاورز", + "title": "کارهای روزانه مزرعه را به یک ریتم تصویری روشن تبدیل کنید.", + "description": "عملیات، بازه های برداشت و فعالیت های تیم را در یک نما با تقویمی آرام تر و هدفمندتر مدیریت کنید.", + "addFarmEvent": "افزودن رویداد مزرعه", + "jumpToToday": "رفتن به امروز", + "openPlannerTools": "باز کردن ابزارهای برنامه ریزی", + "nextCheckpoint": "ایستگاه بعدی", + "noScheduledAction": "اقدام برنامه ریزی نشده", + "noScheduledActionDescription": "برای شروع ثبت آبیاری، پایش، برداشت یا برنامه های تیمی از دکمه افزودن استفاده کنید.", + "nextCheckpointAllDay": "{date} - تمام روز", + "nextCheckpointTimed": "{date} ساعت {time}", + "categoryCount": "{label}: {count}" + }, + "calendar": { + "title": "تقویم فرمان مزرعه", + "description": "رویدادها را مستقیما در بوم زمان بندی جابه جا، تغییر اندازه یا ایجاد کنید.", + "visibleCount": "{count} مورد قابل مشاهده", + "todayCount": "{count} مورد امروز", + "thisWeekCount": "{count} مورد این هفته", + "allDaySuffix": " - تمام روز", + "timeSuffix": " - {time}" + }, + "drawer": { + "title": "ابزارهای برنامه ریزی", + "description": "فیلترها، جابه جایی بین تاریخ ها و اقدام های پیش رو." + }, + "modal": { + "titleCreate": "افزودن رویداد جدید", + "titleEdit": "ویرایش رویداد", + "description": "جزئیات رویداد مزرعه را ثبت کنید.", + "close": "بستن", + "fields": { + "title": "عنوان", + "tag": "برچسب", + "tagPlaceholder": "انتخاب برچسب", + "startDate": "تاریخ شروع", + "endDate": "تاریخ پایان", + "allDay": "تمام روز", + "description": "توضیحات" + }, + "actions": { + "cancel": "انصراف", + "create": "افزودن رویداد", + "update": "بروزرسانی", + "delete": "حذف" + }, + "validation": { + "titleRequired": "عنوان رویداد الزامی است" + } + }, + "details": { + "title": "جزئیات رویداد", + "subtitle": "اطلاعات کامل رویداد انتخاب شده را ببینید.", + "close": "بستن", + "untitled": "رویداد بدون عنوان", + "allDay": "تمام روز", + "emptyTag": "برچسبی ثبت نشده است.", + "emptyDescription": "برای این رویداد توضیحی ثبت نشده است.", + "timeRange": "{start} تا {end}", + "fields": { + "date": "تاریخ", + "time": "زمان", + "tag": "برچسب", + "description": "توضیحات" + }, + "actions": { + "close": "بستن", + "edit": "ویرایش رویداد" + }, + "categories": { + "personal": "کارهای مزرعه", + "business": "عملیات", + "family": "تیم", + "holiday": "برداشت", + "etc": "پشتیبانی" + } + } } } diff --git a/package-lock.json b/package-lock.json index 08e57e1..031f311 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,7 +71,7 @@ "react-apexcharts": "1.4.1", "react-chartjs-2": "^5.3.1", "react-colorful": "5.6.1", - "react-date-object": "1.1.9", + "react-date-object": "^2.1.9", "react-datepicker": "7.3.0", "react-dom": "18.3.1", "react-dropzone": "14.3.5", @@ -9044,9 +9044,9 @@ } }, "node_modules/react-date-object": { - "version": "1.1.9", - "resolved": "https://mirror-npm.runflare.com/react-date-object/-/react-date-object-1.1.9.tgz", - "integrity": "sha512-NEbp/JSqwpF3nm4bXcez9XGwmlpOjSPJSYoRyPC1XOxzRHHaijp6xjMbYpyKB3O4yrluxsbwBHbO1WgeFKip3Q==", + "version": "2.1.9", + "resolved": "https://mirror-npm.runflare.com/react-date-object/-/react-date-object-2.1.9.tgz", + "integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==", "license": "MIT" }, "node_modules/react-datepicker": { @@ -9197,12 +9197,6 @@ "react-dom": ">=16.8.0" } }, - "node_modules/react-multi-date-picker/node_modules/react-date-object": { - "version": "2.1.9", - "resolved": "https://mirror-npm.runflare.com/react-date-object/-/react-date-object-2.1.9.tgz", - "integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==", - "license": "MIT" - }, "node_modules/react-onclickoutside": { "version": "6.13.1", "license": "MIT", diff --git a/package.json b/package.json index a247d0b..b48e8d7 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "react-apexcharts": "1.4.1", "react-chartjs-2": "^5.3.1", "react-colorful": "5.6.1", - "react-date-object": "1.1.9", + "react-date-object": "^2.1.9", "react-datepicker": "7.3.0", "react-dom": "18.3.1", "react-dropzone": "14.3.5", diff --git a/src/libs/styles/AppJalaliDatepicker.tsx b/src/libs/styles/AppJalaliDatepicker.tsx index f02081f..9de2101 100644 --- a/src/libs/styles/AppJalaliDatepicker.tsx +++ b/src/libs/styles/AppJalaliDatepicker.tsx @@ -9,7 +9,7 @@ import { useTheme } from '@mui/material/styles' import type { BoxProps } from '@mui/material/Box' // Third-party Imports -import { Calendar } from 'react-multi-date-picker' +import DatePicker from 'react-multi-date-picker' import DateObject from 'react-date-object' import persian from 'react-date-object/calendars/persian' import persian_fa from 'react-date-object/locales/persian_fa' @@ -20,11 +20,12 @@ import 'react-multi-date-picker/styles/colors/teal.css' interface AppJalaliDatepickerProps { value?: Date | null onChange?: (date: Date) => void + placeholder?: string boxProps?: BoxProps } const AppJalaliDatepicker = (props: AppJalaliDatepickerProps) => { - const { value: externalValue, onChange, boxProps } = props + const { value: externalValue, onChange, placeholder, boxProps } = props const theme = useTheme() const [internalValue, setInternalValue] = useState(() => { @@ -51,46 +52,18 @@ const AppJalaliDatepicker = (props: AppJalaliDatepickerProps) => { return ( - ) diff --git a/src/libs/styles/AppReactDatepicker.tsx b/src/libs/styles/AppReactDatepicker.tsx index 523a04c..ac19dc9 100644 --- a/src/libs/styles/AppReactDatepicker.tsx +++ b/src/libs/styles/AppReactDatepicker.tsx @@ -1,526 +1,264 @@ -'use client' +"use client"; -// React Imports -import type { ComponentProps } from 'react' +import type { ComponentProps, ReactElement } from "react"; +import { cloneElement, isValidElement } from "react"; -// MUI imports -import Box from '@mui/material/Box' -import { styled } from '@mui/material/styles' -import type { BoxProps } from '@mui/material/Box' +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import type { BoxProps } from "@mui/material/Box"; -// Third-party Imports -import ReactDatePickerComponent from 'react-datepicker' +import DatePicker, { Calendar } from "react-multi-date-picker"; +import type { DateObject } from "react-multi-date-picker"; +import type { Value } from "react-multi-date-picker"; +import TimePicker from "react-multi-date-picker/plugins/time_picker"; +import persian from "react-date-object/calendars/persian"; +import persian_fa from "react-date-object/locales/persian_fa"; -// Styles -import 'react-datepicker/dist/react-datepicker.css' +import "react-multi-date-picker/styles/colors/teal.css"; -type Props = ComponentProps & { - boxProps?: BoxProps -} +type Props = Omit< + ComponentProps, + "value" | "onChange" | "render" | "calendar" | "locale" +> & { + boxProps?: BoxProps; + selected?: Date | null; + startDate?: Date | null; + endDate?: Date | null; + selectsRange?: boolean; + showTimeSelect?: boolean; + customInput?: ReactElement; + placeholderText?: string; + inline?: boolean; + dateFormat?: string; + minDate?: Date | string | number | null; + maxDate?: Date | string | number | null; + shouldCloseOnSelect?: boolean; + onChange?: ( + value: Date | [Date | null, Date | null] | null, + event?: unknown, + ) => void; + onSelect?: (value: Date | null) => void; +}; -// Styled Components -const StyledReactDatePicker = styled(Box)(({ theme }) => { - return { - '& .react-datepicker-popper': { - zIndex: 20, - paddingTop: `${theme.spacing(0.5)} !important` - }, - '& .react-datepicker-wrapper': { - width: '100%' - }, - '& .react-datepicker__triangle': { - display: 'none' - }, - '& .react-datepicker': { - color: 'var(--mui-palette-text-primary)', - borderRadius: 'var(--mui-shape-borderRadius)', - fontFamily: theme.typography.fontFamily, - backgroundColor: 'var(--mui-palette-background-paper)', - boxShadow: 'var(--mui-customShadows-md)', - border: 'none', - '& .react-datepicker__header': { - padding: 0, - border: 'none', - fontWeight: 'normal', - backgroundColor: 'var(--mui-palette-background-paper)', - '& .react-datepicker__current-month, &.react-datepicker-year-header': { - textAlign: 'left' - }, - '&:not(.react-datepicker-year-header)': { - '& + .react-datepicker__month, & + .react-datepicker__year': { - margin: theme.spacing(2), - marginTop: theme.spacing(4.5) - } - }, - '&.react-datepicker-year-header': { - '& + .react-datepicker__month, & + .react-datepicker__year': { - margin: theme.spacing(2), - marginTop: theme.spacing(0) - } - } - }, - '& > .react-datepicker__navigation': { - top: 13, - borderRadius: '50%', - backgroundColor: 'var(--mui-palette-action-selected)', - '&.react-datepicker__navigation--previous': { - width: 30, - height: 30, - border: 'none', - top: 12, - left: 'auto', - right: '57px', - ...(theme.direction === 'ltr' - ? { - backgroundImage: - "url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")", - ...theme.applyStyles('dark', { - backgroundImage: - "url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")" - }) - } - : { - backgroundImage: - "url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")", - ...theme.applyStyles('dark', { - backgroundImage: - "url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")" - }) - }), - '& .react-datepicker__navigation-icon': { - display: 'none' - }, - '&:has(+ .react-datepicker__navigation--next--with-time)': - theme.direction === 'ltr' ? { right: 177 } : { left: 177 } - }, - '&.react-datepicker__navigation--next': { - width: 30, - height: 30, - border: 'none', - top: 12, - right: 15, - left: 'auto', - ...(theme.direction === 'ltr' - ? { - backgroundImage: - "url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")", - ...theme.applyStyles('dark', { - backgroundImage: - "url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")" - }) - } - : { - backgroundImage: - "url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")", - ...theme.applyStyles('dark', { - backgroundImage: - "url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")" - }) - }), - '& .react-datepicker__navigation-icon': { - display: 'none' - } - }, - '&.react-datepicker__navigation--next--with-time': theme.direction === 'ltr' ? { right: 135 } : { left: 135 }, - '&:focus, &:active': { - outline: 0 - } - }, - '& .react-datepicker__current-month, & .react-datepicker-year-header': { - ...theme.typography.subtitle1, - lineHeight: 2, - paddingBlockStart: theme.spacing(3), - paddingBlockEnd: theme.spacing(4.5), - paddingInline: theme.spacing(4), - color: 'var(--mui-palette-text-primary)' - }, - '& .react-datepicker__day-name': { - margin: 0, - width: '2.25rem', - ...theme.typography.subtitle2, - color: 'var(--mui-palette-text-primary)' - }, - '& .react-datepicker__day-names': { - marginBottom: 0 - }, - '& .react-datepicker__day': { - margin: 0, - width: '2.25rem', - borderRadius: '50%', - lineHeight: '2.25rem', - color: 'var(--mui-palette-text-primary)', - fontSize: theme.typography.body1.fontSize, - '&.react-datepicker__day--selected.react-datepicker__day--in-selecting-range.react-datepicker__day--selecting-range-start, &.react-datepicker__day--selected.react-datepicker__day--range-start.react-datepicker__day--in-range, &.react-datepicker__day--range-start': - { - borderRadius: '18px 0px 0px 18px;', - color: 'var(--mui-palette-common-white) !important', - backgroundColor: 'var(--mui-palette-primary-main) !important' - }, - '&.react-datepicker__day--range-end.react-datepicker__day--in-range': { - borderRadius: '0px 18px 18px 0px', - color: 'var(--mui-palette-common-white) !important', - backgroundColor: 'var(--mui-palette-primary-main) !important' - }, - '&:focus, &:active': { - outline: 0 - }, - '&.react-datepicker__day--outside-month, &.react-datepicker__day--disabled:not(.react-datepicker__day--selected)': - { - color: 'var(--mui-palette-text-disabled)', - '&:hover': { - backgroundColor: 'transparent' - } - }, - '&.react-datepicker__day--highlighted, &.react-datepicker__day--highlighted:hover': { - color: 'var(--mui-palette-success-main)', - backgroundColor: 'var(--mui-palette-success-lightOpacity)', - '&.react-datepicker__day--selected': { - backgroundColor: 'var(--mui-palette-primary-main) !important' - } - } - }, - '&:has(.react-datepicker__day--in-range)': { - '& > .react-datepicker__navigation': { - '&.react-datepicker__navigation--previous': { - ...(theme.direction === 'ltr' ? { left: 15 } : { right: 15 }) - } - }, - '& .react-datepicker__header': { - '& .react-datepicker__current-month': { - textAlign: 'center' - } - } - }, - '& .react-datepicker__day--in-range, & .react-datepicker__day--in-selecting-range': { - borderRadius: 0, - color: 'var(--mui-palette-primary-main) !important', - backgroundColor: 'var(--mui-palette-primary-lightOpacity) !important' - }, - '& .react-datepicker__day--today': { - fontWeight: 'normal', - '&:not(.react-datepicker__day--selected):not(:empty)': { - color: 'var(--mui-palette-primary-main)', - backgroundColor: 'var(--mui-palette-primary-lightOpacity)', - '&:hover': { - backgroundColor: 'var(--mui-palette-primary-mainOpacity)' - }, - '&.react-datepicker__day--keyboard-selected': { - backgroundColor: 'var(--mui-palette-primary-lightOpacity)', - '&:hover': { - backgroundColor: 'var(--mui-palette-primary-lightOpacity)' - } - } - } - }, - '& .react-datepicker__month-text--today': { - fontWeight: 'normal', - '&:not(.react-datepicker__month-text--selected)': { - lineHeight: '2.125rem', - color: 'var(--mui-palette-primary-main)', - border: '1px solid var(--mui-palette-primary-main)', - '&:hover': { - backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.04)' - } - } - }, - '& .react-datepicker__year-text--today': { - fontWeight: 'normal', - '&:not(.react-datepicker__year-text--selected)': { - lineHeight: '2.125rem', - color: 'var(--mui-palette-primary-main)', - border: '1px solid var(--mui-palette-primary-main)', - '&:hover': { - backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.04)' - }, - '&.react-datepicker__year-text--keyboard-selected': { - color: 'var(--mui-palette-primary-main)', - backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.06)', - '&:hover': { - color: 'var(--mui-palette-primary-main)', - backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.06)' - } - } - } - }, - '& .react-datepicker__day--keyboard-selected': { - '&:not(.react-datepicker__day--in-range)': { - color: 'var(--mui-palette-primary-main)', - backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)', - '&:hover': { - backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)' - } - } - }, - '& .react-datepicker__month-text--keyboard-selected': { - '&:not(.react-datepicker__month--in-range)': { - color: 'var(--mui-palette-primary-main)', - backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)', - '&:hover': { - backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)' - } - } - }, - '& .react-datepicker__year-text--keyboard-selected, & .react-datepicker__quarter-text--keyboard-selected': { - color: 'var(--mui-palette-primary-main)', - backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)' - }, - '& .react-datepicker__day--selected, & .react-datepicker__month-text--selected, & .react-datepicker__year-text--selected, & .react-datepicker__quarter-text--selected': - { - color: 'var(--mui-palette-common-white) !important', - backgroundColor: 'var(--mui-palette-primary-main) !important', - boxShadow: 'var(--mui-customShadows-primary-sm)', - '&:hover': { - backgroundColor: 'var(--mui-palette-primary-dark) !important' - } - }, - '& .react-datepicker__header__dropdown': { - '& .react-datepicker__month-dropdown-container:not(:last-child)': { - marginRight: theme.spacing(8) - }, - '& .react-datepicker__month-dropdown-container, & .react-datepicker__year-dropdown-container': { - marginBottom: theme.spacing(4) - }, - '& .react-datepicker__month-read-view--selected-month, & .react-datepicker__year-read-view--selected-year': { - fontSize: '0.875rem', - marginRight: theme.spacing(1), - color: 'var(--mui-palette-text-primary)' - }, - '& .react-datepicker__month-read-view:hover .react-datepicker__month-read-view--down-arrow, & .react-datepicker__year-read-view:hover .react-datepicker__year-read-view--down-arrow': - { - borderColor: 'var(--mui-palette-text-primary)' - }, - '& .react-datepicker__month-read-view--down-arrow, & .react-datepicker__year-read-view--down-arrow': { - top: 4, - borderColor: 'var(--mui-palette-text-secondary)' - }, - '& .react-datepicker__month-dropdown, & .react-datepicker__year-dropdown': { - padding: theme.spacing(2), - border: 'none', - borderRadius: 'var(--mui-shape-borderRadius)', - backgroundColor: 'var(--mui-palette-background-paper)', - boxShadow: 'var(--mui-customShadows-lg)', - '[data-skin="bordered"] &': { - boxShadow: 'none', - border: `1px solid var(--mui-palette-divider)` - } - }, - '& .react-datepicker__month-option, & .react-datepicker__year-option': { - ...theme.typography.body1, - padding: theme.spacing(1.5, 4), - borderRadius: 'var(--mui-shape-borderRadius)', - marginBlockEnd: theme.spacing(0.5), - '&:hover': { - backgroundColor: 'var(--mui-palette-action-hover)' - } - }, - '& .react-datepicker__month-option.react-datepicker__month-option--selected_month': { - color: 'var(--mui-palette-primary-main)', - backgroundColor: 'var(--mui-palette-primary-lightOpacity)', - '&:hover': { - backgroundColor: 'var(--mui-palette-primary-lightOpacity)' - }, - '& .react-datepicker__month-option--selected': { - display: 'none' - } - }, - '& .react-datepicker__year-option.react-datepicker__year-option--selected_year': { - color: 'var(--mui-palette-primary-main)', - backgroundColor: 'var(--mui-palette-primary-lightOpacity)', - '&:hover': { - backgroundColor: 'var(--mui-palette-primary-lightOpacity)' - }, - '& .react-datepicker__year-option--selected': { - display: 'none' - } - }, - '& .react-datepicker__year-option': { - // TODO: Remove some of the following styles for arrow in Year dropdown when react-datepicker give arrows in Year dropdown - '& .react-datepicker__navigation--years-upcoming': { - width: 9, - height: 9, - borderStyle: 'solid', - borderWidth: '3px 3px 0 0', - transform: 'rotate(-45deg)', - borderTopColor: 'var(--mui-palette-text-secondary)', - borderRightColor: 'var(--mui-palette-text-secondary)', - margin: `${theme.spacing(2.75)} auto ${theme.spacing(0)}` - }, - '&:hover .react-datepicker__navigation--years-upcoming': { - borderTopColor: 'var(--mui-palette-text-primary)', - borderRightColor: 'var(--mui-palette-text-primary)' - }, - '& .react-datepicker__navigation--years-previous': { - width: 9, - height: 9, - borderStyle: 'solid', - borderWidth: '0 0 3px 3px', - transform: 'rotate(-45deg)', - borderLeftColor: 'var(--mui-palette-text-secondary)', - borderBottomColor: 'var(--mui-palette-text-secondary)', - margin: `${theme.spacing(0)} auto ${theme.spacing(2.75)}` - }, - '&:hover .react-datepicker__navigation--years-previous': { - borderLeftColor: 'var(--mui-palette-text-primary)', - borderBottomColor: 'var(--mui-palette-text-primary)' - } - } - }, - '& .react-datepicker__week-number': { - margin: 0, - fontWeight: 500, - width: '2.25rem', - lineHeight: '2.25rem', - fontSize: theme.typography.body2.fontSize, - color: 'var(--mui-palette-text-primary)' - }, - '& .react-datepicker__month-text, & .react-datepicker__year-text, & .react-datepicker__quarter-text': { - margin: 0, - alignItems: 'center', - fontSize: theme.typography.body1.fontSize, - lineHeight: '2rem', - display: 'inline-flex', - justifyContent: 'center', - borderRadius: 'var(--mui-shape-borderRadius)', - '&:focus, &:active': { - outline: 0 - } - }, - '& .react-datepicker__year-wrapper': { - maxWidth: 205, - justifyContent: 'center' - }, - '& .react-datepicker__input-time-container': { - display: 'flex', - alignItems: 'center', - ...(theme.direction === 'rtl' ? { flexDirection: 'row-reverse' } : {}) - }, - '& .react-datepicker__today-button': { - borderTop: 0, - borderRadius: '1rem', - margin: theme.spacing(0, 4, 4), - color: 'var(--mui-palette-common-white)', - backgroundColor: 'var(--mui-palette-primary-main)' - }, +const convertFormat = (format?: string) => { + if (!format) return "YYYY/MM/DD"; - // Time Picker - '&:not(.react-datepicker--time-only)': { - '& .react-datepicker__time-container': { - borderLeftColor: 'var(--mui-palette-divider)', - [theme.breakpoints.down('sm')]: { - width: '5.5rem' - }, - [theme.breakpoints.up('sm')]: { - width: '7.4375rem' - } - } - }, - '&.react-datepicker--time-only': { - width: '7.4375rem', - '& .react-datepicker__time-container': { - width: '7.4375rem' - } - }, - '& .react-datepicker__time-container': { - padding: theme.spacing(0.75, 0), - '& .react-datepicker-time__header': { - ...theme.typography.subtitle2, - marginBottom: theme.spacing(3.5), - marginTop: theme.spacing(3.5), - color: 'var(--mui-palette-text-primary)' - }, + return format + .replace(/yyyy/g, "YYYY") + .replace(/yy/g, "YY") + .replace(/dd/g, "DD") + .replace(/d/g, "D") + .replace(/HH/g, "HH") + .replace(/hh/g, "HH") + .replace(/mm/g, "mm"); +}; - '& .react-datepicker__time': { - background: 'var(--mui-palette-background-paper)', - '& .react-datepicker__time-box .react-datepicker__time-list-item--disabled': { - pointerEvents: 'none', - color: 'var(--mui-palette-text-disabled)', - '&.react-datepicker__time-list-item--selected': { - fontWeight: 'normal', - backgroundColor: 'var(--mui-palette-action-disabledBackground)' - } - } - }, +const toDate = (value: Value | null | undefined): Date | null => { + if (!value) return null; + if (value instanceof Date) return value; + if (typeof value === "string" || typeof value === "number") { + const parsed = new Date(value); - '& .react-datepicker__time-list-item': { - height: 'auto !important', - padding: `${theme.spacing(1.75, 0)} !important`, - marginLeft: theme.spacing(4.25), - marginRight: theme.spacing(2.2), - ...theme.typography.body1, - color: 'var(--mui-palette-text-primary)', - borderRadius: 'var(--mui-shape-borderRadius)', - '&:focus, &:active': { - outline: 0 - }, - '&:hover': { - backgroundColor: 'var(--mui-palette-action-hover) !important' - }, - '&.react-datepicker__time-list-item--selected:not(.react-datepicker__time-list-item--disabled)': { - fontWeight: 'normal', - color: 'var(--mui-palette-common-white) !important', - backgroundColor: 'var(--mui-palette-primary-main) !important', - boxShadow: 'var(--mui-customShadows-xs)' - } - }, - - '& .react-datepicker__time-box': { - width: '100%' - }, - '& .react-datepicker__time-list': { - '&::-webkit-scrollbar': { - width: 8 - }, - - /* Track */ - '&::-webkit-scrollbar-track': { - background: 'var(--mui-palette-background-paper)' - }, - - /* Handle */ - '&::-webkit-scrollbar-thumb': { - borderRadius: 10, - background: '#aaa' - }, - - /* Handle on hover */ - '&::-webkit-scrollbar-thumb:hover': { - background: '#999' - } - } - }, - '& .react-datepicker__day:hover, & .react-datepicker__month-text:hover, & .react-datepicker__quarter-text:hover, & .react-datepicker__year-text:hover': - { - backgroundColor: 'var(--mui-palette-action-hover)' - }, - '[data-skin="bordered"] &': { - boxShadow: 'none', - border: `1px solid var(--mui-palette-divider)` - } - }, - '& .react-datepicker__close-icon': { - top: 10, - paddingRight: theme.spacing(4), - ...(theme.direction === 'rtl' ? { right: 0, left: 'auto' } : {}), - '&:after': { - width: 'unset', - height: 'unset', - fontSize: '1.5rem', - color: 'var(--mui-palette-text-primary)', - backgroundColor: 'transparent !important' - } - } + return Number.isNaN(parsed.getTime()) ? null : parsed; } -}) as typeof Box + if (Array.isArray(value)) return null; + if (typeof (value as DateObject).toDate === "function") + return (value as DateObject).toDate(); + + return null; +}; + +const baseSx: BoxProps["sx"] = { + "& .rmdp-container": { + width: "100%", + }, + "& .rmdp-wrapper, & .rmdp-calendar": { + width: "100%", + border: "1px solid var(--mui-palette-divider)", + borderRadius: "var(--mui-shape-borderRadius)", + backgroundColor: "var(--mui-palette-background-paper)", + boxShadow: "var(--mui-customShadows-md)", + color: "var(--mui-palette-text-primary)", + }, + "& .rmdp-shadow": { + boxShadow: "var(--mui-customShadows-md)", + }, + "& .rmdp-top-class": { + width: "100%", + }, + "& .rmdp-header-values": { + color: "var(--mui-palette-text-primary)", + fontWeight: 700, + }, + "& .rmdp-arrow-container:hover": { + backgroundColor: "var(--mui-palette-primary-lightOpacity)", + }, + "& .rmdp-arrow": { + borderColor: "var(--mui-palette-text-secondary)", + }, + "& .rmdp-week-day": { + color: "var(--mui-palette-text-secondary)", + fontWeight: 700, + }, + "& .rmdp-day, & .rmdp-time-picker div input": { + color: "var(--mui-palette-text-primary)", + }, + "& .rmdp-day.rmdp-today span": { + backgroundColor: "var(--mui-palette-primary-lightOpacity)", + color: "var(--mui-palette-primary-main)", + }, + "& .rmdp-day.rmdp-selected span:not(.highlight)": { + backgroundColor: "var(--mui-palette-primary-main)", + color: "var(--mui-palette-common-white)", + }, + "& .rmdp-range, & .rmdp-range.start span, & .rmdp-range.end span": { + backgroundColor: "var(--mui-palette-primary-main)", + color: "var(--mui-palette-common-white)", + }, + "& .rmdp-day.rmdp-disabled, & .rmdp-day.rmdp-deactive": { + color: "var(--mui-palette-text-disabled)", + }, + "& .rmdp-input": { + width: "100%", + minHeight: 42, + padding: "10px 12px", + borderRadius: "var(--mui-shape-borderRadius)", + border: "1px solid var(--mui-palette-divider)", + backgroundColor: "var(--mui-palette-background-paper)", + color: "var(--mui-palette-text-primary)", + font: "inherit", + }, + "& .rmdp-input:focus": { + borderColor: "var(--mui-palette-primary-main)", + boxShadow: "0 0 0 2px var(--mui-palette-primary-lightOpacity)", + outline: "none", + }, + "& .rmdp-time-picker": { + padding: "8px 12px", + }, + "& .rmdp-time-picker div input": { + border: "1px solid var(--mui-palette-divider)", + borderRadius: 8, + backgroundColor: "var(--mui-palette-background-paper)", + }, +}; const AppReactDatepicker = (props: Props) => { - // Props - const { boxProps, ...rest } = props + const { + boxProps, + selected, + startDate, + endDate, + selectsRange, + showTimeSelect, + customInput, + placeholderText, + onChange, + onSelect, + inline, + dateFormat, + minDate, + maxDate, + shouldCloseOnSelect, + ...rest + } = props; + + const theme = useTheme(); + const format = convertFormat(dateFormat); + const plugins = showTimeSelect + ? [] + : []; + const rangeValue = + startDate && endDate ? [startDate, endDate] : startDate ? [startDate] : []; + const value = selectsRange ? rangeValue : (selected ?? null); + + const handleChange = (nextValue: Value) => { + if (selectsRange) { + const dates = Array.isArray(nextValue) + ? nextValue.map((item) => toDate(item as Value)) + : []; + const normalized: [Date | null, Date | null] = [ + dates[0] ?? null, + dates[1] ?? null, + ]; + + onChange?.(normalized); + + return; + } + + const normalized = toDate(nextValue); + + onChange?.(normalized); + onSelect?.(normalized); + }; + + const renderInput = + customInput && isValidElement(customInput) + ? (valueText: string, openCalendar: () => void) => { + const existingProps = customInput.props as Record; + + return ( + + {cloneElement(customInput, { + ...existingProps, + value: existingProps.value ?? valueText, + onClick: openCalendar, + onFocus: openCalendar, + readOnly: true, + placeholder: existingProps.placeholder ?? placeholderText, + slotProps: { + ...(typeof existingProps.slotProps === "object" && + existingProps.slotProps + ? existingProps.slotProps + : {}), + input: { + ...(existingProps.slotProps?.input || {}), + readOnly: true, + }, + }, + })} + + ); + } + : undefined; + + const sharedProps = { + value, + onChange: handleChange, + calendar: persian, + locale: persian_fa, + format, + minDate, + maxDate, + plugins, + weekStartDayIndex: 6, + calendarPosition: + theme.direction === "rtl" ? "bottom-right" : "bottom-left", + shadow: true, + className: "teal", + ...(shouldCloseOnSelect === false ? { closeOnScroll: false } : {}), + ...rest, + }; return ( - - - - ) -} + + {inline ? ( + + ) : ( + + )} + + ); +}; -export default AppReactDatepicker +export default AppReactDatepicker; diff --git a/src/views/apps/calendar/AddEventSidebar.tsx b/src/views/apps/calendar/AddEventSidebar.tsx index 6904a4f..952bcf3 100644 --- a/src/views/apps/calendar/AddEventSidebar.tsx +++ b/src/views/apps/calendar/AddEventSidebar.tsx @@ -1,74 +1,87 @@ // React Imports -import { useState, useEffect, forwardRef, useCallback } from 'react' +import { useState, useEffect, forwardRef, useCallback } from "react"; // MUI Imports -import Box from '@mui/material/Box' -import Drawer from '@mui/material/Drawer' -import Switch from '@mui/material/Switch' -import Button from '@mui/material/Button' -import MenuItem from '@mui/material/MenuItem' -import IconButton from '@mui/material/IconButton' -import Typography from '@mui/material/Typography' -import useMediaQuery from '@mui/material/useMediaQuery' -import FormControl from '@mui/material/FormControl' -import FormControlLabel from '@mui/material/FormControlLabel' -import type { SelectChangeEvent } from '@mui/material/Select' -import type { Theme } from '@mui/material/styles' +import Box from "@mui/material/Box"; +import Drawer from "@mui/material/Drawer"; +import Switch from "@mui/material/Switch"; +import Button from "@mui/material/Button"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import FormControl from "@mui/material/FormControl"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import type { Theme } from "@mui/material/styles"; // Third-party Imports -import { useForm, Controller } from 'react-hook-form' -import PerfectScrollbar from 'react-perfect-scrollbar' +import { useForm, Controller } from "react-hook-form"; +import PerfectScrollbar from "react-perfect-scrollbar"; // Type Imports -import type { AddEventSidebarType, AddEventType } from '@/types/apps/calendarTypes' +import type { + AddEventSidebarType, + AddEventType, +} from "@/types/apps/calendarTypes"; // Component Imports -import CustomTextField from '@core/components/mui/TextField' +import CustomTextField from "@core/components/mui/TextField"; // Styled Component Imports -import AppReactDatepicker from '@/libs/styles/AppReactDatepicker' +import AppReactDatepicker from "@/libs/styles/AppReactDatepicker"; // Slice Imports -import { createEventAsync, updateEventAsync, deleteEventAsync, selectedEvent, filterEvents } from '@/redux-store/slices/calendar' +import { + addEvent, + deleteEvent, + updateEvent, + selectedEvent, + filterEvents, +} from "@/redux-store/slices/calendar"; interface PickerProps { - label?: string - error?: boolean - registername?: string + label?: string; + error?: boolean; + registername?: string; } interface DefaultStateType { - url: string - title: string - allDay: boolean - calendar: string - description: string - endDate: Date - startDate: Date - guests: string[] | undefined + url: string; + title: string; + allDay: boolean; + calendar: string; + description: string; + endDate: Date; + startDate: Date; + guests: string[] | undefined; } // Vars -const capitalize = (string: string) => string && string[0].toUpperCase() + string.slice(1) +const capitalize = (string: string) => + string && string[0].toUpperCase() + string.slice(1); // Vars const defaultState: DefaultStateType = { - url: '', - title: '', + url: "", + title: "", guests: [], allDay: true, - description: '', + description: "", endDate: new Date(), - calendar: 'Business', - startDate: new Date() -} + calendar: "Business", + startDate: new Date(), +}; const AddEventSidebar = (props: AddEventSidebarType) => { // Props - const { calendarStore, dispatch, addEventSidebarOpen, handleAddEventSidebarToggle } = props + const { + calendarStore, + dispatch, + addEventSidebarOpen, + handleAddEventSidebarToggle, + } = props; // States - const [values, setValues] = useState(defaultState) + const [values, setValues] = useState(defaultState); // Refs const PickersComponent = forwardRef(({ ...props }: PickerProps, ref) => { @@ -77,109 +90,108 @@ const AddEventSidebar = (props: AddEventSidebarType) => { inputRef={ref} fullWidth {...props} - label={props.label || ''} - className='is-full' + label={props.label || ""} + className="is-full" error={props.error} /> - ) - }) + ); + }); // Hooks - const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')) + const isBelowSmScreen = useMediaQuery((theme: Theme) => + theme.breakpoints.down("sm"), + ); const { control, setValue, clearErrors, handleSubmit, - formState: { errors } - } = useForm({ defaultValues: { title: '' } }) + formState: { errors }, + } = useForm({ defaultValues: { title: "" } }); const resetToStoredValues = useCallback(() => { if (calendarStore.selectedEvent !== null) { - const event = calendarStore.selectedEvent + const event = calendarStore.selectedEvent; - setValue('title', event.title || '') + setValue("title", event.title || ""); setValues({ - url: event.url || '', - title: event.title || '', + url: event.url || "", + title: event.title || "", allDay: event.allDay, guests: event.extendedProps.guests || [], - description: event.extendedProps.description || '', - calendar: event.extendedProps.calendar || 'Business', + description: event.extendedProps.description || "", + calendar: event.extendedProps.calendar || "Business", endDate: event.end !== null ? event.end : event.start, - startDate: event.start !== null ? event.start : new Date() - }) + startDate: event.start !== null ? event.start : new Date(), + }); } - }, [setValue, calendarStore.selectedEvent]) + }, [setValue, calendarStore.selectedEvent]); const resetToEmptyValues = useCallback(() => { - setValue('title', '') - setValues(defaultState) - }, [setValue]) + setValue("title", ""); + setValues(defaultState); + }, [setValue]); const handleSidebarClose = () => { - setValues(defaultState) - clearErrors() - dispatch(selectedEvent(null)) - handleAddEventSidebarToggle() - } + setValues(defaultState); + clearErrors(); + dispatch(selectedEvent(null)); + handleAddEventSidebarToggle(); + }; const onSubmit = (data: { title: string }) => { const modifiedEvent: AddEventType = { url: values.url, - display: 'block', + display: "block", title: data.title, end: values.endDate, allDay: values.allDay, start: values.startDate, extendedProps: { calendar: capitalize(values.calendar), - guests: values.guests && values.guests.length ? values.guests : undefined, - description: values.description.length ? values.description : undefined - } - } - - const eventData = { - title: modifiedEvent.title || '', - description: modifiedEvent.extendedProps?.description || '', - calendar: (modifiedEvent.extendedProps?.calendar || 'Business') as 'Personal' | 'Business' | 'Family' | 'Holiday' | 'ETC', - start: modifiedEvent.start ? new Date(modifiedEvent.start).toISOString() : new Date().toISOString(), - end: modifiedEvent.end ? new Date(modifiedEvent.end).toISOString() : new Date().toISOString(), - allDay: modifiedEvent.allDay || false, - tags: modifiedEvent.extendedProps?.tags || [], - deadline: modifiedEvent.extendedProps?.deadline || Math.floor(Date.now() / 1000), - extendedProps: modifiedEvent.extendedProps || {} - } + guests: + values.guests && values.guests.length ? values.guests : undefined, + description: values.description.length ? values.description : undefined, + }, + }; if ( calendarStore.selectedEvent === null || - (calendarStore.selectedEvent !== null && !calendarStore.selectedEvent.title.length) + (calendarStore.selectedEvent !== null && + !calendarStore.selectedEvent.title.length) ) { - dispatch(createEventAsync(eventData)) + dispatch(addEvent(modifiedEvent)); } else { - dispatch(updateEventAsync({ id: calendarStore.selectedEvent.id, data: eventData })) + dispatch( + updateEvent({ ...modifiedEvent, id: calendarStore.selectedEvent.id }), + ); } - dispatch(filterEvents()) + dispatch(filterEvents()); - handleSidebarClose() - } + handleSidebarClose(); + }; const handleDeleteButtonClick = () => { if (calendarStore.selectedEvent) { - dispatch(deleteEventAsync(calendarStore.selectedEvent.id)) - dispatch(filterEvents()) + dispatch(deleteEvent(calendarStore.selectedEvent.id)); + dispatch(filterEvents()); } - handleSidebarClose() - } + // calendarApi.getEventById(calendarStore.selectedEvent.id).remove() + handleSidebarClose(); + }; const handleStartDate = (date: Date | null) => { if (date && date > values.endDate) { - setValues({ ...values, startDate: new Date(date), endDate: new Date(date) }) + setValues({ + ...values, + startDate: new Date(date), + endDate: new Date(date), + }); } - } + }; const RenderSidebarFooter = () => { if ( @@ -187,184 +199,180 @@ const AddEventSidebar = (props: AddEventSidebarType) => { (calendarStore.selectedEvent && !calendarStore.selectedEvent.title.length) ) { return ( -
- -
- ) + ); } else { return ( -
- -
- ) + ); } - } + }; - const ScrollWrapper = isBelowSmScreen ? 'div' : PerfectScrollbar + const ScrollWrapper = isBelowSmScreen ? "div" : PerfectScrollbar; useEffect(() => { if (calendarStore.selectedEvent !== null) { - resetToStoredValues() + resetToStoredValues(); } else { - resetToEmptyValues() + resetToEmptyValues(); } - }, [addEventSidebarOpen, resetToStoredValues, resetToEmptyValues, calendarStore.selectedEvent]) + }, [ + addEventSidebarOpen, + resetToStoredValues, + resetToEmptyValues, + calendarStore.selectedEvent, + ]); return ( - - - {calendarStore.selectedEvent && calendarStore.selectedEvent.title.length ? 'Update Event' : 'Add Event'} + + + {calendarStore.selectedEvent && + calendarStore.selectedEvent.title.length + ? "Update Event" + : "Add Event"} - {calendarStore.selectedEvent && calendarStore.selectedEvent.title.length ? ( - - - + {calendarStore.selectedEvent && + calendarStore.selectedEvent.title.length ? ( + + + - - + + ) : ( - - + + )} - -
+ + ( )} /> - setValues({ ...values, calendar: e.target.value })} - > - Personal - Business - Family - Holiday - ETC - - } - onChange={(date: Date | null) => date !== null && setValues({ ...values, startDate: new Date(date) })} + dateFormat={!values.allDay ? "yyyy-MM-dd hh:mm" : "yyyy-MM-dd"} + customInput={ + + } + onChange={(date: Date | null) => + date !== null && + setValues({ ...values, startDate: new Date(date) }) + } onSelect={handleStartDate} /> } - onChange={(date: Date | null) => date !== null && setValues({ ...values, endDate: new Date(date) })} + dateFormat={!values.allDay ? "yyyy-MM-dd hh:mm" : "yyyy-MM-dd"} + customInput={ + + } + onChange={(date: Date | null) => + date !== null && + setValues({ ...values, endDate: new Date(date) }) + } /> setValues({ ...values, allDay: e.target.checked })} /> + + setValues({ ...values, allDay: e.target.checked }) + } + /> } /> - setValues({ ...values, url: e.target.value })} - /> - ) => { - setValues({ - ...values, - guests: typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value - }) - }} - slotProps={{ - select: { - multiple: true - } - }} - > - Bruce - Clark - Diana - John - Barry - setValues({ ...values, description: e.target.value })} + onChange={(e) => + setValues({ ...values, description: e.target.value }) + } /> -
+
- ) -} + ); +}; -export default AddEventSidebar +export default AddEventSidebar; diff --git a/src/views/apps/calendar/Calendar.tsx b/src/views/apps/calendar/Calendar.tsx index 6c6ae71..6db1411 100644 --- a/src/views/apps/calendar/Calendar.tsx +++ b/src/views/apps/calendar/Calendar.tsx @@ -1,53 +1,68 @@ // React Imports -import { useEffect, useRef } from 'react' +import { useEffect, useRef } from "react"; // MUI Imports -import { useTheme } from '@mui/material/styles' +import { useTheme } from "@mui/material/styles"; // Third-party imports -import 'bootstrap-icons/font/bootstrap-icons.css' +import type { Dispatch } from "@reduxjs/toolkit"; +import "bootstrap-icons/font/bootstrap-icons.css"; -import FullCalendar from '@fullcalendar/react' -import faLocale from '@fullcalendar/core/locales/fa' -import listPlugin from '@fullcalendar/list' -import dayGridPlugin from '@fullcalendar/daygrid' -import timeGridPlugin from '@fullcalendar/timegrid' -import interactionPlugin from '@fullcalendar/interaction' -import type { CalendarOptions } from '@fullcalendar/core' +import FullCalendar from "@fullcalendar/react"; +import listPlugin from "@fullcalendar/list"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import timeGridPlugin from "@fullcalendar/timegrid"; +import interactionPlugin from "@fullcalendar/interaction"; +import type { CalendarOptions } from "@fullcalendar/core"; +import faLocale from "@fullcalendar/core/locales/fa"; // Type Imports -import type { AddEventType, CalendarColors, CalendarType } from '@/types/apps/calendarTypes' -import type { AppDispatch } from '@/redux-store' +import type { + AddEventType, + CalendarColors, + CalendarType, +} from "@/types/apps/calendarTypes"; // Slice Imports -import { filterEvents, selectedEvent, updateEventAsync } from '@/redux-store/slices/calendar' +import { + filterEvents, + selectedEvent, + updateEvent, +} from "@/redux-store/slices/calendar"; type CalenderProps = { - calendarStore: CalendarType - calendarApi: any - setCalendarApi: (val: any) => void - calendarsColor: CalendarColors - dispatch?: AppDispatch - handleLeftSidebarToggle?: () => void - handleAddEventSidebarToggle?: () => void - editable?: boolean - showSidebarToggle?: boolean - onDateClick?: (date: Date) => void - onEventClick?: (event: any) => void -} + calendarStore: CalendarType; + calendarApi: any; + setCalendarApi: (val: any) => void; + calendarsColor: CalendarColors; + dispatch: Dispatch; + handleLeftSidebarToggle: () => void; + handleAddEventSidebarToggle: () => void; + handleEventDetailsOpen: () => void; +}; const blankEvent: AddEventType = { - title: '', - start: '', - end: '', + title: "", + start: "", + end: "", allDay: false, - url: '', + url: "", extendedProps: { - calendar: '', + calendar: "", guests: [], - description: '' - } -} + description: "", + }, +}; + +const formatPersianDay = (date: Date) => + new Intl.DateTimeFormat("fa-IR-u-ca-persian", { day: "numeric" }).format( + date, + ); + +const formatPersianWeekday = (date: Date) => + new Intl.DateTimeFormat("fa-IR-u-ca-persian", { weekday: "short" }).format( + date, + ); const Calendar = (props: CalenderProps) => { // Props @@ -58,110 +73,58 @@ const Calendar = (props: CalenderProps) => { calendarsColor, dispatch, handleAddEventSidebarToggle, + handleEventDetailsOpen, handleLeftSidebarToggle, - editable = true, - showSidebarToggle = true, - onDateClick, - onEventClick - } = props + } = props; // Refs - const calendarRef = useRef() + const calendarRef = useRef(); // Hooks - const theme = useTheme() + const theme = useTheme(); useEffect(() => { if (calendarApi === null) { // @ts-ignore - setCalendarApi(calendarRef.current?.getApi()) + setCalendarApi(calendarRef.current?.getApi()); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, []); // calendarOptions(Props) const calendarOptions: CalendarOptions = { events: calendarStore.events, plugins: [interactionPlugin, dayGridPlugin, timeGridPlugin, listPlugin], - initialView: 'dayGridMonth', - locale: 'fa', + locale: faLocale, + initialView: "dayGridMonth", headerToolbar: { - start: `${showSidebarToggle ? 'sidebarToggle,' : ''}prev,next,title`, - end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth' + start: "sidebarToggle, prev, next, title", + end: "dayGridMonth,timeGridWeek,timeGridDay,listMonth", + }, + buttonText: { + today: "امروز", + month: "ماه", + week: "هفته", + day: "روز", + list: "فهرست", }, views: { week: { - titleFormat(arg: any) { - const start = arg.start - const end = arg.end - - const formatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { - year: 'numeric', - month: 'long', - day: 'numeric' - }) - - if (!start && !end) { - return '' - } - - if (start && !end) { - return formatter.format(start instanceof Date ? start : new Date(start.marker ?? start)) - } - - if (!start && end) { - return formatter.format(end instanceof Date ? end : new Date(end.marker ?? end)) - } - - const s = start instanceof Date ? start : new Date(start.marker ?? start) - const e = end instanceof Date ? end : new Date(end.marker ?? end) - - return `${formatter.format(s)} - ${formatter.format(e)}` - } - } + titleFormat: { year: "numeric", month: "short", day: "numeric" }, + }, }, dayHeaderContent(arg) { - const formatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { weekday: 'short' }) - - return formatter.format(arg.date) + return formatPersianWeekday(arg.date); }, - titleFormat(arg: any) { - const { start, end } = arg - - const monthFormatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { - year: 'numeric', - month: 'long' - }) - - // FullCalendar بعضی وقت‌ها به‌جای Date، DateMarker خودش را می‌فرستد؛ - // با new Date(...) مطمئن می‌شویم ورودی برای Intl معتبر است. - const target = start || end - - if (!target) { - return '' - } - - const asDate = target instanceof Date ? target : new Date(target as any) - - if (Number.isNaN(asDate.getTime())) { - return '' - } - - return monthFormatter.format(asDate) - }, - buttonText: { - today: 'امروز', - month: 'ماه', - week: 'هفته', - day: 'روز', - list: 'لیست' + dayCellContent(arg) { + return formatPersianDay(arg.date); }, /* Enable dragging and resizing event ? Docs: https://fullcalendar.io/docs/editable */ - editable, + editable: true, /* Enable resizing event from start @@ -189,28 +152,30 @@ const Calendar = (props: CalenderProps) => { eventClassNames({ event: calendarEvent }: any) { // @ts-ignore - const colorName = calendarsColor[calendarEvent._def.extendedProps.calendar] + const colorName = + calendarsColor[calendarEvent._def.extendedProps.calendar]; return [ // Background Color - `event-bg-${colorName}` - ] + `event-bg-${colorName}`, + ]; }, eventClick({ event: clickedEvent, jsEvent }: any) { - jsEvent.preventDefault() + jsEvent.preventDefault(); - onEventClick?.(clickedEvent) - - if (dispatch && handleAddEventSidebarToggle) { - dispatch(selectedEvent(clickedEvent)) - handleAddEventSidebarToggle() - } - - if (clickedEvent.url) { - // Open the URL in a new tab - window.open(clickedEvent.url, '_blank') - } + dispatch( + selectedEvent({ + id: clickedEvent.id, + url: clickedEvent.url, + title: clickedEvent.title, + allDay: clickedEvent.allDay, + end: clickedEvent.end, + start: clickedEvent.start, + extendedProps: clickedEvent.extendedProps, + }), + ); + handleEventDetailsOpen(); //* Only grab required field otherwise it goes in infinity loop //! Always grab all fields rendered by form (even if it get `undefined`) @@ -220,28 +185,22 @@ const Calendar = (props: CalenderProps) => { customButtons: { sidebarToggle: { - icon: 'tabler tabler-menu-2', + icon: "tabler tabler-menu-2", click() { - handleLeftSidebarToggle?.() - } - } + handleLeftSidebarToggle(); + }, + }, }, dateClick(info: any) { - onDateClick?.(info.date) + const ev = { ...blankEvent }; - if (!dispatch || !handleAddEventSidebarToggle) { - return - } + ev.start = info.date; + ev.end = info.date; + ev.allDay = true; - const ev = { ...blankEvent } - - ev.start = info.date - ev.end = info.date - ev.allDay = true - - dispatch(selectedEvent(ev)) - handleAddEventSidebarToggle() + dispatch(selectedEvent(ev)); + handleAddEventSidebarToggle(); }, /* @@ -250,18 +209,8 @@ const Calendar = (props: CalenderProps) => { ? We can use `eventDragStop` but it doesn't return updated event so we have to use `eventDrop` which returns updated event */ eventDrop({ event: droppedEvent }: any) { - if (!dispatch) { - return - } - - // Convert FullCalendar event to API format - const eventData = { - start: droppedEvent.start ? new Date(droppedEvent.start).toISOString() : '', - end: droppedEvent.end ? new Date(droppedEvent.end).toISOString() : '', - allDay: droppedEvent.allDay || false - } - dispatch(updateEventAsync({ id: droppedEvent.id, data: eventData })) - dispatch(filterEvents()) + dispatch(updateEvent(droppedEvent)); + dispatch(filterEvents()); }, /* @@ -269,27 +218,17 @@ const Calendar = (props: CalenderProps) => { ? Docs: https://fullcalendar.io/docs/eventResize */ eventResize({ event: resizedEvent }: any) { - if (!dispatch) { - return - } - - // Convert FullCalendar event to API format - const eventData = { - start: resizedEvent.start ? new Date(resizedEvent.start).toISOString() : '', - end: resizedEvent.end ? new Date(resizedEvent.end).toISOString() : '', - allDay: resizedEvent.allDay || false - } - dispatch(updateEventAsync({ id: resizedEvent.id, data: eventData })) - dispatch(filterEvents()) + dispatch(updateEvent(resizedEvent)); + dispatch(filterEvents()); }, // @ts-ignore ref: calendarRef, - direction: theme.direction - } + direction: theme.direction, + }; - return -} + return ; +}; -export default Calendar +export default Calendar; diff --git a/src/views/apps/calendar/CalendarWrapper.tsx b/src/views/apps/calendar/CalendarWrapper.tsx index 07551d9..59fd5cd 100644 --- a/src/views/apps/calendar/CalendarWrapper.tsx +++ b/src/views/apps/calendar/CalendarWrapper.tsx @@ -1,58 +1,79 @@ 'use client' +// React Imports +import { useState } from 'react' + +// MUI Imports +import { useMediaQuery } from '@mui/material' +import type { Theme } from '@mui/material/styles' + +// Third-party Imports +import { useDispatch, useSelector } from 'react-redux' + +// Type Imports +import type { CalendarColors, CalendarType } from '@/types/apps/calendarTypes' + +// Component Imports +import Calendar from './Calendar' +import SidebarLeft from './SidebarLeft' +import AddEventSidebar from './AddEventSidebar' + +// CalendarColors Object +const calendarsColor: CalendarColors = { + Personal: 'error', + Business: 'primary', + Family: 'warning', + Holiday: 'success', + ETC: 'info' +} + const AppCalendar = () => { - return <> - // // States - // const [calendarApi, setCalendarApi] = useState(null) - // const [leftSidebarOpen, setLeftSidebarOpen] = useState(false) - // const [addEventSidebarOpen, setAddEventSidebarOpen] = useState(false) + // States + const [calendarApi, setCalendarApi] = useState(null) + const [leftSidebarOpen, setLeftSidebarOpen] = useState(false) + const [addEventSidebarOpen, setAddEventSidebarOpen] = useState(false) - // // Hooks - // const dispatch = useAppDispatch() - // const calendarStore = useSelector((state: { calendarReducer: CalendarType & { loading: boolean; error: string | null } }) => state.calendarReducer) - // const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')) + // Hooks + const dispatch = useDispatch() + const calendarStore = useSelector((state: { calendarReducer: CalendarType }) => state.calendarReducer) + const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')) - // // Fetch events on mount - // useEffect(() => { - // dispatch(fetchEvents()) - // }, [dispatch]) + const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen) - // const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen) + const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen) - // const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen) - - // return ( - // <> - // - //
- // - //
- // - // - // ) + return ( + <> + +
+ +
+ + + ) } export default AppCalendar diff --git a/src/views/apps/calendar/SidebarLeft.tsx b/src/views/apps/calendar/SidebarLeft.tsx index 5a31780..e181182 100644 --- a/src/views/apps/calendar/SidebarLeft.tsx +++ b/src/views/apps/calendar/SidebarLeft.tsx @@ -1,6 +1,23 @@ +// MUI Imports +import Button from '@mui/material/Button' +import Drawer from '@mui/material/Drawer' +import Divider from '@mui/material/Divider' +import Checkbox from '@mui/material/Checkbox' +import Typography from '@mui/material/Typography' +import FormControlLabel from '@mui/material/FormControlLabel' + +// Third-party imports +import classnames from 'classnames' // Types Imports import type { SidebarLeftProps, CalendarFiltersType } from '@/types/apps/calendarTypes' +import type { ThemeColor } from '@core/types' + +// Styled Component Imports +import AppReactDatepicker from '@/libs/styles/AppReactDatepicker' + +// Slice Imports +import { filterAllCalendarLabels, filterCalendarLabel, selectedEvent } from '@/redux-store/slices/calendar' const SidebarLeft = (props: SidebarLeftProps) => { // Props @@ -15,205 +32,106 @@ const SidebarLeft = (props: SidebarLeftProps) => { handleAddEventSidebarToggle } = props - // // Vars - // const colorsArr = calendarsColor ? Object.entries(calendarsColor) : [] + // Vars + const colorsArr = calendarsColor ? Object.entries(calendarsColor) : [] - // const renderFilters = colorsArr.length - // ? colorsArr.map(([key, value]: string[]) => { - // return ( - // -1} - // onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))} - // /> - // } - // /> - // ) - // }) - // : null + const renderFilters = colorsArr.length + ? colorsArr.map(([key, value]: string[]) => { + return ( + -1} + onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))} + /> + } + /> + ) + }) + : null - // const handleSidebarToggleSidebar = () => { - // dispatch(selectedEvent(null)) - // handleAddEventSidebarToggle() - // } + const handleSidebarToggleSidebar = () => { + dispatch(selectedEvent(null)) + handleAddEventSidebarToggle() + } - // if (renderFilters) { - // return ( - // - //
- // - //
- // - // calendarApi?.gotoDate(date)} - // boxProps={{ - // className: 'flex justify-center is-full', - // sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } } - // }} - // /> - // + if (renderFilters) { + return ( + +
+ +
+ + calendarApi.gotoDate(date)} + boxProps={{ + className: 'flex justify-center is-full', + sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } } + }} + /> + - //
- // - // Event Filters - // - // dispatch(filterAllCalendarLabels(e.target.checked))} - // /> - // } - // /> - // {renderFilters} - //
- //
- // ) - // } else { - // return null - // } - // // Vars - // const colorsArr = calendarsColor ? Object.entries(calendarsColor) : [] - - // const renderFilters = colorsArr.length - // ? colorsArr.map(([key, value]: string[]) => { - // return ( - // -1} - // onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))} - // /> - // } - // /> - // ) - // }) - // : null - - // const handleSidebarToggleSidebar = () => { - // dispatch(selectedEvent(null)) - // handleAddEventSidebarToggle() - // } - - // if (renderFilters) { - // return ( - // - //
- // - //
- // - // calendarApi?.gotoDate(date)} - // boxProps={{ - // className: 'flex justify-center is-full', - // sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } } - // }} - // /> - // - - //
- // - // Event Filters - // - // dispatch(filterAllCalendarLabels(e.target.checked))} - // /> - // } - // /> - // {renderFilters} - //
- //
- // ) - // } else { - // return null - // } - return <> +
+ + Event Filters + + dispatch(filterAllCalendarLabels(e.target.checked))} + /> + } + /> + {renderFilters} +
+
+ ) + } else { + return null + } } export default SidebarLeft diff --git a/src/views/dashboards/farm/FarmerCalendarEventDetails.tsx b/src/views/dashboards/farm/FarmerCalendarEventDetails.tsx new file mode 100644 index 0000000..d38f2dc --- /dev/null +++ b/src/views/dashboards/farm/FarmerCalendarEventDetails.tsx @@ -0,0 +1,268 @@ +"use client"; + +import { memo } from "react"; + +import Button from "@mui/material/Button"; +import Chip from "@mui/material/Chip"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import Divider from "@mui/material/Divider"; +import Drawer from "@mui/material/Drawer"; +import IconButton from "@mui/material/IconButton"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import type { Theme } from "@mui/material/styles"; + +import { format, parseISO } from "date-fns"; +import { useTranslations } from "next-intl"; + +import { selectedEvent } from "@/redux-store/slices/calendar"; +import type { AppDispatch } from "@/redux-store"; +import type { CalendarType } from "@/types/apps/calendarTypes"; + +const modalTransitionDuration = { + appear: 0, + enter: 140, + exit: 100, +}; + +type FarmerCalendarEventDetailsProps = { + open: boolean; + onClose: () => void; + onEdit: () => void; + calendarStore: CalendarType; + dispatch: AppDispatch; +}; + +const toDate = (value: unknown) => { + if (!value) return null; + if (value instanceof Date) return value; + + if (Array.isArray(value)) { + const [year, month = 1, day = 1, hour = 0, minute = 0, second = 0] = + value.map(Number); + const fallback = new Date(year, month - 1, day, hour, minute, second); + + return Number.isNaN(fallback.getTime()) ? null : fallback; + } + + if (typeof value === "string") { + const parsed = parseISO(value); + + if (!Number.isNaN(parsed.getTime())) return parsed; + + const fallback = new Date(value); + + return Number.isNaN(fallback.getTime()) ? null : fallback; + } + + if (typeof value === "number") { + const fallback = new Date(value); + + return Number.isNaN(fallback.getTime()) ? null : fallback; + } + + return null; +}; + +const FarmerCalendarEventDetails = ({ + open, + onClose, + onEdit, + calendarStore, + dispatch, +}: FarmerCalendarEventDetailsProps) => { + const t = useTranslations("farmerCalendar.details"); + const tagLabel = t.has("fields.tag") ? t("fields.tag") : "Tag"; + const emptyTagLabel = t.has("emptyTag") + ? t("emptyTag") + : "No tag has been set."; + const isMobile = useMediaQuery( + (theme: Theme) => theme.breakpoints.down("sm"), + { noSsr: true }, + ); + const event = calendarStore.selectedEvent; + + const handleClose = () => { + dispatch(selectedEvent(null)); + onClose(); + }; + + if (!event) return null; + + const startDate = toDate(event.start); + const endDate = toDate(event.end); + const category = event.extendedProps?.calendar || "ETC"; + const categoryLabel = t( + `categories.${String(category).toLowerCase()}` as + | "categories.personal" + | "categories.business" + | "categories.family" + | "categories.holiday" + | "categories.etc", + ); + const description = event.extendedProps?.description || t("emptyDescription"); + const tag = + Array.isArray(event.extendedProps?.tags) && + typeof event.extendedProps.tags[0] === "string" && + event.extendedProps.tags[0].trim().length + ? event.extendedProps.tags[0].trim() + : emptyTagLabel; + + const content = ( + + + + + {event.title || t("untitled")} + + + + +
+ + {t("fields.date")} + + + {startDate ? format(startDate, "EEEE, MMMM d, yyyy") : "-"} + +
+ +
+ + {t("fields.time")} + + + {event.allDay + ? t("allDay") + : startDate + ? endDate + ? t("timeRange", { + start: format(startDate, "p"), + end: format(endDate, "p"), + }) + : format(startDate, "p") + : "-"} + +
+ +
+ + {tagLabel} + + + {tag} + +
+ +
+ + {t("fields.description")} + + + {description} + +
+
+ + + + + + + +
+ ); + + if (isMobile) { + return ( + +
+
+ {t("title")} + + {t("subtitle")} + +
+ + + +
+
{content}
+
+ ); + } + + return ( + +
+
+ {t("title")} + + {t("subtitle")} + +
+ + + +
+ {content} +
+ ); +}; + +export default memo(FarmerCalendarEventDetails); diff --git a/src/views/dashboards/farm/FarmerCalendarEventModal.tsx b/src/views/dashboards/farm/FarmerCalendarEventModal.tsx new file mode 100644 index 0000000..ce0b87d --- /dev/null +++ b/src/views/dashboards/farm/FarmerCalendarEventModal.tsx @@ -0,0 +1,484 @@ +"use client"; + +import { forwardRef, memo, useCallback, useEffect, useState } from "react"; + +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import Drawer from "@mui/material/Drawer"; +import FormControl from "@mui/material/FormControl"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import IconButton from "@mui/material/IconButton"; +import MenuItem from "@mui/material/MenuItem"; +import Stack from "@mui/material/Stack"; +import Switch from "@mui/material/Switch"; +import Typography from "@mui/material/Typography"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import type { Theme } from "@mui/material/styles"; + +import { Controller, useForm } from "react-hook-form"; +import { useTranslations } from "next-intl"; + +import CustomTextField from "@core/components/mui/TextField"; +import { eventService } from "@/libs/api"; +import AppReactDatepicker from "@/libs/styles/AppReactDatepicker"; +import { + addEvent, + deleteEvent, + filterEvents, + selectedEvent, + updateEvent, +} from "@/redux-store/slices/calendar"; +import type { + AddEventSidebarType, + AddEventType, +} from "@/types/apps/calendarTypes"; + +type FarmerCalendarEventModalProps = Omit< + AddEventSidebarType, + "addEventSidebarOpen" | "handleAddEventSidebarToggle" +> & { + open: boolean; + onClose: () => void; +}; + +type PickerProps = { + label?: string; + error?: boolean; +}; + +type DefaultStateType = { + title: string; + tag: string; + allDay: boolean; + description: string; + endDate: Date; + startDate: Date; +}; + +const asSingleDate = ( + value: Date | [Date | null, Date | null] | null, +): Date | null => (value instanceof Date ? value : null); + +const defaultState: DefaultStateType = { + title: "", + tag: "", + allDay: true, + description: "", + endDate: new Date(), + startDate: new Date(), +}; + +const modalTransitionDuration = { + appear: 0, + enter: 140, + exit: 100, +}; + +const PickerInput = forwardRef( + ({ ...props }, ref) => ( + + ), +); + +PickerInput.displayName = "PickerInput"; + +const FarmerCalendarEventModal = ({ + calendarStore, + dispatch, + open, + onClose, +}: FarmerCalendarEventModalProps) => { + const t = useTranslations("farmerCalendar.modal"); + const tagLabel = t.has("fields.tag") ? t("fields.tag") : "Tag"; + const tagPlaceholder = t.has("fields.tagPlaceholder") + ? t("fields.tagPlaceholder") + : "Select a tag"; + const isMobile = useMediaQuery( + (theme: Theme) => theme.breakpoints.down("sm"), + { noSsr: true }, + ); + const [values, setValues] = useState(defaultState); + const [tagOptions, setTagOptions] = useState([]); + const [tagLoading, setTagLoading] = useState(false); + + const { + control, + setValue, + clearErrors, + handleSubmit, + formState: { errors }, + } = useForm({ defaultValues: { title: "" } }); + + const resetToStoredValues = useCallback(() => { + if (calendarStore.selectedEvent !== null) { + const event = calendarStore.selectedEvent; + + setValue("title", event.title || ""); + setValues({ + title: event.title || "", + tag: + Array.isArray(event.extendedProps?.tags) && + typeof event.extendedProps.tags[0] === "string" + ? event.extendedProps.tags[0] + : "", + allDay: event.allDay ?? true, + description: event.extendedProps?.description || "", + endDate: event.end !== null ? event.end : event.start, + startDate: event.start !== null ? event.start : new Date(), + }); + } + }, [calendarStore.selectedEvent, setValue]); + + const resetToEmptyValues = useCallback(() => { + setValue("title", ""); + setValues(defaultState); + }, [setValue]); + + const handleClose = () => { + setValues(defaultState); + clearErrors(); + dispatch(selectedEvent(null)); + onClose(); + }; + + const onSubmit = (data: { title: string }) => { + const modifiedEvent: AddEventType = { + display: "block", + title: data.title, + end: values.endDate, + allDay: values.allDay, + start: values.startDate, + extendedProps: { + calendar: + calendarStore.selectedEvent?.extendedProps?.calendar || "Business", + description: values.description.length ? values.description : undefined, + tags: values.tag.trim().length ? [values.tag.trim()] : [], + }, + }; + + if ( + calendarStore.selectedEvent === null || + (calendarStore.selectedEvent !== null && + !calendarStore.selectedEvent.title.length) + ) { + dispatch(addEvent(modifiedEvent)); + } else { + dispatch( + updateEvent({ ...modifiedEvent, id: calendarStore.selectedEvent.id }), + ); + } + + dispatch(filterEvents()); + handleClose(); + }; + + const handleDeleteButtonClick = () => { + if (calendarStore.selectedEvent?.id) { + dispatch(deleteEvent(calendarStore.selectedEvent.id)); + dispatch(filterEvents()); + } + + handleClose(); + }; + + const handleStartDate = (date: Date | null) => { + if (!date) return; + + setValues((previous) => ({ + ...previous, + startDate: new Date(date), + endDate: date > previous.endDate ? new Date(date) : previous.endDate, + })); + }; + + const isEditMode = + calendarStore.selectedEvent !== null && + Boolean(calendarStore.selectedEvent.title.length); + + useEffect(() => { + if (!open) return; + + if (calendarStore.selectedEvent !== null) { + resetToStoredValues(); + } else { + resetToEmptyValues(); + } + }, [ + open, + calendarStore.selectedEvent, + resetToEmptyValues, + resetToStoredValues, + ]); + + useEffect(() => { + if (!open) return; + if (tagOptions.length) return; + + let active = true; + + const fetchTags = async () => { + setTagLoading(true); + + try { + const events = await eventService.listEvents(); + const options = Array.from( + new Set( + events.flatMap((event) => + Array.isArray(event.tags) + ? event.tags + .filter((tag): tag is string => typeof tag === "string") + .map((tag) => tag.trim()) + .filter(Boolean) + : [], + ), + ), + ); + + if (active) { + setTagOptions(options); + } + } finally { + if (active) { + setTagLoading(false); + } + } + }; + + fetchTags(); + + return () => { + active = false; + }; + }, [open, tagOptions.length]); + + const content = ( +
+ ( + + )} + /> + + } + onChange={(date) => { + const nextDate = asSingleDate(date); + + if (nextDate !== null) { + setValues((previous) => ({ + ...previous, + startDate: new Date(nextDate), + })); + } + }} + onSelect={handleStartDate} + /> + + } + onChange={(date) => { + const nextDate = asSingleDate(date); + + if (nextDate !== null) { + setValues((previous) => ({ + ...previous, + endDate: new Date(nextDate), + })); + } + }} + /> + + + + setValues((previous) => ({ + ...previous, + allDay: event.target.checked, + })) + } + /> + } + /> + + + + setValues((previous) => ({ + ...previous, + tag: event.target.value, + })) + } + > + {tagLoading ? "..." : tagPlaceholder} + {tagOptions.map((option) => ( + + {option} + + ))} + + + + setValues((previous) => ({ + ...previous, + description: event.target.value, + })) + } + /> + + + {isEditMode ? ( + + ) : null} + + + + + ); + + if (isMobile) { + return ( + +
+
+ + {isEditMode ? t("titleEdit") : t("titleCreate")} + + + {t("description")} + +
+ + + +
+
{content}
+
+ ); + } + + return ( + +
+
+ + {isEditMode ? t("titleEdit") : t("titleCreate")} + + + {t("description")} + +
+ + + +
+ {content} +
+ ); +}; + +export default memo(FarmerCalendarEventModal); diff --git a/src/views/dashboards/farm/FarmerCalendarPage.tsx b/src/views/dashboards/farm/FarmerCalendarPage.tsx index 52e42e9..0ba4816 100644 --- a/src/views/dashboards/farm/FarmerCalendarPage.tsx +++ b/src/views/dashboards/farm/FarmerCalendarPage.tsx @@ -1,486 +1,1064 @@ -'use client' +"use client"; -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from "react"; -import type { EventInput } from '@fullcalendar/core' +import { useTranslations } from "next-intl"; -import Box from '@mui/material/Box' -import Card from '@mui/material/Card' -import CardContent from '@mui/material/CardContent' -import Chip from '@mui/material/Chip' -import Divider from '@mui/material/Divider' -import Grid from '@mui/material/Grid2' -import Stack from '@mui/material/Stack' -import Typography from '@mui/material/Typography' +import { alpha, styled, useTheme } from "@mui/material/styles"; +import Alert from "@mui/material/Alert"; +import Avatar from "@mui/material/Avatar"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import Chip from "@mui/material/Chip"; +import Divider from "@mui/material/Divider"; +import Drawer from "@mui/material/Drawer"; +import Grid from "@mui/material/Grid2"; +import IconButton from "@mui/material/IconButton"; +import LinearProgress from "@mui/material/LinearProgress"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import useMediaQuery from "@mui/material/useMediaQuery"; -import AppFullCalendar from '@/libs/styles/AppFullCalendar' -import Calendar from '@views/apps/calendar/Calendar' -import type { ThemeColor } from '@/@core/types' -import type { CalendarColors, CalendarType } from '@/types/apps/calendarTypes' +import type { EventInput } from "@fullcalendar/core"; +import { format, isThisWeek, isToday, parseISO } from "date-fns"; -const calendarColors: CalendarColors = { - Personal: 'success', - Business: 'warning', - Family: 'primary', - Holiday: 'info', - ETC: 'error' -} +import AppReactDatepicker from "@/libs/styles/AppReactDatepicker"; +import AppFullCalendar from "@/libs/styles/AppFullCalendar"; +import { useAppDispatch, useAppSelector } from "@/redux-store"; +import { + fetchEvents, + filterAllCalendarLabels, + filterCalendarLabel, + selectedEvent, +} from "@/redux-store/slices/calendar"; +import type { + CalendarColors, + CalendarFiltersType, +} from "@/types/apps/calendarTypes"; +import Calendar from "@/views/apps/calendar/Calendar"; +import FarmerCalendarEventDetails from "@/views/dashboards/farm/FarmerCalendarEventDetails"; +import FarmerCalendarEventModal from "@/views/dashboards/farm/FarmerCalendarEventModal"; -const dayFormatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { - weekday: 'long', - day: 'numeric', - month: 'long' -}) +type PaletteAccent = "primary" | "success" | "warning" | "error" | "info"; -const fullDateFormatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { - weekday: 'long', - day: 'numeric', - month: 'long', - year: 'numeric' -}) +type EventCardData = { + id: string; + title: string; + category: CalendarFiltersType; + start: Date; + allDay: boolean; + description?: string; +}; -const makeDate = (year: number, month: number, day: number, hour = 8, minute = 0) => - new Date(year, month, day, hour, minute) +const calendarsColor: CalendarColors = { + Personal: "error", + Business: "primary", + Family: "warning", + Holiday: "success", + ETC: "info", +}; -const today = new Date() -const year = today.getFullYear() -const month = today.getMonth() - -const farmerEvents: EventInput[] = [ - { - id: 'irrigation-1', - title: 'آبیاری قطعه شمالی', - start: makeDate(year, month, 4, 6, 30).toISOString(), - end: makeDate(year, month, 4, 8, 0).toISOString(), - extendedProps: { - calendar: 'Personal', - description: 'آبیاری قطره ای برای گوجه فرنگی ها با بررسی فشار خطوط قبل از شروع.' - } +const categoryDetails: Record< + CalendarFiltersType, + { labelKey: string; accent: PaletteAccent; icon: string } +> = { + Personal: { + labelKey: "categories.personal", + accent: "error", + icon: "tabler-sun-high", }, - { - id: 'nutrition-1', - title: 'کوددهی مزرعه ذرت', - start: makeDate(year, month, 6, 9, 0).toISOString(), - end: makeDate(year, month, 6, 11, 30).toISOString(), - extendedProps: { - calendar: 'Business', - description: 'محلول پاشی مرحله رشد رویشی و ثبت مقدار مصرف برای هر هکتار.' - } + Business: { + labelKey: "categories.business", + accent: "primary", + icon: "tabler-tractor", }, - { - id: 'scouting-1', - title: 'بازدید آفات و بیماری', - start: makeDate(year, month, 8, 7, 30).toISOString(), - end: makeDate(year, month, 8, 9, 0).toISOString(), - extendedProps: { - calendar: 'ETC', - description: 'بررسی لکه های برگی، جمع آوری نمونه و ثبت نقاط بحرانی در مزرعه.' - } + Family: { + labelKey: "categories.family", + accent: "warning", + icon: "tabler-users-group", }, - { - id: 'harvest-1', - title: 'برداشت آزمایشی زعفران', - start: makeDate(year, month, 11, 5, 0).toISOString(), - end: makeDate(year, month, 11, 9, 0).toISOString(), - extendedProps: { - calendar: 'Family', - description: 'هماهنگی نیروی کار و ثبت عملکرد اولیه برای برنامه ریزی برداشت اصلی.' - } + Holiday: { + labelKey: "categories.holiday", + accent: "success", + icon: "tabler-leaf", }, - { - id: 'market-1', - title: 'جلسه فروش با خریدار عمده', - start: makeDate(year, month, 14, 12, 0).toISOString(), - end: makeDate(year, month, 14, 13, 0).toISOString(), - extendedProps: { - calendar: 'Holiday', - description: 'بررسی قیمت هفتگی، کیفیت محصول و زمان بندی تحویل بار.' - } + ETC: { + labelKey: "categories.etc", + accent: "info", + icon: "tabler-sparkles", }, - { - id: 'irrigation-2', - title: 'شست وشوی فیلترهای آبیاری', - start: makeDate(year, month, 18, 6, 0).toISOString(), - end: makeDate(year, month, 18, 7, 0).toISOString(), - extendedProps: { - calendar: 'Personal', - description: 'سرویس پیشگیرانه برای جلوگیری از افت دبی در آبیاری نوبت بعد.' - } - }, - { - id: 'soil-1', - title: 'نمونه برداری خاک', - start: makeDate(year, month, 20, 8, 0).toISOString(), - end: makeDate(year, month, 20, 10, 0).toISOString(), - extendedProps: { - calendar: 'Business', - description: 'نمونه گیری از سه ناحیه برای ارسال به آزمایشگاه و تنظیم نسخه تغذیه.' - } - }, - { - id: 'maintenance-1', - title: 'سرویس تراکتور و ادوات', - start: makeDate(year, month, 23, 15, 0).toISOString(), - end: makeDate(year, month, 23, 17, 0).toISOString(), - extendedProps: { - calendar: 'ETC', - description: 'تعویض فیلتر روغن، گریس کاری و آماده سازی برای عملیات آخر هفته.' - } - } -] +}; -const calendarStore: CalendarType = { - events: farmerEvents, - filteredEvents: farmerEvents, - selectedEvent: null, - selectedCalendars: ['Personal', 'Business', 'Family', 'Holiday', 'ETC'] -} - -const overviewItems = [ - { - label: 'کارهای این هفته', - value: '۸ برنامه', - note: '۳ مورد نیازمند پیگیری امروز', - accent: 'linear-gradient(135deg, rgba(34,197,94,0.18), rgba(22,163,74,0.05))' +const FarmCalendarShell = styled(AppFullCalendar)(({ theme }) => ({ + borderRadius: 28, + overflow: "hidden", + background: [ + `radial-gradient(circle at top left, ${alpha(theme.palette.success.light, 0.22)}, transparent 28%)`, + `radial-gradient(circle at top right, ${alpha(theme.palette.warning.light, 0.16)}, transparent 24%)`, + alpha(theme.palette.background.paper, 0.92), + ].join(","), + border: `1px solid ${alpha(theme.palette.success.main, 0.12)}`, + boxShadow: `0 24px 60px ${alpha(theme.palette.common.black, 0.08)}`, + backdropFilter: "blur(16px)", + "&::before": { + content: '""', + position: "absolute", + inset: 0, + pointerEvents: "none", + background: `linear-gradient(135deg, ${alpha(theme.palette.success.main, 0.08)}, transparent 40%)`, }, - { - label: 'اقدام بعدی', - value: 'آبیاری شمال مزرعه', - note: 'فردا، ساعت ۶:۳۰ صبح', - accent: 'linear-gradient(135deg, rgba(14,165,233,0.18), rgba(2,132,199,0.05))' + "& .calendar-frame": { + + position: "relative", + zIndex: 1, + padding: theme.spacing(3, 3, 4), + [theme.breakpoints.up("md")]: { + padding: theme.spacing(4, 4, 5), + }, }, - { - label: 'اولویت بحرانی', - value: 'بازدید آفات', - note: 'تا ۴۸ ساعت آینده انجام شود', - accent: 'linear-gradient(135deg, rgba(249,115,22,0.18), rgba(234,88,12,0.05))' - } -] + "& .fc": { + "& .fc-toolbar.fc-header-toolbar": { + marginBottom: theme.spacing(4), + padding: theme.spacing(0.5, 0), + [theme.breakpoints.down("md")]: { + alignItems: "flex-start", + }, + }, + "& .fc-toolbar-chunk": { + gap: theme.spacing(1.5), + }, + "& .fc-toolbar-title": { + ...theme.typography.h3, + fontWeight: 800, + letterSpacing: "-0.03em", + color: theme.palette.text.primary, + marginInline: theme.spacing(1.5), + }, + "& .fc-button-group:has(.fc-next-button)": { + marginInlineStart: theme.spacing(0.5), + }, + "& .fc-prev-button, & .fc-next-button": { + borderRadius: 999, + border: `1px solid ${alpha(theme.palette.divider, 0.9)}`, + backgroundColor: alpha(theme.palette.background.default, 0.9), + minHeight: 42, + minWidth: 42, + transition: + "transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease", + "& .fc-icon": { + color: theme.palette.text.primary, + }, + "&:hover": { + transform: "translateY(-1px)", + backgroundColor: alpha(theme.palette.success.main, 0.08), + borderColor: alpha(theme.palette.success.main, 0.2), + }, + }, + "& .fc-button-group .fc-button-primary:not(.fc-prev-button):not(.fc-next-button)": + { + borderRadius: 999, + padding: theme.spacing(1.35, 2.6), + fontWeight: 700, + letterSpacing: "0.01em", + backgroundColor: alpha(theme.palette.success.main, 0.08), + color: theme.palette.success.dark, + border: `1px solid ${alpha(theme.palette.success.main, 0.12)}`, + "&.fc-button-active, &:hover": { + backgroundColor: alpha(theme.palette.success.main, 0.16), + color: theme.palette.success.dark, + borderColor: alpha(theme.palette.success.main, 0.26), + }, + }, + "& .fc-sidebarToggle-button": { + borderRadius: 999, + minHeight: 42, + minWidth: 42, + marginInlineEnd: theme.spacing(0.5), + color: `${theme.palette.text.primary} !important`, + backgroundColor: `${alpha(theme.palette.background.default, 0.9)} !important`, + border: `1px solid ${alpha(theme.palette.divider, 0.9)} !important`, + }, + "& .fc-view-harness": { + minHeight: 720, + margin: 0, + borderRadius: 24, + overflow: "hidden", + border: `1px solid ${alpha(theme.palette.divider, 0.7)}`, + backgroundColor: alpha(theme.palette.background.paper, 0.92), + }, + "& .fc-scrollgrid": { + border: 0, + }, + "& .fc-col-header ": { + backgroundColor: alpha(theme.palette.success.main, 0.05), + }, + "& .fc-col-header-cell": { + borderColor: alpha(theme.palette.divider, 0.7), + backgroundColor: "transparent", + }, + "& .fc-col-header-cell-cushion": { + padding: theme.spacing(2.25, 1), + fontWeight: 700, + color: theme.palette.text.primary, + textTransform: "uppercase", + letterSpacing: "0.08em", + fontSize: theme.typography.caption.fontSize, + }, + "& .fc-daygrid-day, & .fc-timegrid-col": { + backgroundColor: alpha(theme.palette.background.paper, 0.94), + }, + "& .fc-daygrid-day-frame": { + minHeight: 132, + padding: theme.spacing(1.5), + transition: "background-color 0.2s ease, transform 0.2s ease", + }, + "& .fc-daygrid-day:hover .fc-daygrid-day-frame": { + backgroundColor: alpha(theme.palette.success.main, 0.035), + }, + "& .fc-daygrid-day-number": { + display: "grid", + placeItems: "center", + width: 32, + height: 32, + borderRadius: 999, + fontWeight: 700, + color: `${theme.palette.text.primary} !important`, + }, + "& .fc-day-today:not(.fc-popover)": { + backgroundColor: alpha(theme.palette.success.main, 0.07), + "& .fc-daygrid-day-number": { + backgroundColor: theme.palette.success.main, + color: `${theme.palette.common.white} !important`, + }, + }, + "& .fc-day-other .fc-daygrid-day-frame": { + backgroundColor: alpha(theme.palette.action.disabledBackground, 0.24), + }, + "& .fc-daygrid-day-events": { + marginTop: theme.spacing(1.75), + minHeight: "5.75rem !important", + }, + "& .fc-daygrid-event-harness .fc-event, & .fc-timegrid-event": { + borderRadius: 14, + padding: theme.spacing(0.9, 1.4), + boxShadow: `0 10px 24px ${alpha(theme.palette.common.black, 0.08)}`, + }, + "& .fc-event-main-frame": { + gap: theme.spacing(0.5), + }, + "& .fc-event-title": { + fontWeight: 700, + }, + "& .fc-timegrid-slot": { + height: "3.5rem", + borderColor: alpha(theme.palette.divider, 0.65), + }, + "& .fc-timegrid-axis, & .fc-timegrid-slot-label": { + backgroundColor: alpha(theme.palette.background.default, 0.8), + }, + "& .fc-timegrid-axis-cushion, & .fc-timegrid-slot-label-cushion": { + fontWeight: 600, + color: theme.palette.text.secondary, + }, + "& .fc-list": { + border: 0, + }, + "& .fc-list-day-cushion": { + backgroundColor: alpha(theme.palette.success.main, 0.06), + }, + "& .fc-list-event:hover td": { + backgroundColor: alpha(theme.palette.success.main, 0.05), + }, + [theme.breakpoints.down("md")]: { + "& .fc-toolbar-title": { + ...theme.typography.h4, + marginInline: 0, + }, + "& .fc-view-harness": { + minHeight: 640, + }, + }, + }, +})); -const legendItems = [ - { label: 'آبیاری و عملیات روزانه', color: 'success.main' }, - { label: 'تغذیه و خاک', color: 'warning.main' }, - { label: 'برداشت و نیروی کار', color: 'primary.main' }, - { label: 'جلسات و هماهنگی فروش', color: 'info.main' }, - { label: 'ریسک ها و نگهداری', color: 'error.main' } -] +const toEventDate = (value: unknown) => { + if (!value) return null; + if (value instanceof Date) return value; -const getEventDate = (event: EventInput) => { - const raw = event.start + if (Array.isArray(value)) { + const [year, month = 1, day = 1, hour = 0, minute = 0, second = 0] = + value.map(Number); + const fallback = new Date(year, month - 1, day, hour, minute, second); - if (raw instanceof Date) { - return raw + return Number.isNaN(fallback.getTime()) ? null : fallback; } - return raw ? new Date(raw) : new Date() -} + if (typeof value === "string") { + const parsed = parseISO(value); -const formatTimeRange = (event: EventInput) => { - const start = getEventDate(event) - const end = - event.end instanceof Date ? event.end : event.end ? new Date(event.end) : null + if (!Number.isNaN(parsed.getTime())) return parsed; - const timeFormatter = new Intl.DateTimeFormat('fa-IR', { - hour: '2-digit', - minute: '2-digit' - }) + const fallback = new Date(value); - return end - ? `${timeFormatter.format(start)} تا ${timeFormatter.format(end)}` - : timeFormatter.format(start) -} - -const getCalendarLabel = (event: EventInput) => { - const type = event.extendedProps?.calendar - - switch (type) { - case 'Personal': - return 'آبیاری' - case 'Business': - return 'تغذیه' - case 'Family': - return 'برداشت' - case 'Holiday': - return 'بازار' - default: - return 'نگهداری' + return Number.isNaN(fallback.getTime()) ? null : fallback; } -} -const getCalendarChipColor = (event: EventInput): ThemeColor => { - const type = event.extendedProps?.calendar as keyof CalendarColors | undefined + if (typeof value === "number") { + const fallback = new Date(value); - return type ? calendarColors[type] : 'secondary' -} + return Number.isNaN(fallback.getTime()) ? null : fallback; + } -const upcomingEvents = [...farmerEvents] - .sort((left, right) => getEventDate(left).getTime() - getEventDate(right).getTime()) - .slice(0, 5) + return null; +}; + +const createEventCards = (calendarEvents: EventInput[]) => { + const mappedEvents: EventCardData[] = []; + + calendarEvents.forEach((event) => { + const start = toEventDate(event.start); + + if (!start) return; + + mappedEvents.push({ + id: String(event.id ?? event.title), + title: event.title || "", + category: (event.extendedProps?.calendar || "ETC") as CalendarFiltersType, + start, + allDay: Boolean(event.allDay), + description: + typeof event.extendedProps?.description === "string" + ? event.extendedProps.description + : undefined, + }); + }); + + return mappedEvents.sort( + (left, right) => left.start.getTime() - right.start.getTime(), + ); +}; const FarmerCalendarPage = () => { - const [calendarApi, setCalendarApi] = useState(null) - const [selectedEvent, setSelectedEvent] = useState(upcomingEvents[0]) + const t = useTranslations("farmerCalendar"); + const theme = useTheme(); + const dispatch = useAppDispatch(); + const mdUp = useMediaQuery(theme.breakpoints.up("lg")); + const [calendarApi, setCalendarApi] = useState(null); + const [leftSidebarOpen, setLeftSidebarOpen] = useState(false); + const [eventModalOpen, setEventModalOpen] = useState(false); + const [eventDetailsOpen, setEventDetailsOpen] = useState(false); - const selectedEventDate = useMemo(() => fullDateFormatter.format(getEventDate(selectedEvent)), [selectedEvent]) + const calendarStore = useAppSelector((state) => state.calendarReducer); + const { events, selectedCalendars, loading, error } = calendarStore; - return ( - + useEffect(() => { + dispatch(fetchEvents()); + }, [dispatch]); + + const eventCards = useMemo(() => createEventCards(events), [events]); + const allEventCards = useMemo( + () => + createEventCards( + calendarStore.filteredEvents.length + ? calendarStore.filteredEvents + : events, + ), + [calendarStore.filteredEvents, events], + ); + + const todayCount = useMemo( + () => eventCards.filter((event) => isToday(event.start)).length, + [eventCards], + ); + const weekCount = useMemo( + () => + eventCards.filter((event) => isThisWeek(event.start, { weekStartsOn: 1 })) + .length, + [eventCards], + ); + const allDayCount = useMemo( + () => eventCards.filter((event) => event.allDay).length, + [eventCards], + ); + const nextEvent = useMemo( + () => + eventCards.find((event) => event.start.getTime() >= Date.now()) || null, + [eventCards], + ); + const upcomingEvents = useMemo( + () => + eventCards + .filter((event) => event.start.getTime() >= Date.now()) + .slice(0, 5), + [eventCards], + ); + const categoryCounts = useMemo( + () => + allEventCards.reduce( + (accumulator, event) => { + accumulator[event.category] += 1; + + return accumulator; + }, + { + Personal: 0, + Business: 0, + Family: 0, + Holiday: 0, + ETC: 0, + } as Record, + ), + [allEventCards], + ); + + const selectedCount = selectedCalendars.length; + const allSelected = selectedCount === Object.keys(calendarsColor).length; + + const handleLeftSidebarToggle = () => + setLeftSidebarOpen((previous) => !previous); + const handleAddEventSidebarToggle = () => { + setEventDetailsOpen(false); + setEventModalOpen(true); + }; + const handleEventDetailsOpen = () => setEventDetailsOpen(true); + + const handleCreateEvent = () => { + dispatch(selectedEvent(null)); + setEventDetailsOpen(false); + setEventModalOpen(true); + }; + + const handleEditSelectedEvent = () => { + setEventDetailsOpen(false); + setEventModalOpen(true); + }; + + const insightCards = [ + { + label: t("insights.today.label"), + value: todayCount, + hint: t("insights.today.hint"), + icon: "tabler-sunrise", + accent: theme.palette.warning.main, + }, + { + label: t("insights.thisWeek.label"), + value: weekCount, + hint: t("insights.thisWeek.hint"), + icon: "tabler-calendar-week", + accent: theme.palette.primary.main, + }, + { + label: t("insights.allDay.label"), + value: allDayCount, + hint: t("insights.allDay.hint"), + icon: "tabler-clock-hour-4", + accent: theme.palette.success.main, + }, + ]; + + const sidebarContent = ( + + + + + {t("sidebar.planningPulse")} + + + {t("sidebar.farmWeekAtGlance")} + + + + {insightCards.map((card) => ( + + + + + + + + + {card.value} + + + {card.label} + + + {card.hint} + + + + + + ))} + + + +{/* + + + + + {t("sidebar.navigator.title")} + + + {t("sidebar.navigator.description")} + + + date && calendarApi?.gotoDate(date)} + boxProps={{ + sx: { + "& .react-datepicker": { + width: "100%", + border: "0 !important", + boxShadow: "none !important", + backgroundColor: "transparent", + }, + "& .react-datepicker__month-container": { + width: "100%", + }, + "& .react-datepicker__day-name, & .react-datepicker__current-month": + { + fontWeight: 700, + }, + }, + }} + /> + + */} + + + + + + + {t("sidebar.focusLanes.title")} + + + {t("sidebar.focusLanes.description")} + + + + + + dispatch(filterAllCalendarLabels(!allSelected))} + /> + {(Object.keys(categoryDetails) as CalendarFiltersType[]).map( + (key) => { + const detail = categoryDetails[key]; + const selected = selectedCalendars.includes(key); + + return ( + } + label={t("sidebar.focusLanes.categoryCount", { + label: t(detail.labelKey), + count: categoryCounts[key], + })} + clickable + color={detail.accent} + variant={selected ? "filled" : "outlined"} + onClick={() => dispatch(filterCalendarLabel(key))} + sx={{ + "& .MuiChip-icon": { + fontSize: "1rem", + }, + }} + /> + ); + }, + )} + + + + + + + + + {t("sidebar.upcomingActions.title")} + + + {t("sidebar.upcomingActions.description")} + + + + {upcomingEvents.length > 0 ? ( + upcomingEvents.map((event) => { + const detail = categoryDetails[event.category]; + + return ( + + + + + + {event.title || t("fallbacks.untitledTask")} + + + {format(event.start, "EEE, MMM d")}{" "} + {event.allDay + ? t("calendar.allDaySuffix") + : t("calendar.timeSuffix", { + time: format(event.start, "p"), + })} + + + + + {event.description ? ( + + {event.description} + + ) : null} + + + ); + }) + ) : ( + + + {t("sidebar.upcomingActions.empty")} + + + )} + + + + ); + + return ( + + - - - - - - - تقویم بزرگ عملیات کشاورز برای مدیریت آبیاری، تغذیه، برداشت و کارهای روزانه - - - این صفحه تمام برنامه های مزرعه را در یک نمای ماهانه جمع می کند تا کشاورز بداند - امروز چه کاری مهم تر است و در روزهای آینده چه چیزی باید آماده شود. - - - - - - {overviewItems.map(item => ( - - - - - {item.label} - - - {item.value} - - - {item.note} - - - - - ))} - - - - - - - - - - - - - تقویم عملیات مزرعه - - روی هر برنامه کلیک کن تا جزئیات آن در پنل کناری نمایش داده شود. - - - - {legendItems.map(item => ( - - - - {item.label} - - - ))} - - - - + + + + + + {t("hero.title")} + + + {t("hero.description")} + + + + + + {!mdUp ? ( + + ) : null} + + + + + + - setSelectedEvent(event.toPlainObject())} - onDateClick={date => { - const nearest = - [...farmerEvents].find(event => { - const eventDate = getEventDate(event) - - return ( - eventDate.getFullYear() === date.getFullYear() && - eventDate.getMonth() === date.getMonth() && - eventDate.getDate() === date.getDate() - ) - }) || selectedEvent - - setSelectedEvent(nearest) - }} - /> - - - - - - - - - - - - - برنامه انتخاب شده - - - {selectedEvent.title} - - - - - - - - تاریخ - - - {selectedEventDate} - - - - - - بازه زمانی - - - {formatTimeRange(selectedEvent)} - - - - - - - - توضیحات اجرایی - - - {selectedEvent.extendedProps?.description as string} - - - - - - - - - - برنامه های نزدیک - - - {upcomingEvents.map(event => ( - setSelectedEvent(event)} + {t("hero.nextCheckpoint")} + + + {nextEvent ? nextEvent.title : t("hero.noScheduledAction")} + + + {nextEvent + ? nextEvent.allDay + ? t("hero.nextCheckpointAllDay", { + date: format(nextEvent.start, "EEEE, MMMM d"), + }) + : t("hero.nextCheckpointTimed", { + date: format(nextEvent.start, "EEEE, MMMM d"), + time: format(nextEvent.start, "p"), + }) + : t("hero.noScheduledActionDescription")} + + + + {(Object.keys(categoryDetails) as CalendarFiltersType[]).map( + (key) => ( + `1px solid ${theme.palette.divider}`, - backgroundColor: - selectedEvent.id === event.id ? 'action.hover' : 'background.paper', - transition: 'all 0.2s ease', - '&:hover': { - borderColor: 'primary.main', - transform: 'translateY(-2px)' - } + color: "common.white", + backgroundColor: alpha( + theme.palette.common.white, + 0.12, + ), + border: `1px solid ${alpha(theme.palette.common.white, 0.14)}`, }} - > - - - {event.title} - - {dayFormatter.format(getEventDate(event))} - - - - - - ))} + /> + ), + )} + + + + + + + {error ? {error} : null} + + + {mdUp ? ( + + + {sidebarContent} + + + ) : null} + + + + + + + + {t("calendar.title")} + + + {t("calendar.description")} + + + + } + label={t("calendar.visibleCount", { count: events.length })} + variant="outlined" + /> + } + label={t("calendar.todayCount", { count: todayCount })} + color="warning" + variant="outlined" + /> + } + label={t("calendar.thisWeekCount", { count: weekCount })} + color="success" + variant="outlined" + /> - - - + + + {loading ? ( + + ) : null} + + + + - - ) -} -export default FarmerCalendarPage + + + + + {t("drawer.title")} + + + {t("drawer.description")} + + + + + + + {sidebarContent} + + + setEventModalOpen(false)} + /> + setEventDetailsOpen(false)} + onEdit={handleEditSelectedEvent} + /> + + ); +}; + +export default FarmerCalendarPage; diff --git a/src/views/dashboards/farm/farmerCalendar.constants.ts b/src/views/dashboards/farm/farmerCalendar.constants.ts new file mode 100644 index 0000000..f618113 --- /dev/null +++ b/src/views/dashboards/farm/farmerCalendar.constants.ts @@ -0,0 +1,13 @@ +export const FARMER_CALENDAR_TAG_OPTIONS = [ + "آبیاری", + "کوددهی", + "سم پاشی", + "برداشت", + "پایش", + "نگهداری", + "تیم", + "لجستیک", +] as const; + +export type FarmerCalendarTag = + (typeof FARMER_CALENDAR_TAG_OPTIONS)[number];