UPDATE
This commit is contained in:
@@ -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<string, unknown>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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": "روزانه"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -873,5 +873,131 @@
|
|||||||
"empty": "هنوز مکالمهای ندارید",
|
"empty": "هنوز مکالمهای ندارید",
|
||||||
"chatLabel": "چت"
|
"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": "پشتیبانی"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+4
-10
@@ -71,7 +71,7 @@
|
|||||||
"react-apexcharts": "1.4.1",
|
"react-apexcharts": "1.4.1",
|
||||||
"react-chartjs-2": "^5.3.1",
|
"react-chartjs-2": "^5.3.1",
|
||||||
"react-colorful": "5.6.1",
|
"react-colorful": "5.6.1",
|
||||||
"react-date-object": "1.1.9",
|
"react-date-object": "^2.1.9",
|
||||||
"react-datepicker": "7.3.0",
|
"react-datepicker": "7.3.0",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-dropzone": "14.3.5",
|
"react-dropzone": "14.3.5",
|
||||||
@@ -9044,9 +9044,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-date-object": {
|
"node_modules/react-date-object": {
|
||||||
"version": "1.1.9",
|
"version": "2.1.9",
|
||||||
"resolved": "https://mirror-npm.runflare.com/react-date-object/-/react-date-object-1.1.9.tgz",
|
"resolved": "https://mirror-npm.runflare.com/react-date-object/-/react-date-object-2.1.9.tgz",
|
||||||
"integrity": "sha512-NEbp/JSqwpF3nm4bXcez9XGwmlpOjSPJSYoRyPC1XOxzRHHaijp6xjMbYpyKB3O4yrluxsbwBHbO1WgeFKip3Q==",
|
"integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/react-datepicker": {
|
"node_modules/react-datepicker": {
|
||||||
@@ -9197,12 +9197,6 @@
|
|||||||
"react-dom": ">=16.8.0"
|
"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": {
|
"node_modules/react-onclickoutside": {
|
||||||
"version": "6.13.1",
|
"version": "6.13.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@
|
|||||||
"react-apexcharts": "1.4.1",
|
"react-apexcharts": "1.4.1",
|
||||||
"react-chartjs-2": "^5.3.1",
|
"react-chartjs-2": "^5.3.1",
|
||||||
"react-colorful": "5.6.1",
|
"react-colorful": "5.6.1",
|
||||||
"react-date-object": "1.1.9",
|
"react-date-object": "^2.1.9",
|
||||||
"react-datepicker": "7.3.0",
|
"react-datepicker": "7.3.0",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-dropzone": "14.3.5",
|
"react-dropzone": "14.3.5",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useTheme } from '@mui/material/styles'
|
|||||||
import type { BoxProps } from '@mui/material/Box'
|
import type { BoxProps } from '@mui/material/Box'
|
||||||
|
|
||||||
// Third-party Imports
|
// Third-party Imports
|
||||||
import { Calendar } from 'react-multi-date-picker'
|
import DatePicker from 'react-multi-date-picker'
|
||||||
import DateObject from 'react-date-object'
|
import DateObject from 'react-date-object'
|
||||||
import persian from 'react-date-object/calendars/persian'
|
import persian from 'react-date-object/calendars/persian'
|
||||||
import persian_fa from 'react-date-object/locales/persian_fa'
|
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 {
|
interface AppJalaliDatepickerProps {
|
||||||
value?: Date | null
|
value?: Date | null
|
||||||
onChange?: (date: Date) => void
|
onChange?: (date: Date) => void
|
||||||
|
placeholder?: string
|
||||||
boxProps?: BoxProps
|
boxProps?: BoxProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppJalaliDatepicker = (props: AppJalaliDatepickerProps) => {
|
const AppJalaliDatepicker = (props: AppJalaliDatepickerProps) => {
|
||||||
const { value: externalValue, onChange, boxProps } = props
|
const { value: externalValue, onChange, placeholder, boxProps } = props
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const [internalValue, setInternalValue] = useState<DateObject | undefined>(() => {
|
const [internalValue, setInternalValue] = useState<DateObject | undefined>(() => {
|
||||||
@@ -51,46 +52,18 @@ const AppJalaliDatepicker = (props: AppJalaliDatepickerProps) => {
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
{...boxProps}
|
{...boxProps}
|
||||||
sx={{
|
|
||||||
...(typeof boxProps?.sx === 'object' ? boxProps.sx : {}),
|
|
||||||
'& .rmdp-container': {
|
|
||||||
width: '100%'
|
|
||||||
},
|
|
||||||
'& .rmdp-wrapper': {
|
|
||||||
boxShadow: 'none !important',
|
|
||||||
border: 'none !important',
|
|
||||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
|
||||||
color: 'var(--mui-palette-text-primary)',
|
|
||||||
fontFamily: theme.typography.fontFamily
|
|
||||||
},
|
|
||||||
'& .rmdp-day:not(.rmdp-disabled):not(.rmdp-day-hidden)': {
|
|
||||||
color: 'var(--mui-palette-text-primary)'
|
|
||||||
},
|
|
||||||
'& .rmdp-day.rmdp-selected span:not(.highlight)': {
|
|
||||||
backgroundColor: 'var(--mui-palette-primary-main) !important',
|
|
||||||
color: 'var(--mui-palette-common-white) !important'
|
|
||||||
},
|
|
||||||
'& .rmdp-day.rmdp-today span': {
|
|
||||||
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
|
|
||||||
color: 'var(--mui-palette-primary-main)'
|
|
||||||
},
|
|
||||||
'& .rmdp-week-day': {
|
|
||||||
color: 'var(--mui-palette-text-primary)'
|
|
||||||
},
|
|
||||||
'& .rmdp-header-values': {
|
|
||||||
color: 'var(--mui-palette-text-primary)'
|
|
||||||
},
|
|
||||||
'& .rmdp-arrow': {
|
|
||||||
borderColor: 'var(--mui-palette-text-secondary)'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Calendar
|
<DatePicker
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
onChange={handleChange as any}
|
onChange={handleChange as any}
|
||||||
calendar={persian}
|
calendar={persian}
|
||||||
locale={persian_fa}
|
locale={persian_fa}
|
||||||
className="teal"
|
format='YYYY/MM/DD'
|
||||||
|
calendarPosition='bottom-right'
|
||||||
|
inputClass='rmdp-input'
|
||||||
|
placeholder={placeholder || 'انتخاب تاریخ'}
|
||||||
|
className='teal'
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,526 +1,264 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
// React Imports
|
import type { ComponentProps, ReactElement } from "react";
|
||||||
import type { ComponentProps } from 'react'
|
import { cloneElement, isValidElement } from "react";
|
||||||
|
|
||||||
// MUI imports
|
import Box from "@mui/material/Box";
|
||||||
import Box from '@mui/material/Box'
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { styled } from '@mui/material/styles'
|
import type { BoxProps } from "@mui/material/Box";
|
||||||
import type { BoxProps } from '@mui/material/Box'
|
|
||||||
|
|
||||||
// Third-party Imports
|
import DatePicker, { Calendar } from "react-multi-date-picker";
|
||||||
import ReactDatePickerComponent from 'react-datepicker'
|
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-multi-date-picker/styles/colors/teal.css";
|
||||||
import 'react-datepicker/dist/react-datepicker.css'
|
|
||||||
|
|
||||||
type Props = ComponentProps<typeof ReactDatePickerComponent> & {
|
type Props = Omit<
|
||||||
boxProps?: BoxProps
|
ComponentProps<typeof DatePicker>,
|
||||||
}
|
"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 convertFormat = (format?: string) => {
|
||||||
const StyledReactDatePicker = styled(Box)<BoxProps>(({ theme }) => {
|
if (!format) return "YYYY/MM/DD";
|
||||||
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)'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Time Picker
|
return format
|
||||||
'&:not(.react-datepicker--time-only)': {
|
.replace(/yyyy/g, "YYYY")
|
||||||
'& .react-datepicker__time-container': {
|
.replace(/yy/g, "YY")
|
||||||
borderLeftColor: 'var(--mui-palette-divider)',
|
.replace(/dd/g, "DD")
|
||||||
[theme.breakpoints.down('sm')]: {
|
.replace(/d/g, "D")
|
||||||
width: '5.5rem'
|
.replace(/HH/g, "HH")
|
||||||
},
|
.replace(/hh/g, "HH")
|
||||||
[theme.breakpoints.up('sm')]: {
|
.replace(/mm/g, "mm");
|
||||||
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)'
|
|
||||||
},
|
|
||||||
|
|
||||||
'& .react-datepicker__time': {
|
const toDate = (value: Value | null | undefined): Date | null => {
|
||||||
background: 'var(--mui-palette-background-paper)',
|
if (!value) return null;
|
||||||
'& .react-datepicker__time-box .react-datepicker__time-list-item--disabled': {
|
if (value instanceof Date) return value;
|
||||||
pointerEvents: 'none',
|
if (typeof value === "string" || typeof value === "number") {
|
||||||
color: 'var(--mui-palette-text-disabled)',
|
const parsed = new Date(value);
|
||||||
'&.react-datepicker__time-list-item--selected': {
|
|
||||||
fontWeight: 'normal',
|
|
||||||
backgroundColor: 'var(--mui-palette-action-disabledBackground)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'& .react-datepicker__time-list-item': {
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}) 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) => {
|
const AppReactDatepicker = (props: Props) => {
|
||||||
// Props
|
const {
|
||||||
const { boxProps, ...rest } = props
|
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
|
||||||
|
? [<TimePicker key="time-picker" position="bottom" hideSeconds />]
|
||||||
|
: [];
|
||||||
|
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<string, any>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
onClick={openCalendar}
|
||||||
|
onFocus={openCalendar}
|
||||||
|
sx={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: 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 (
|
return (
|
||||||
<StyledReactDatePicker {...boxProps}>
|
<Box {...boxProps} sx={[baseSx, boxProps?.sx]}>
|
||||||
<ReactDatePickerComponent popperPlacement='bottom-start' {...rest} />
|
{inline ? (
|
||||||
</StyledReactDatePicker>
|
<Calendar {...sharedProps} />
|
||||||
)
|
) : (
|
||||||
}
|
<DatePicker
|
||||||
|
{...sharedProps}
|
||||||
|
render={renderInput}
|
||||||
|
inputClass="rmdp-input"
|
||||||
|
placeholder={placeholderText}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default AppReactDatepicker
|
export default AppReactDatepicker;
|
||||||
|
|||||||
@@ -1,74 +1,87 @@
|
|||||||
// React Imports
|
// React Imports
|
||||||
import { useState, useEffect, forwardRef, useCallback } from 'react'
|
import { useState, useEffect, forwardRef, useCallback } from "react";
|
||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import Box from '@mui/material/Box'
|
import Box from "@mui/material/Box";
|
||||||
import Drawer from '@mui/material/Drawer'
|
import Drawer from "@mui/material/Drawer";
|
||||||
import Switch from '@mui/material/Switch'
|
import Switch from "@mui/material/Switch";
|
||||||
import Button from '@mui/material/Button'
|
import Button from "@mui/material/Button";
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import IconButton from "@mui/material/IconButton";
|
||||||
import IconButton from '@mui/material/IconButton'
|
import Typography from "@mui/material/Typography";
|
||||||
import Typography from '@mui/material/Typography'
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
import FormControl from "@mui/material/FormControl";
|
||||||
import FormControl from '@mui/material/FormControl'
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import type { Theme } from "@mui/material/styles";
|
||||||
import type { SelectChangeEvent } from '@mui/material/Select'
|
|
||||||
import type { Theme } from '@mui/material/styles'
|
|
||||||
|
|
||||||
// Third-party Imports
|
// Third-party Imports
|
||||||
import { useForm, Controller } from 'react-hook-form'
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
import PerfectScrollbar from "react-perfect-scrollbar";
|
||||||
|
|
||||||
// Type Imports
|
// Type Imports
|
||||||
import type { AddEventSidebarType, AddEventType } from '@/types/apps/calendarTypes'
|
import type {
|
||||||
|
AddEventSidebarType,
|
||||||
|
AddEventType,
|
||||||
|
} from "@/types/apps/calendarTypes";
|
||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import CustomTextField from '@core/components/mui/TextField'
|
import CustomTextField from "@core/components/mui/TextField";
|
||||||
|
|
||||||
// Styled Component Imports
|
// Styled Component Imports
|
||||||
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
|
import AppReactDatepicker from "@/libs/styles/AppReactDatepicker";
|
||||||
|
|
||||||
// Slice Imports
|
// 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 {
|
interface PickerProps {
|
||||||
label?: string
|
label?: string;
|
||||||
error?: boolean
|
error?: boolean;
|
||||||
registername?: string
|
registername?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DefaultStateType {
|
interface DefaultStateType {
|
||||||
url: string
|
url: string;
|
||||||
title: string
|
title: string;
|
||||||
allDay: boolean
|
allDay: boolean;
|
||||||
calendar: string
|
calendar: string;
|
||||||
description: string
|
description: string;
|
||||||
endDate: Date
|
endDate: Date;
|
||||||
startDate: Date
|
startDate: Date;
|
||||||
guests: string[] | undefined
|
guests: string[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vars
|
// Vars
|
||||||
const capitalize = (string: string) => string && string[0].toUpperCase() + string.slice(1)
|
const capitalize = (string: string) =>
|
||||||
|
string && string[0].toUpperCase() + string.slice(1);
|
||||||
|
|
||||||
// Vars
|
// Vars
|
||||||
const defaultState: DefaultStateType = {
|
const defaultState: DefaultStateType = {
|
||||||
url: '',
|
url: "",
|
||||||
title: '',
|
title: "",
|
||||||
guests: [],
|
guests: [],
|
||||||
allDay: true,
|
allDay: true,
|
||||||
description: '',
|
description: "",
|
||||||
endDate: new Date(),
|
endDate: new Date(),
|
||||||
calendar: 'Business',
|
calendar: "Business",
|
||||||
startDate: new Date()
|
startDate: new Date(),
|
||||||
}
|
};
|
||||||
|
|
||||||
const AddEventSidebar = (props: AddEventSidebarType) => {
|
const AddEventSidebar = (props: AddEventSidebarType) => {
|
||||||
// Props
|
// Props
|
||||||
const { calendarStore, dispatch, addEventSidebarOpen, handleAddEventSidebarToggle } = props
|
const {
|
||||||
|
calendarStore,
|
||||||
|
dispatch,
|
||||||
|
addEventSidebarOpen,
|
||||||
|
handleAddEventSidebarToggle,
|
||||||
|
} = props;
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const [values, setValues] = useState<DefaultStateType>(defaultState)
|
const [values, setValues] = useState<DefaultStateType>(defaultState);
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const PickersComponent = forwardRef(({ ...props }: PickerProps, ref) => {
|
const PickersComponent = forwardRef(({ ...props }: PickerProps, ref) => {
|
||||||
@@ -77,109 +90,108 @@ const AddEventSidebar = (props: AddEventSidebarType) => {
|
|||||||
inputRef={ref}
|
inputRef={ref}
|
||||||
fullWidth
|
fullWidth
|
||||||
{...props}
|
{...props}
|
||||||
label={props.label || ''}
|
label={props.label || ""}
|
||||||
className='is-full'
|
className="is-full"
|
||||||
error={props.error}
|
error={props.error}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
|
const isBelowSmScreen = useMediaQuery((theme: Theme) =>
|
||||||
|
theme.breakpoints.down("sm"),
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
setValue,
|
setValue,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors }
|
formState: { errors },
|
||||||
} = useForm({ defaultValues: { title: '' } })
|
} = useForm({ defaultValues: { title: "" } });
|
||||||
|
|
||||||
const resetToStoredValues = useCallback(() => {
|
const resetToStoredValues = useCallback(() => {
|
||||||
if (calendarStore.selectedEvent !== null) {
|
if (calendarStore.selectedEvent !== null) {
|
||||||
const event = calendarStore.selectedEvent
|
const event = calendarStore.selectedEvent;
|
||||||
|
|
||||||
setValue('title', event.title || '')
|
setValue("title", event.title || "");
|
||||||
setValues({
|
setValues({
|
||||||
url: event.url || '',
|
url: event.url || "",
|
||||||
title: event.title || '',
|
title: event.title || "",
|
||||||
allDay: event.allDay,
|
allDay: event.allDay,
|
||||||
guests: event.extendedProps.guests || [],
|
guests: event.extendedProps.guests || [],
|
||||||
description: event.extendedProps.description || '',
|
description: event.extendedProps.description || "",
|
||||||
calendar: event.extendedProps.calendar || 'Business',
|
calendar: event.extendedProps.calendar || "Business",
|
||||||
endDate: event.end !== null ? event.end : event.start,
|
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(() => {
|
const resetToEmptyValues = useCallback(() => {
|
||||||
setValue('title', '')
|
setValue("title", "");
|
||||||
setValues(defaultState)
|
setValues(defaultState);
|
||||||
}, [setValue])
|
}, [setValue]);
|
||||||
|
|
||||||
const handleSidebarClose = () => {
|
const handleSidebarClose = () => {
|
||||||
setValues(defaultState)
|
setValues(defaultState);
|
||||||
clearErrors()
|
clearErrors();
|
||||||
dispatch(selectedEvent(null))
|
dispatch(selectedEvent(null));
|
||||||
handleAddEventSidebarToggle()
|
handleAddEventSidebarToggle();
|
||||||
}
|
};
|
||||||
|
|
||||||
const onSubmit = (data: { title: string }) => {
|
const onSubmit = (data: { title: string }) => {
|
||||||
const modifiedEvent: AddEventType = {
|
const modifiedEvent: AddEventType = {
|
||||||
url: values.url,
|
url: values.url,
|
||||||
display: 'block',
|
display: "block",
|
||||||
title: data.title,
|
title: data.title,
|
||||||
end: values.endDate,
|
end: values.endDate,
|
||||||
allDay: values.allDay,
|
allDay: values.allDay,
|
||||||
start: values.startDate,
|
start: values.startDate,
|
||||||
extendedProps: {
|
extendedProps: {
|
||||||
calendar: capitalize(values.calendar),
|
calendar: capitalize(values.calendar),
|
||||||
guests: values.guests && values.guests.length ? values.guests : undefined,
|
guests:
|
||||||
description: values.description.length ? values.description : undefined
|
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 || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
calendarStore.selectedEvent === null ||
|
calendarStore.selectedEvent === null ||
|
||||||
(calendarStore.selectedEvent !== null && !calendarStore.selectedEvent.title.length)
|
(calendarStore.selectedEvent !== null &&
|
||||||
|
!calendarStore.selectedEvent.title.length)
|
||||||
) {
|
) {
|
||||||
dispatch(createEventAsync(eventData))
|
dispatch(addEvent(modifiedEvent));
|
||||||
} else {
|
} else {
|
||||||
dispatch(updateEventAsync({ id: calendarStore.selectedEvent.id, data: eventData }))
|
dispatch(
|
||||||
|
updateEvent({ ...modifiedEvent, id: calendarStore.selectedEvent.id }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(filterEvents())
|
dispatch(filterEvents());
|
||||||
|
|
||||||
handleSidebarClose()
|
handleSidebarClose();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDeleteButtonClick = () => {
|
const handleDeleteButtonClick = () => {
|
||||||
if (calendarStore.selectedEvent) {
|
if (calendarStore.selectedEvent) {
|
||||||
dispatch(deleteEventAsync(calendarStore.selectedEvent.id))
|
dispatch(deleteEvent(calendarStore.selectedEvent.id));
|
||||||
dispatch(filterEvents())
|
dispatch(filterEvents());
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSidebarClose()
|
// calendarApi.getEventById(calendarStore.selectedEvent.id).remove()
|
||||||
}
|
handleSidebarClose();
|
||||||
|
};
|
||||||
|
|
||||||
const handleStartDate = (date: Date | null) => {
|
const handleStartDate = (date: Date | null) => {
|
||||||
if (date && date > values.endDate) {
|
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 = () => {
|
const RenderSidebarFooter = () => {
|
||||||
if (
|
if (
|
||||||
@@ -187,184 +199,180 @@ const AddEventSidebar = (props: AddEventSidebarType) => {
|
|||||||
(calendarStore.selectedEvent && !calendarStore.selectedEvent.title.length)
|
(calendarStore.selectedEvent && !calendarStore.selectedEvent.title.length)
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className='flex gap-4'>
|
<div className="flex gap-4">
|
||||||
<Button type='submit' variant='contained'>
|
<Button type="submit" variant="contained">
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='outlined' color='secondary' onClick={resetToEmptyValues}>
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={resetToEmptyValues}
|
||||||
|
>
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className='flex gap-4'>
|
<div className="flex gap-4">
|
||||||
<Button type='submit' variant='contained'>
|
<Button type="submit" variant="contained">
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='outlined' color='secondary' onClick={resetToStoredValues}>
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={resetToStoredValues}
|
||||||
|
>
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const ScrollWrapper = isBelowSmScreen ? 'div' : PerfectScrollbar
|
const ScrollWrapper = isBelowSmScreen ? "div" : PerfectScrollbar;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (calendarStore.selectedEvent !== null) {
|
if (calendarStore.selectedEvent !== null) {
|
||||||
resetToStoredValues()
|
resetToStoredValues();
|
||||||
} else {
|
} else {
|
||||||
resetToEmptyValues()
|
resetToEmptyValues();
|
||||||
}
|
}
|
||||||
}, [addEventSidebarOpen, resetToStoredValues, resetToEmptyValues, calendarStore.selectedEvent])
|
}, [
|
||||||
|
addEventSidebarOpen,
|
||||||
|
resetToStoredValues,
|
||||||
|
resetToEmptyValues,
|
||||||
|
calendarStore.selectedEvent,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
anchor='right'
|
anchor="right"
|
||||||
open={addEventSidebarOpen}
|
open={addEventSidebarOpen}
|
||||||
onClose={handleSidebarClose}
|
onClose={handleSidebarClose}
|
||||||
ModalProps={{ keepMounted: true }}
|
ModalProps={{ keepMounted: true }}
|
||||||
sx={{ '& .MuiDrawer-paper': { width: ['100%', 400] } }}
|
sx={{ "& .MuiDrawer-paper": { width: ["100%", 400] } }}
|
||||||
>
|
>
|
||||||
<Box className='flex justify-between items-center sidebar-header plb-5 pli-6 border-be'>
|
<Box className="flex justify-between items-center sidebar-header plb-5 pli-6 border-be">
|
||||||
<Typography variant='h5'>
|
<Typography variant="h5">
|
||||||
{calendarStore.selectedEvent && calendarStore.selectedEvent.title.length ? 'Update Event' : 'Add Event'}
|
{calendarStore.selectedEvent &&
|
||||||
|
calendarStore.selectedEvent.title.length
|
||||||
|
? "Update Event"
|
||||||
|
: "Add Event"}
|
||||||
</Typography>
|
</Typography>
|
||||||
{calendarStore.selectedEvent && calendarStore.selectedEvent.title.length ? (
|
{calendarStore.selectedEvent &&
|
||||||
<Box className='flex items-center' sx={{ gap: calendarStore.selectedEvent !== null ? 1 : 0 }}>
|
calendarStore.selectedEvent.title.length ? (
|
||||||
<IconButton size='small' onClick={handleDeleteButtonClick}>
|
<Box
|
||||||
<i className='tabler-trash text-2xl text-textPrimary' />
|
className="flex items-center"
|
||||||
|
sx={{ gap: calendarStore.selectedEvent !== null ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
<IconButton size="small" onClick={handleDeleteButtonClick}>
|
||||||
|
<i className="tabler-trash text-2xl text-textPrimary" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton size='small' onClick={handleSidebarClose}>
|
<IconButton size="small" onClick={handleSidebarClose}>
|
||||||
<i className='tabler-x text-2xl text-textPrimary' />
|
<i className="tabler-x text-2xl text-textPrimary" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<IconButton size='small' onClick={handleSidebarClose}>
|
<IconButton size="small" onClick={handleSidebarClose}>
|
||||||
<i className='tabler-x text-2xl text-textPrimary' />
|
<i className="tabler-x text-2xl text-textPrimary" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<ScrollWrapper
|
<ScrollWrapper
|
||||||
{...(isBelowSmScreen
|
{...(isBelowSmScreen
|
||||||
? { className: 'bs-full overflow-y-auto overflow-x-hidden' }
|
? { className: "bs-full overflow-y-auto overflow-x-hidden" }
|
||||||
: { options: { wheelPropagation: false, suppressScrollX: true } })}
|
: { options: { wheelPropagation: false, suppressScrollX: true } })}
|
||||||
>
|
>
|
||||||
<Box className='sidebar-body plb-5 pli-6'>
|
<Box className="sidebar-body plb-5 pli-6">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete='off' className='flex flex-col gap-6'>
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
autoComplete="off"
|
||||||
|
className="flex flex-col gap-6"
|
||||||
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name='title'
|
name="title"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label='عنوان'
|
label="Title"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
{...(errors.title && { error: true, helperText: 'This field is required' })}
|
{...(errors.title && {
|
||||||
|
error: true,
|
||||||
|
helperText: "This field is required",
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<CustomTextField
|
|
||||||
select
|
|
||||||
fullWidth
|
|
||||||
label='تقویم'
|
|
||||||
value={values.calendar}
|
|
||||||
onChange={e => setValues({ ...values, calendar: e.target.value })}
|
|
||||||
>
|
|
||||||
<MenuItem value='Personal'>Personal</MenuItem>
|
|
||||||
<MenuItem value='Business'>Business</MenuItem>
|
|
||||||
<MenuItem value='Family'>Family</MenuItem>
|
|
||||||
<MenuItem value='Holiday'>Holiday</MenuItem>
|
|
||||||
<MenuItem value='ETC'>ETC</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
|
|
||||||
<AppReactDatepicker
|
<AppReactDatepicker
|
||||||
selectsStart
|
selectsStart
|
||||||
id='event-start-date'
|
id="event-start-date"
|
||||||
endDate={values.endDate}
|
endDate={values.endDate}
|
||||||
selected={values.startDate}
|
selected={values.startDate}
|
||||||
startDate={values.startDate}
|
startDate={values.startDate}
|
||||||
showTimeSelect={!values.allDay}
|
showTimeSelect={!values.allDay}
|
||||||
dateFormat={!values.allDay ? 'yyyy-MM-dd hh:mm' : 'yyyy-MM-dd'}
|
dateFormat={!values.allDay ? "yyyy-MM-dd hh:mm" : "yyyy-MM-dd"}
|
||||||
customInput={<PickersComponent label='Start Date' registername='startDate' />}
|
customInput={
|
||||||
onChange={(date: Date | null) => date !== null && setValues({ ...values, startDate: new Date(date) })}
|
<PickersComponent label="Start Date" registername="startDate" />
|
||||||
|
}
|
||||||
|
onChange={(date: Date | null) =>
|
||||||
|
date !== null &&
|
||||||
|
setValues({ ...values, startDate: new Date(date) })
|
||||||
|
}
|
||||||
onSelect={handleStartDate}
|
onSelect={handleStartDate}
|
||||||
/>
|
/>
|
||||||
<AppReactDatepicker
|
<AppReactDatepicker
|
||||||
selectsEnd
|
selectsEnd
|
||||||
id='event-end-date'
|
id="event-end-date"
|
||||||
endDate={values.endDate}
|
endDate={values.endDate}
|
||||||
selected={values.endDate}
|
selected={values.endDate}
|
||||||
minDate={values.startDate}
|
minDate={values.startDate}
|
||||||
startDate={values.startDate}
|
startDate={values.startDate}
|
||||||
showTimeSelect={!values.allDay}
|
showTimeSelect={!values.allDay}
|
||||||
dateFormat={!values.allDay ? 'yyyy-MM-dd hh:mm' : 'yyyy-MM-dd'}
|
dateFormat={!values.allDay ? "yyyy-MM-dd hh:mm" : "yyyy-MM-dd"}
|
||||||
customInput={<PickersComponent label='End Date' registername='endDate' />}
|
customInput={
|
||||||
onChange={(date: Date | null) => date !== null && setValues({ ...values, endDate: new Date(date) })}
|
<PickersComponent label="End Date" registername="endDate" />
|
||||||
|
}
|
||||||
|
onChange={(date: Date | null) =>
|
||||||
|
date !== null &&
|
||||||
|
setValues({ ...values, endDate: new Date(date) })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label='All Day'
|
label="All Day"
|
||||||
control={
|
control={
|
||||||
<Switch checked={values.allDay} onChange={e => setValues({ ...values, allDay: e.target.checked })} />
|
<Switch
|
||||||
|
checked={values.allDay}
|
||||||
|
onChange={(e) =>
|
||||||
|
setValues({ ...values, allDay: e.target.checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
type='url'
|
|
||||||
id='event-url'
|
|
||||||
label='Event URL'
|
|
||||||
value={values.url}
|
|
||||||
onChange={e => setValues({ ...values, url: e.target.value })}
|
|
||||||
/>
|
|
||||||
<CustomTextField
|
|
||||||
fullWidth
|
|
||||||
select
|
|
||||||
label='Guests'
|
|
||||||
value={values.guests}
|
|
||||||
id='event-guests-select'
|
|
||||||
// eslint-disable-next-line lines-around-comment
|
|
||||||
// @ts-ignore
|
|
||||||
onChange={(e: SelectChangeEvent<(typeof values)['guests']>) => {
|
|
||||||
setValues({
|
|
||||||
...values,
|
|
||||||
guests: typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
slotProps={{
|
|
||||||
select: {
|
|
||||||
multiple: true
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem value='bruce'>Bruce</MenuItem>
|
|
||||||
<MenuItem value='clark'>Clark</MenuItem>
|
|
||||||
<MenuItem value='diana'>Diana</MenuItem>
|
|
||||||
<MenuItem value='john'>John</MenuItem>
|
|
||||||
<MenuItem value='barry'>Barry</MenuItem>
|
|
||||||
</CustomTextField>
|
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
rows={4}
|
rows={4}
|
||||||
multiline
|
multiline
|
||||||
fullWidth
|
fullWidth
|
||||||
label='توضیحات'
|
label="Description"
|
||||||
id='event-description'
|
id="event-description"
|
||||||
value={values.description}
|
value={values.description}
|
||||||
onChange={e => setValues({ ...values, description: e.target.value })}
|
onChange={(e) =>
|
||||||
|
setValues({ ...values, description: e.target.value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className='flex items-center'>
|
<div className="flex items-center">
|
||||||
<RenderSidebarFooter />
|
<RenderSidebarFooter />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
</ScrollWrapper>
|
</ScrollWrapper>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AddEventSidebar
|
export default AddEventSidebar;
|
||||||
|
|||||||
@@ -1,53 +1,68 @@
|
|||||||
// React Imports
|
// React Imports
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import { useTheme } from '@mui/material/styles'
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
// Third-party imports
|
// 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 FullCalendar from "@fullcalendar/react";
|
||||||
import faLocale from '@fullcalendar/core/locales/fa'
|
import listPlugin from "@fullcalendar/list";
|
||||||
import listPlugin from '@fullcalendar/list'
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
import interactionPlugin from '@fullcalendar/interaction'
|
import type { CalendarOptions } from "@fullcalendar/core";
|
||||||
import type { CalendarOptions } from '@fullcalendar/core'
|
import faLocale from "@fullcalendar/core/locales/fa";
|
||||||
|
|
||||||
// Type Imports
|
// Type Imports
|
||||||
import type { AddEventType, CalendarColors, CalendarType } from '@/types/apps/calendarTypes'
|
import type {
|
||||||
import type { AppDispatch } from '@/redux-store'
|
AddEventType,
|
||||||
|
CalendarColors,
|
||||||
|
CalendarType,
|
||||||
|
} from "@/types/apps/calendarTypes";
|
||||||
|
|
||||||
// Slice Imports
|
// Slice Imports
|
||||||
import { filterEvents, selectedEvent, updateEventAsync } from '@/redux-store/slices/calendar'
|
import {
|
||||||
|
filterEvents,
|
||||||
|
selectedEvent,
|
||||||
|
updateEvent,
|
||||||
|
} from "@/redux-store/slices/calendar";
|
||||||
|
|
||||||
type CalenderProps = {
|
type CalenderProps = {
|
||||||
calendarStore: CalendarType
|
calendarStore: CalendarType;
|
||||||
calendarApi: any
|
calendarApi: any;
|
||||||
setCalendarApi: (val: any) => void
|
setCalendarApi: (val: any) => void;
|
||||||
calendarsColor: CalendarColors
|
calendarsColor: CalendarColors;
|
||||||
dispatch?: AppDispatch
|
dispatch: Dispatch;
|
||||||
handleLeftSidebarToggle?: () => void
|
handleLeftSidebarToggle: () => void;
|
||||||
handleAddEventSidebarToggle?: () => void
|
handleAddEventSidebarToggle: () => void;
|
||||||
editable?: boolean
|
handleEventDetailsOpen: () => void;
|
||||||
showSidebarToggle?: boolean
|
};
|
||||||
onDateClick?: (date: Date) => void
|
|
||||||
onEventClick?: (event: any) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const blankEvent: AddEventType = {
|
const blankEvent: AddEventType = {
|
||||||
title: '',
|
title: "",
|
||||||
start: '',
|
start: "",
|
||||||
end: '',
|
end: "",
|
||||||
allDay: false,
|
allDay: false,
|
||||||
url: '',
|
url: "",
|
||||||
extendedProps: {
|
extendedProps: {
|
||||||
calendar: '',
|
calendar: "",
|
||||||
guests: [],
|
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) => {
|
const Calendar = (props: CalenderProps) => {
|
||||||
// Props
|
// Props
|
||||||
@@ -58,110 +73,58 @@ const Calendar = (props: CalenderProps) => {
|
|||||||
calendarsColor,
|
calendarsColor,
|
||||||
dispatch,
|
dispatch,
|
||||||
handleAddEventSidebarToggle,
|
handleAddEventSidebarToggle,
|
||||||
|
handleEventDetailsOpen,
|
||||||
handleLeftSidebarToggle,
|
handleLeftSidebarToggle,
|
||||||
editable = true,
|
} = props;
|
||||||
showSidebarToggle = true,
|
|
||||||
onDateClick,
|
|
||||||
onEventClick
|
|
||||||
} = props
|
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const calendarRef = useRef()
|
const calendarRef = useRef();
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const theme = useTheme()
|
const theme = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (calendarApi === null) {
|
if (calendarApi === null) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
setCalendarApi(calendarRef.current?.getApi())
|
setCalendarApi(calendarRef.current?.getApi());
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// calendarOptions(Props)
|
// calendarOptions(Props)
|
||||||
const calendarOptions: CalendarOptions = {
|
const calendarOptions: CalendarOptions = {
|
||||||
events: calendarStore.events,
|
events: calendarStore.events,
|
||||||
plugins: [interactionPlugin, dayGridPlugin, timeGridPlugin, listPlugin],
|
plugins: [interactionPlugin, dayGridPlugin, timeGridPlugin, listPlugin],
|
||||||
initialView: 'dayGridMonth',
|
locale: faLocale,
|
||||||
locale: 'fa',
|
initialView: "dayGridMonth",
|
||||||
headerToolbar: {
|
headerToolbar: {
|
||||||
start: `${showSidebarToggle ? 'sidebarToggle,' : ''}prev,next,title`,
|
start: "sidebarToggle, prev, next, title",
|
||||||
end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth'
|
end: "dayGridMonth,timeGridWeek,timeGridDay,listMonth",
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
today: "امروز",
|
||||||
|
month: "ماه",
|
||||||
|
week: "هفته",
|
||||||
|
day: "روز",
|
||||||
|
list: "فهرست",
|
||||||
},
|
},
|
||||||
views: {
|
views: {
|
||||||
week: {
|
week: {
|
||||||
titleFormat(arg: any) {
|
titleFormat: { year: "numeric", month: "short", day: "numeric" },
|
||||||
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)}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
dayHeaderContent(arg) {
|
dayHeaderContent(arg) {
|
||||||
const formatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { weekday: 'short' })
|
return formatPersianWeekday(arg.date);
|
||||||
|
|
||||||
return formatter.format(arg.date)
|
|
||||||
},
|
},
|
||||||
titleFormat(arg: any) {
|
dayCellContent(arg) {
|
||||||
const { start, end } = arg
|
return formatPersianDay(arg.date);
|
||||||
|
|
||||||
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: 'لیست'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Enable dragging and resizing event
|
Enable dragging and resizing event
|
||||||
? Docs: https://fullcalendar.io/docs/editable
|
? Docs: https://fullcalendar.io/docs/editable
|
||||||
*/
|
*/
|
||||||
editable,
|
editable: true,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Enable resizing event from start
|
Enable resizing event from start
|
||||||
@@ -189,28 +152,30 @@ const Calendar = (props: CalenderProps) => {
|
|||||||
|
|
||||||
eventClassNames({ event: calendarEvent }: any) {
|
eventClassNames({ event: calendarEvent }: any) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const colorName = calendarsColor[calendarEvent._def.extendedProps.calendar]
|
const colorName =
|
||||||
|
calendarsColor[calendarEvent._def.extendedProps.calendar];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
// Background Color
|
// Background Color
|
||||||
`event-bg-${colorName}`
|
`event-bg-${colorName}`,
|
||||||
]
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
eventClick({ event: clickedEvent, jsEvent }: any) {
|
eventClick({ event: clickedEvent, jsEvent }: any) {
|
||||||
jsEvent.preventDefault()
|
jsEvent.preventDefault();
|
||||||
|
|
||||||
onEventClick?.(clickedEvent)
|
dispatch(
|
||||||
|
selectedEvent({
|
||||||
if (dispatch && handleAddEventSidebarToggle) {
|
id: clickedEvent.id,
|
||||||
dispatch(selectedEvent(clickedEvent))
|
url: clickedEvent.url,
|
||||||
handleAddEventSidebarToggle()
|
title: clickedEvent.title,
|
||||||
}
|
allDay: clickedEvent.allDay,
|
||||||
|
end: clickedEvent.end,
|
||||||
if (clickedEvent.url) {
|
start: clickedEvent.start,
|
||||||
// Open the URL in a new tab
|
extendedProps: clickedEvent.extendedProps,
|
||||||
window.open(clickedEvent.url, '_blank')
|
}),
|
||||||
}
|
);
|
||||||
|
handleEventDetailsOpen();
|
||||||
|
|
||||||
//* Only grab required field otherwise it goes in infinity loop
|
//* Only grab required field otherwise it goes in infinity loop
|
||||||
//! Always grab all fields rendered by form (even if it get `undefined`)
|
//! Always grab all fields rendered by form (even if it get `undefined`)
|
||||||
@@ -220,28 +185,22 @@ const Calendar = (props: CalenderProps) => {
|
|||||||
|
|
||||||
customButtons: {
|
customButtons: {
|
||||||
sidebarToggle: {
|
sidebarToggle: {
|
||||||
icon: 'tabler tabler-menu-2',
|
icon: "tabler tabler-menu-2",
|
||||||
click() {
|
click() {
|
||||||
handleLeftSidebarToggle?.()
|
handleLeftSidebarToggle();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
dateClick(info: any) {
|
dateClick(info: any) {
|
||||||
onDateClick?.(info.date)
|
const ev = { ...blankEvent };
|
||||||
|
|
||||||
if (!dispatch || !handleAddEventSidebarToggle) {
|
ev.start = info.date;
|
||||||
return
|
ev.end = info.date;
|
||||||
}
|
ev.allDay = true;
|
||||||
|
|
||||||
const ev = { ...blankEvent }
|
dispatch(selectedEvent(ev));
|
||||||
|
handleAddEventSidebarToggle();
|
||||||
ev.start = info.date
|
|
||||||
ev.end = info.date
|
|
||||||
ev.allDay = true
|
|
||||||
|
|
||||||
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
|
? 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) {
|
eventDrop({ event: droppedEvent }: any) {
|
||||||
if (!dispatch) {
|
dispatch(updateEvent(droppedEvent));
|
||||||
return
|
dispatch(filterEvents());
|
||||||
}
|
|
||||||
|
|
||||||
// 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())
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -269,27 +218,17 @@ const Calendar = (props: CalenderProps) => {
|
|||||||
? Docs: https://fullcalendar.io/docs/eventResize
|
? Docs: https://fullcalendar.io/docs/eventResize
|
||||||
*/
|
*/
|
||||||
eventResize({ event: resizedEvent }: any) {
|
eventResize({ event: resizedEvent }: any) {
|
||||||
if (!dispatch) {
|
dispatch(updateEvent(resizedEvent));
|
||||||
return
|
dispatch(filterEvents());
|
||||||
}
|
|
||||||
|
|
||||||
// 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())
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
ref: calendarRef,
|
ref: calendarRef,
|
||||||
|
|
||||||
direction: theme.direction
|
direction: theme.direction,
|
||||||
}
|
};
|
||||||
|
|
||||||
return <FullCalendar {...calendarOptions} />
|
return <FullCalendar {...calendarOptions} />;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Calendar
|
export default Calendar;
|
||||||
|
|||||||
@@ -1,58 +1,79 @@
|
|||||||
'use client'
|
'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 = () => {
|
const AppCalendar = () => {
|
||||||
return <></>
|
// States
|
||||||
// // States
|
const [calendarApi, setCalendarApi] = useState<null | any>(null)
|
||||||
// const [calendarApi, setCalendarApi] = useState<null | any>(null)
|
const [leftSidebarOpen, setLeftSidebarOpen] = useState<boolean>(false)
|
||||||
// const [leftSidebarOpen, setLeftSidebarOpen] = useState<boolean>(false)
|
const [addEventSidebarOpen, setAddEventSidebarOpen] = useState<boolean>(false)
|
||||||
// const [addEventSidebarOpen, setAddEventSidebarOpen] = useState<boolean>(false)
|
|
||||||
|
|
||||||
// // Hooks
|
// Hooks
|
||||||
// const dispatch = useAppDispatch()
|
const dispatch = useDispatch()
|
||||||
// const calendarStore = useSelector((state: { calendarReducer: CalendarType & { loading: boolean; error: string | null } }) => state.calendarReducer)
|
const calendarStore = useSelector((state: { calendarReducer: CalendarType }) => state.calendarReducer)
|
||||||
// const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md'))
|
const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md'))
|
||||||
|
|
||||||
// // Fetch events on mount
|
const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen)
|
||||||
// useEffect(() => {
|
|
||||||
// dispatch(fetchEvents())
|
|
||||||
// }, [dispatch])
|
|
||||||
|
|
||||||
// const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen)
|
const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen)
|
||||||
|
|
||||||
// const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen)
|
return (
|
||||||
|
<>
|
||||||
// return (
|
<SidebarLeft
|
||||||
// <>
|
mdAbove={mdAbove}
|
||||||
// <SidebarLeft
|
dispatch={dispatch}
|
||||||
// mdAbove={mdAbove}
|
calendarApi={calendarApi}
|
||||||
// dispatch={dispatch}
|
calendarStore={calendarStore}
|
||||||
// calendarApi={calendarApi}
|
calendarsColor={calendarsColor}
|
||||||
// calendarStore={calendarStore}
|
leftSidebarOpen={leftSidebarOpen}
|
||||||
// calendarsColor={calendarsColor}
|
handleLeftSidebarToggle={handleLeftSidebarToggle}
|
||||||
// leftSidebarOpen={leftSidebarOpen}
|
handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||||
// handleLeftSidebarToggle={handleLeftSidebarToggle}
|
/>
|
||||||
// handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
<div className='p-6 pbe-0 flex-grow overflow-visible bg-backgroundPaper rounded'>
|
||||||
// />
|
<Calendar
|
||||||
// <div className='p-6 pbe-0 flex-grow overflow-visible bg-backgroundPaper rounded'>
|
dispatch={dispatch}
|
||||||
// <Calendar
|
calendarApi={calendarApi}
|
||||||
// dispatch={dispatch}
|
calendarStore={calendarStore}
|
||||||
// calendarApi={calendarApi}
|
setCalendarApi={setCalendarApi}
|
||||||
// calendarStore={calendarStore}
|
calendarsColor={calendarsColor}
|
||||||
// setCalendarApi={setCalendarApi}
|
handleLeftSidebarToggle={handleLeftSidebarToggle}
|
||||||
// calendarsColor={calendarsColor}
|
handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||||
// handleLeftSidebarToggle={handleLeftSidebarToggle}
|
/>
|
||||||
// handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
</div>
|
||||||
// />
|
<AddEventSidebar
|
||||||
// </div>
|
dispatch={dispatch}
|
||||||
// <AddEventSidebar
|
calendarApi={calendarApi}
|
||||||
// dispatch={dispatch}
|
calendarStore={calendarStore}
|
||||||
// calendarApi={calendarApi}
|
addEventSidebarOpen={addEventSidebarOpen}
|
||||||
// calendarStore={calendarStore}
|
handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||||
// addEventSidebarOpen={addEventSidebarOpen}
|
/>
|
||||||
// handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
</>
|
||||||
// />
|
)
|
||||||
// </>
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppCalendar
|
export default AppCalendar
|
||||||
|
|||||||
@@ -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
|
// Types Imports
|
||||||
import type { SidebarLeftProps, CalendarFiltersType } from '@/types/apps/calendarTypes'
|
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) => {
|
const SidebarLeft = (props: SidebarLeftProps) => {
|
||||||
// Props
|
// Props
|
||||||
@@ -15,205 +32,106 @@ const SidebarLeft = (props: SidebarLeftProps) => {
|
|||||||
handleAddEventSidebarToggle
|
handleAddEventSidebarToggle
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
// // Vars
|
// Vars
|
||||||
// const colorsArr = calendarsColor ? Object.entries(calendarsColor) : []
|
const colorsArr = calendarsColor ? Object.entries(calendarsColor) : []
|
||||||
|
|
||||||
// const renderFilters = colorsArr.length
|
const renderFilters = colorsArr.length
|
||||||
// ? colorsArr.map(([key, value]: string[]) => {
|
? colorsArr.map(([key, value]: string[]) => {
|
||||||
// return (
|
return (
|
||||||
// <FormControlLabel
|
<FormControlLabel
|
||||||
// className='mbe-1'
|
className='mbe-1'
|
||||||
// key={key}
|
key={key}
|
||||||
// label={key}
|
label={key}
|
||||||
// control={
|
control={
|
||||||
// <Checkbox
|
<Checkbox
|
||||||
// color={value as ThemeColor}
|
color={value as ThemeColor}
|
||||||
// checked={calendarStore.selectedCalendars.indexOf(key as CalendarFiltersType) > -1}
|
checked={calendarStore.selectedCalendars.indexOf(key as CalendarFiltersType) > -1}
|
||||||
// onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))}
|
onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))}
|
||||||
// />
|
/>
|
||||||
// }
|
}
|
||||||
// />
|
/>
|
||||||
// )
|
)
|
||||||
// })
|
})
|
||||||
// : null
|
: null
|
||||||
|
|
||||||
// const handleSidebarToggleSidebar = () => {
|
const handleSidebarToggleSidebar = () => {
|
||||||
// dispatch(selectedEvent(null))
|
dispatch(selectedEvent(null))
|
||||||
// handleAddEventSidebarToggle()
|
handleAddEventSidebarToggle()
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (renderFilters) {
|
if (renderFilters) {
|
||||||
// return (
|
return (
|
||||||
// <Drawer
|
<Drawer
|
||||||
// open={leftSidebarOpen}
|
open={leftSidebarOpen}
|
||||||
// onClose={handleLeftSidebarToggle}
|
onClose={handleLeftSidebarToggle}
|
||||||
// variant={mdAbove ? 'permanent' : 'temporary'}
|
variant={mdAbove ? 'permanent' : 'temporary'}
|
||||||
// ModalProps={{
|
ModalProps={{
|
||||||
// disablePortal: true,
|
disablePortal: true,
|
||||||
// disableAutoFocus: true,
|
disableAutoFocus: true,
|
||||||
// disableScrollLock: true,
|
disableScrollLock: true,
|
||||||
// keepMounted: true // Better open performance on mobile.
|
keepMounted: true // Better open performance on mobile.
|
||||||
// }}
|
}}
|
||||||
// className={classnames('block', { static: mdAbove, absolute: !mdAbove })}
|
className={classnames('block', { static: mdAbove, absolute: !mdAbove })}
|
||||||
// PaperProps={{
|
PaperProps={{
|
||||||
// className: classnames('items-start is-[280px] shadow-none rounded rounded-se-none rounded-ee-none', {
|
className: classnames('items-start is-[280px] shadow-none rounded rounded-se-none rounded-ee-none', {
|
||||||
// static: mdAbove,
|
static: mdAbove,
|
||||||
// absolute: !mdAbove
|
absolute: !mdAbove
|
||||||
// })
|
})
|
||||||
// }}
|
}}
|
||||||
// sx={{
|
sx={{
|
||||||
// zIndex: 3,
|
zIndex: 3,
|
||||||
// '& .MuiDrawer-paper': {
|
'& .MuiDrawer-paper': {
|
||||||
// zIndex: mdAbove ? 2 : 'drawer'
|
zIndex: mdAbove ? 2 : 'drawer'
|
||||||
// },
|
},
|
||||||
// '& .MuiBackdrop-root': {
|
'& .MuiBackdrop-root': {
|
||||||
// borderRadius: 1,
|
borderRadius: 1,
|
||||||
// position: 'absolute'
|
position: 'absolute'
|
||||||
// }
|
}
|
||||||
// }}
|
}}
|
||||||
// >
|
>
|
||||||
// <div className='is-full p-6'>
|
<div className='is-full p-6'>
|
||||||
// <Button
|
<Button
|
||||||
// fullWidth
|
fullWidth
|
||||||
// variant='contained'
|
variant='contained'
|
||||||
// onClick={handleSidebarToggleSidebar}
|
onClick={handleSidebarToggleSidebar}
|
||||||
// startIcon={<i className='tabler-plus' />}
|
startIcon={<i className='tabler-plus' />}
|
||||||
// >
|
>
|
||||||
// Add Event
|
Add Event
|
||||||
// </Button>
|
</Button>
|
||||||
// </div>
|
</div>
|
||||||
// <Divider className='is-full' />
|
<Divider className='is-full' />
|
||||||
// <AppJalaliDatepicker
|
<AppReactDatepicker
|
||||||
// onChange={date => calendarApi?.gotoDate(date)}
|
inline
|
||||||
// boxProps={{
|
onChange={date => calendarApi.gotoDate(date)}
|
||||||
// className: 'flex justify-center is-full',
|
boxProps={{
|
||||||
// sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } }
|
className: 'flex justify-center is-full',
|
||||||
// }}
|
sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } }
|
||||||
// />
|
}}
|
||||||
// <Divider className='is-full' />
|
/>
|
||||||
|
<Divider className='is-full' />
|
||||||
|
|
||||||
// <div className='flex flex-col p-6 is-full'>
|
<div className='flex flex-col p-6 is-full'>
|
||||||
// <Typography variant='h5' className='mbe-4'>
|
<Typography variant='h5' className='mbe-4'>
|
||||||
// Event Filters
|
Event Filters
|
||||||
// </Typography>
|
</Typography>
|
||||||
// <FormControlLabel
|
<FormControlLabel
|
||||||
// className='mbe-1'
|
className='mbe-1'
|
||||||
// label='View All'
|
label='View All'
|
||||||
// control={
|
control={
|
||||||
// <Checkbox
|
<Checkbox
|
||||||
// color='secondary'
|
color='secondary'
|
||||||
// checked={calendarStore.selectedCalendars.length === colorsArr.length}
|
checked={calendarStore.selectedCalendars.length === colorsArr.length}
|
||||||
// onChange={e => dispatch(filterAllCalendarLabels(e.target.checked))}
|
onChange={e => dispatch(filterAllCalendarLabels(e.target.checked))}
|
||||||
// />
|
/>
|
||||||
// }
|
}
|
||||||
// />
|
/>
|
||||||
// {renderFilters}
|
{renderFilters}
|
||||||
// </div>
|
</div>
|
||||||
// </Drawer>
|
</Drawer>
|
||||||
// )
|
)
|
||||||
// } else {
|
} else {
|
||||||
// return null
|
return null
|
||||||
// }
|
}
|
||||||
// // Vars
|
|
||||||
// const colorsArr = calendarsColor ? Object.entries(calendarsColor) : []
|
|
||||||
|
|
||||||
// const renderFilters = colorsArr.length
|
|
||||||
// ? colorsArr.map(([key, value]: string[]) => {
|
|
||||||
// return (
|
|
||||||
// <FormControlLabel
|
|
||||||
// className='mbe-1'
|
|
||||||
// key={key}
|
|
||||||
// label={key}
|
|
||||||
// control={
|
|
||||||
// <Checkbox
|
|
||||||
// color={value as ThemeColor}
|
|
||||||
// checked={calendarStore.selectedCalendars.indexOf(key as CalendarFiltersType) > -1}
|
|
||||||
// onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))}
|
|
||||||
// />
|
|
||||||
// }
|
|
||||||
// />
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
// : null
|
|
||||||
|
|
||||||
// const handleSidebarToggleSidebar = () => {
|
|
||||||
// dispatch(selectedEvent(null))
|
|
||||||
// handleAddEventSidebarToggle()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (renderFilters) {
|
|
||||||
// return (
|
|
||||||
// <Drawer
|
|
||||||
// open={leftSidebarOpen}
|
|
||||||
// onClose={handleLeftSidebarToggle}
|
|
||||||
// variant={mdAbove ? 'permanent' : 'temporary'}
|
|
||||||
// ModalProps={{
|
|
||||||
// disablePortal: true,
|
|
||||||
// disableAutoFocus: true,
|
|
||||||
// disableScrollLock: true,
|
|
||||||
// keepMounted: true // Better open performance on mobile.
|
|
||||||
// }}
|
|
||||||
// className={classnames('block', { static: mdAbove, absolute: !mdAbove })}
|
|
||||||
// PaperProps={{
|
|
||||||
// className: classnames('items-start is-[280px] shadow-none rounded rounded-se-none rounded-ee-none', {
|
|
||||||
// static: mdAbove,
|
|
||||||
// absolute: !mdAbove
|
|
||||||
// })
|
|
||||||
// }}
|
|
||||||
// sx={{
|
|
||||||
// zIndex: 3,
|
|
||||||
// '& .MuiDrawer-paper': {
|
|
||||||
// zIndex: mdAbove ? 2 : 'drawer'
|
|
||||||
// },
|
|
||||||
// '& .MuiBackdrop-root': {
|
|
||||||
// borderRadius: 1,
|
|
||||||
// position: 'absolute'
|
|
||||||
// }
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// <div className='is-full p-6'>
|
|
||||||
// <Button
|
|
||||||
// fullWidth
|
|
||||||
// variant='contained'
|
|
||||||
// onClick={handleSidebarToggleSidebar}
|
|
||||||
// startIcon={<i className='tabler-plus' />}
|
|
||||||
// >
|
|
||||||
// Add Event
|
|
||||||
// </Button>
|
|
||||||
// </div>
|
|
||||||
// <Divider className='is-full' />
|
|
||||||
// <AppJalaliDatepicker
|
|
||||||
// onChange={date => calendarApi?.gotoDate(date)}
|
|
||||||
// boxProps={{
|
|
||||||
// className: 'flex justify-center is-full',
|
|
||||||
// sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } }
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// <Divider className='is-full' />
|
|
||||||
|
|
||||||
// <div className='flex flex-col p-6 is-full'>
|
|
||||||
// <Typography variant='h5' className='mbe-4'>
|
|
||||||
// Event Filters
|
|
||||||
// </Typography>
|
|
||||||
// <FormControlLabel
|
|
||||||
// className='mbe-1'
|
|
||||||
// label='View All'
|
|
||||||
// control={
|
|
||||||
// <Checkbox
|
|
||||||
// color='secondary'
|
|
||||||
// checked={calendarStore.selectedCalendars.length === colorsArr.length}
|
|
||||||
// onChange={e => dispatch(filterAllCalendarLabels(e.target.checked))}
|
|
||||||
// />
|
|
||||||
// }
|
|
||||||
// />
|
|
||||||
// {renderFilters}
|
|
||||||
// </div>
|
|
||||||
// </Drawer>
|
|
||||||
// )
|
|
||||||
// } else {
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
return <></>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SidebarLeft
|
export default SidebarLeft
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Stack spacing={1.25}>
|
||||||
|
<Chip
|
||||||
|
label={categoryLabel}
|
||||||
|
color="success"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ alignSelf: "flex-start" }}
|
||||||
|
/>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 800 }}>
|
||||||
|
{event.title || t("untitled")}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("fields.date")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ mt: 0.75, fontWeight: 600 }}>
|
||||||
|
{startDate ? format(startDate, "EEEE, MMMM d, yyyy") : "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("fields.time")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ mt: 0.75, fontWeight: 600 }}>
|
||||||
|
{event.allDay
|
||||||
|
? t("allDay")
|
||||||
|
: startDate
|
||||||
|
? endDate
|
||||||
|
? t("timeRange", {
|
||||||
|
start: format(startDate, "p"),
|
||||||
|
end: format(endDate, "p"),
|
||||||
|
})
|
||||||
|
: format(startDate, "p")
|
||||||
|
: "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{tagLabel}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ mt: 0.75, fontWeight: 600 }}>
|
||||||
|
{tag}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("fields.description")}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color="text.primary"
|
||||||
|
sx={{ mt: 0.75, lineHeight: 1.9 }}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1.5} justifyContent="flex-end">
|
||||||
|
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||||
|
{t("actions.close")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={onEdit}>
|
||||||
|
{t("actions.edit")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
anchor="bottom"
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
ModalProps={{ keepMounted: true }}
|
||||||
|
transitionDuration={modalTransitionDuration}
|
||||||
|
slotProps={{
|
||||||
|
backdrop: {
|
||||||
|
sx: { backgroundColor: "rgba(15, 23, 42, 0.2)" },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
maxHeight: "80vh",
|
||||||
|
borderTopLeftRadius: 24,
|
||||||
|
borderTopRightRadius: 24,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-bs plb-4 pli-6">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<Typography variant="h5">{t("title")}</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("subtitle")}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label={t("close")}
|
||||||
|
>
|
||||||
|
<i className="tabler-x text-2xl" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-auto pli-6 plb-6">{content}</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
fullWidth
|
||||||
|
maxWidth="sm"
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
keepMounted
|
||||||
|
transitionDuration={modalTransitionDuration}
|
||||||
|
slotProps={{
|
||||||
|
backdrop: {
|
||||||
|
sx: { backgroundColor: "rgba(15, 23, 42, 0.2)" },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
"& .MuiDialog-paper": {
|
||||||
|
borderRadius: 5,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-be plb-5 pli-6">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<Typography variant="h5">{t("title")}</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("subtitle")}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<IconButton size="small" onClick={handleClose} aria-label={t("close")}>
|
||||||
|
<i className="tabler-x text-2xl" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<DialogContent>{content}</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(FarmerCalendarEventDetails);
|
||||||
@@ -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<HTMLInputElement, PickerProps>(
|
||||||
|
({ ...props }, ref) => (
|
||||||
|
<CustomTextField
|
||||||
|
inputRef={ref}
|
||||||
|
fullWidth
|
||||||
|
{...props}
|
||||||
|
label={props.label || ""}
|
||||||
|
className="is-full"
|
||||||
|
error={props.error}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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<DefaultStateType>(defaultState);
|
||||||
|
const [tagOptions, setTagOptions] = useState<string[]>([]);
|
||||||
|
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 = (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
autoComplete="off"
|
||||||
|
className="flex flex-col gap-6"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="title"
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<CustomTextField
|
||||||
|
fullWidth
|
||||||
|
label={t("fields.title")}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
{...(errors.title && {
|
||||||
|
error: true,
|
||||||
|
helperText: t("validation.titleRequired"),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AppReactDatepicker
|
||||||
|
id="farmer-calendar-event-start-date"
|
||||||
|
endDate={values.endDate}
|
||||||
|
selected={values.startDate}
|
||||||
|
startDate={values.startDate}
|
||||||
|
showTimeSelect={!values.allDay}
|
||||||
|
dateFormat={!values.allDay ? "yyyy-MM-dd hh:mm" : "yyyy-MM-dd"}
|
||||||
|
customInput={<PickerInput label={t("fields.startDate")} />}
|
||||||
|
onChange={(date) => {
|
||||||
|
const nextDate = asSingleDate(date);
|
||||||
|
|
||||||
|
if (nextDate !== null) {
|
||||||
|
setValues((previous) => ({
|
||||||
|
...previous,
|
||||||
|
startDate: new Date(nextDate),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSelect={handleStartDate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AppReactDatepicker
|
||||||
|
id="farmer-calendar-event-end-date"
|
||||||
|
endDate={values.endDate}
|
||||||
|
selected={values.endDate}
|
||||||
|
minDate={values.startDate}
|
||||||
|
startDate={values.startDate}
|
||||||
|
showTimeSelect={!values.allDay}
|
||||||
|
dateFormat={!values.allDay ? "yyyy-MM-dd hh:mm" : "yyyy-MM-dd"}
|
||||||
|
customInput={<PickerInput label={t("fields.endDate")} />}
|
||||||
|
onChange={(date) => {
|
||||||
|
const nextDate = asSingleDate(date);
|
||||||
|
|
||||||
|
if (nextDate !== null) {
|
||||||
|
setValues((previous) => ({
|
||||||
|
...previous,
|
||||||
|
endDate: new Date(nextDate),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<FormControlLabel
|
||||||
|
label={t("fields.allDay")}
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={values.allDay}
|
||||||
|
onChange={(event) =>
|
||||||
|
setValues((previous) => ({
|
||||||
|
...previous,
|
||||||
|
allDay: event.target.checked,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<CustomTextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
label={tagLabel}
|
||||||
|
id="farmer-calendar-event-tag"
|
||||||
|
value={values.tag}
|
||||||
|
disabled={tagLoading}
|
||||||
|
onChange={(event) =>
|
||||||
|
setValues((previous) => ({
|
||||||
|
...previous,
|
||||||
|
tag: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="">{tagLoading ? "..." : tagPlaceholder}</MenuItem>
|
||||||
|
{tagOptions.map((option) => (
|
||||||
|
<MenuItem key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomTextField>
|
||||||
|
|
||||||
|
<CustomTextField
|
||||||
|
rows={4}
|
||||||
|
multiline
|
||||||
|
fullWidth
|
||||||
|
label={t("fields.description")}
|
||||||
|
id="farmer-calendar-event-description"
|
||||||
|
value={values.description}
|
||||||
|
onChange={(event) =>
|
||||||
|
setValues((previous) => ({
|
||||||
|
...previous,
|
||||||
|
description: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1.5} justifyContent="flex-end">
|
||||||
|
{isEditMode ? (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={handleDeleteButtonClick}
|
||||||
|
>
|
||||||
|
{t("actions.delete")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="contained">
|
||||||
|
{isEditMode ? t("actions.update") : t("actions.create")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
anchor="bottom"
|
||||||
|
variant="temporary"
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
ModalProps={{ keepMounted: true }}
|
||||||
|
transitionDuration={modalTransitionDuration}
|
||||||
|
slotProps={{
|
||||||
|
backdrop: {
|
||||||
|
sx: { backgroundColor: "rgba(15, 23, 42, 0.2)" },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
maxHeight: "85vh",
|
||||||
|
borderTopLeftRadius: 24,
|
||||||
|
borderTopRightRadius: 24,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-bs plb-4 pli-6">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<Typography variant="h5">
|
||||||
|
{isEditMode ? t("titleEdit") : t("titleCreate")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("description")}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label={t("close")}
|
||||||
|
>
|
||||||
|
<i className="tabler-x text-2xl" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-auto pli-6 plb-6">{content}</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
fullWidth
|
||||||
|
maxWidth="sm"
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
keepMounted
|
||||||
|
transitionDuration={modalTransitionDuration}
|
||||||
|
slotProps={{
|
||||||
|
backdrop: {
|
||||||
|
sx: { backgroundColor: "rgba(15, 23, 42, 0.2)" },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
"& .MuiDialog-paper": {
|
||||||
|
borderRadius: 5,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-be plb-5 pli-6">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<Typography variant="h5">
|
||||||
|
{isEditMode ? t("titleEdit") : t("titleCreate")}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("description")}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<IconButton size="small" onClick={handleClose} aria-label={t("close")}>
|
||||||
|
<i className="tabler-x text-2xl" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<DialogContent>{content}</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(FarmerCalendarEventModal);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
|||||||
|
export const FARMER_CALENDAR_TAG_OPTIONS = [
|
||||||
|
"آبیاری",
|
||||||
|
"کوددهی",
|
||||||
|
"سم پاشی",
|
||||||
|
"برداشت",
|
||||||
|
"پایش",
|
||||||
|
"نگهداری",
|
||||||
|
"تیم",
|
||||||
|
"لجستیک",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type FarmerCalendarTag =
|
||||||
|
(typeof FARMER_CALENDAR_TAG_OPTIONS)[number];
|
||||||
Reference in New Issue
Block a user