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": "هنوز مکالمهای ندارید",
|
||||
"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-chartjs-2": "^5.3.1",
|
||||
"react-colorful": "5.6.1",
|
||||
"react-date-object": "1.1.9",
|
||||
"react-date-object": "^2.1.9",
|
||||
"react-datepicker": "7.3.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-dropzone": "14.3.5",
|
||||
@@ -9044,9 +9044,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-date-object": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://mirror-npm.runflare.com/react-date-object/-/react-date-object-1.1.9.tgz",
|
||||
"integrity": "sha512-NEbp/JSqwpF3nm4bXcez9XGwmlpOjSPJSYoRyPC1XOxzRHHaijp6xjMbYpyKB3O4yrluxsbwBHbO1WgeFKip3Q==",
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://mirror-npm.runflare.com/react-date-object/-/react-date-object-2.1.9.tgz",
|
||||
"integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-datepicker": {
|
||||
@@ -9197,12 +9197,6 @@
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-multi-date-picker/node_modules/react-date-object": {
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://mirror-npm.runflare.com/react-date-object/-/react-date-object-2.1.9.tgz",
|
||||
"integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-onclickoutside": {
|
||||
"version": "6.13.1",
|
||||
"license": "MIT",
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@
|
||||
"react-apexcharts": "1.4.1",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"react-colorful": "5.6.1",
|
||||
"react-date-object": "1.1.9",
|
||||
"react-date-object": "^2.1.9",
|
||||
"react-datepicker": "7.3.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-dropzone": "14.3.5",
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTheme } from '@mui/material/styles'
|
||||
import type { BoxProps } from '@mui/material/Box'
|
||||
|
||||
// Third-party Imports
|
||||
import { Calendar } from 'react-multi-date-picker'
|
||||
import DatePicker from 'react-multi-date-picker'
|
||||
import DateObject from 'react-date-object'
|
||||
import persian from 'react-date-object/calendars/persian'
|
||||
import persian_fa from 'react-date-object/locales/persian_fa'
|
||||
@@ -20,11 +20,12 @@ import 'react-multi-date-picker/styles/colors/teal.css'
|
||||
interface AppJalaliDatepickerProps {
|
||||
value?: Date | null
|
||||
onChange?: (date: Date) => void
|
||||
placeholder?: string
|
||||
boxProps?: BoxProps
|
||||
}
|
||||
|
||||
const AppJalaliDatepicker = (props: AppJalaliDatepickerProps) => {
|
||||
const { value: externalValue, onChange, boxProps } = props
|
||||
const { value: externalValue, onChange, placeholder, boxProps } = props
|
||||
const theme = useTheme()
|
||||
|
||||
const [internalValue, setInternalValue] = useState<DateObject | undefined>(() => {
|
||||
@@ -51,46 +52,18 @@ const AppJalaliDatepicker = (props: AppJalaliDatepickerProps) => {
|
||||
return (
|
||||
<Box
|
||||
{...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}
|
||||
onChange={handleChange as any}
|
||||
calendar={persian}
|
||||
locale={persian_fa}
|
||||
className="teal"
|
||||
format='YYYY/MM/DD'
|
||||
calendarPosition='bottom-right'
|
||||
inputClass='rmdp-input'
|
||||
placeholder={placeholder || 'انتخاب تاریخ'}
|
||||
className='teal'
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -1,526 +1,264 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
// React Imports
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { ComponentProps, ReactElement } from "react";
|
||||
import { cloneElement, isValidElement } from "react";
|
||||
|
||||
// MUI imports
|
||||
import Box from '@mui/material/Box'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import type { BoxProps } from '@mui/material/Box'
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { BoxProps } from "@mui/material/Box";
|
||||
|
||||
// Third-party Imports
|
||||
import ReactDatePickerComponent from 'react-datepicker'
|
||||
import DatePicker, { Calendar } from "react-multi-date-picker";
|
||||
import type { DateObject } from "react-multi-date-picker";
|
||||
import type { Value } from "react-multi-date-picker";
|
||||
import TimePicker from "react-multi-date-picker/plugins/time_picker";
|
||||
import persian from "react-date-object/calendars/persian";
|
||||
import persian_fa from "react-date-object/locales/persian_fa";
|
||||
|
||||
// Styles
|
||||
import 'react-datepicker/dist/react-datepicker.css'
|
||||
import "react-multi-date-picker/styles/colors/teal.css";
|
||||
|
||||
type Props = ComponentProps<typeof ReactDatePickerComponent> & {
|
||||
boxProps?: BoxProps
|
||||
}
|
||||
type Props = Omit<
|
||||
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 StyledReactDatePicker = styled(Box)<BoxProps>(({ theme }) => {
|
||||
return {
|
||||
'& .react-datepicker-popper': {
|
||||
zIndex: 20,
|
||||
paddingTop: `${theme.spacing(0.5)} !important`
|
||||
},
|
||||
'& .react-datepicker-wrapper': {
|
||||
width: '100%'
|
||||
},
|
||||
'& .react-datepicker__triangle': {
|
||||
display: 'none'
|
||||
},
|
||||
'& .react-datepicker': {
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
borderRadius: 'var(--mui-shape-borderRadius)',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||
boxShadow: 'var(--mui-customShadows-md)',
|
||||
border: 'none',
|
||||
'& .react-datepicker__header': {
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
fontWeight: 'normal',
|
||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||
'& .react-datepicker__current-month, &.react-datepicker-year-header': {
|
||||
textAlign: 'left'
|
||||
},
|
||||
'&:not(.react-datepicker-year-header)': {
|
||||
'& + .react-datepicker__month, & + .react-datepicker__year': {
|
||||
margin: theme.spacing(2),
|
||||
marginTop: theme.spacing(4.5)
|
||||
}
|
||||
},
|
||||
'&.react-datepicker-year-header': {
|
||||
'& + .react-datepicker__month, & + .react-datepicker__year': {
|
||||
margin: theme.spacing(2),
|
||||
marginTop: theme.spacing(0)
|
||||
}
|
||||
}
|
||||
},
|
||||
'& > .react-datepicker__navigation': {
|
||||
top: 13,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'var(--mui-palette-action-selected)',
|
||||
'&.react-datepicker__navigation--previous': {
|
||||
width: 30,
|
||||
height: 30,
|
||||
border: 'none',
|
||||
top: 12,
|
||||
left: 'auto',
|
||||
right: '57px',
|
||||
...(theme.direction === 'ltr'
|
||||
? {
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")",
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")"
|
||||
})
|
||||
}
|
||||
: {
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")",
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")"
|
||||
})
|
||||
}),
|
||||
'& .react-datepicker__navigation-icon': {
|
||||
display: 'none'
|
||||
},
|
||||
'&:has(+ .react-datepicker__navigation--next--with-time)':
|
||||
theme.direction === 'ltr' ? { right: 177 } : { left: 177 }
|
||||
},
|
||||
'&.react-datepicker__navigation--next': {
|
||||
width: 30,
|
||||
height: 30,
|
||||
border: 'none',
|
||||
top: 12,
|
||||
right: 15,
|
||||
left: 'auto',
|
||||
...(theme.direction === 'ltr'
|
||||
? {
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")",
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M12.5 10L17.5 15L12.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")"
|
||||
})
|
||||
}
|
||||
: {
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(47 43 61 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")",
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgb(225 222 245 / 0.7)' d='M17.5 10L12.5 15L17.5 20' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E\")"
|
||||
})
|
||||
}),
|
||||
'& .react-datepicker__navigation-icon': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
'&.react-datepicker__navigation--next--with-time': theme.direction === 'ltr' ? { right: 135 } : { left: 135 },
|
||||
'&:focus, &:active': {
|
||||
outline: 0
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__current-month, & .react-datepicker-year-header': {
|
||||
...theme.typography.subtitle1,
|
||||
lineHeight: 2,
|
||||
paddingBlockStart: theme.spacing(3),
|
||||
paddingBlockEnd: theme.spacing(4.5),
|
||||
paddingInline: theme.spacing(4),
|
||||
color: 'var(--mui-palette-text-primary)'
|
||||
},
|
||||
'& .react-datepicker__day-name': {
|
||||
margin: 0,
|
||||
width: '2.25rem',
|
||||
...theme.typography.subtitle2,
|
||||
color: 'var(--mui-palette-text-primary)'
|
||||
},
|
||||
'& .react-datepicker__day-names': {
|
||||
marginBottom: 0
|
||||
},
|
||||
'& .react-datepicker__day': {
|
||||
margin: 0,
|
||||
width: '2.25rem',
|
||||
borderRadius: '50%',
|
||||
lineHeight: '2.25rem',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
'&.react-datepicker__day--selected.react-datepicker__day--in-selecting-range.react-datepicker__day--selecting-range-start, &.react-datepicker__day--selected.react-datepicker__day--range-start.react-datepicker__day--in-range, &.react-datepicker__day--range-start':
|
||||
{
|
||||
borderRadius: '18px 0px 0px 18px;',
|
||||
color: 'var(--mui-palette-common-white) !important',
|
||||
backgroundColor: 'var(--mui-palette-primary-main) !important'
|
||||
},
|
||||
'&.react-datepicker__day--range-end.react-datepicker__day--in-range': {
|
||||
borderRadius: '0px 18px 18px 0px',
|
||||
color: 'var(--mui-palette-common-white) !important',
|
||||
backgroundColor: 'var(--mui-palette-primary-main) !important'
|
||||
},
|
||||
'&:focus, &:active': {
|
||||
outline: 0
|
||||
},
|
||||
'&.react-datepicker__day--outside-month, &.react-datepicker__day--disabled:not(.react-datepicker__day--selected)':
|
||||
{
|
||||
color: 'var(--mui-palette-text-disabled)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
},
|
||||
'&.react-datepicker__day--highlighted, &.react-datepicker__day--highlighted:hover': {
|
||||
color: 'var(--mui-palette-success-main)',
|
||||
backgroundColor: 'var(--mui-palette-success-lightOpacity)',
|
||||
'&.react-datepicker__day--selected': {
|
||||
backgroundColor: 'var(--mui-palette-primary-main) !important'
|
||||
}
|
||||
}
|
||||
},
|
||||
'&:has(.react-datepicker__day--in-range)': {
|
||||
'& > .react-datepicker__navigation': {
|
||||
'&.react-datepicker__navigation--previous': {
|
||||
...(theme.direction === 'ltr' ? { left: 15 } : { right: 15 })
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__header': {
|
||||
'& .react-datepicker__current-month': {
|
||||
textAlign: 'center'
|
||||
}
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__day--in-range, & .react-datepicker__day--in-selecting-range': {
|
||||
borderRadius: 0,
|
||||
color: 'var(--mui-palette-primary-main) !important',
|
||||
backgroundColor: 'var(--mui-palette-primary-lightOpacity) !important'
|
||||
},
|
||||
'& .react-datepicker__day--today': {
|
||||
fontWeight: 'normal',
|
||||
'&:not(.react-datepicker__day--selected):not(:empty)': {
|
||||
color: 'var(--mui-palette-primary-main)',
|
||||
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--mui-palette-primary-mainOpacity)'
|
||||
},
|
||||
'&.react-datepicker__day--keyboard-selected': {
|
||||
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--mui-palette-primary-lightOpacity)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__month-text--today': {
|
||||
fontWeight: 'normal',
|
||||
'&:not(.react-datepicker__month-text--selected)': {
|
||||
lineHeight: '2.125rem',
|
||||
color: 'var(--mui-palette-primary-main)',
|
||||
border: '1px solid var(--mui-palette-primary-main)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.04)'
|
||||
}
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__year-text--today': {
|
||||
fontWeight: 'normal',
|
||||
'&:not(.react-datepicker__year-text--selected)': {
|
||||
lineHeight: '2.125rem',
|
||||
color: 'var(--mui-palette-primary-main)',
|
||||
border: '1px solid var(--mui-palette-primary-main)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.04)'
|
||||
},
|
||||
'&.react-datepicker__year-text--keyboard-selected': {
|
||||
color: 'var(--mui-palette-primary-main)',
|
||||
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.06)',
|
||||
'&:hover': {
|
||||
color: 'var(--mui-palette-primary-main)',
|
||||
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.06)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__day--keyboard-selected': {
|
||||
'&:not(.react-datepicker__day--in-range)': {
|
||||
color: 'var(--mui-palette-primary-main)',
|
||||
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)'
|
||||
}
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__month-text--keyboard-selected': {
|
||||
'&:not(.react-datepicker__month--in-range)': {
|
||||
color: 'var(--mui-palette-primary-main)',
|
||||
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)'
|
||||
}
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__year-text--keyboard-selected, & .react-datepicker__quarter-text--keyboard-selected': {
|
||||
color: 'var(--mui-palette-primary-main)',
|
||||
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)'
|
||||
},
|
||||
'& .react-datepicker__day--selected, & .react-datepicker__month-text--selected, & .react-datepicker__year-text--selected, & .react-datepicker__quarter-text--selected':
|
||||
{
|
||||
color: 'var(--mui-palette-common-white) !important',
|
||||
backgroundColor: 'var(--mui-palette-primary-main) !important',
|
||||
boxShadow: 'var(--mui-customShadows-primary-sm)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--mui-palette-primary-dark) !important'
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__header__dropdown': {
|
||||
'& .react-datepicker__month-dropdown-container:not(:last-child)': {
|
||||
marginRight: theme.spacing(8)
|
||||
},
|
||||
'& .react-datepicker__month-dropdown-container, & .react-datepicker__year-dropdown-container': {
|
||||
marginBottom: theme.spacing(4)
|
||||
},
|
||||
'& .react-datepicker__month-read-view--selected-month, & .react-datepicker__year-read-view--selected-year': {
|
||||
fontSize: '0.875rem',
|
||||
marginRight: theme.spacing(1),
|
||||
color: 'var(--mui-palette-text-primary)'
|
||||
},
|
||||
'& .react-datepicker__month-read-view:hover .react-datepicker__month-read-view--down-arrow, & .react-datepicker__year-read-view:hover .react-datepicker__year-read-view--down-arrow':
|
||||
{
|
||||
borderColor: 'var(--mui-palette-text-primary)'
|
||||
},
|
||||
'& .react-datepicker__month-read-view--down-arrow, & .react-datepicker__year-read-view--down-arrow': {
|
||||
top: 4,
|
||||
borderColor: 'var(--mui-palette-text-secondary)'
|
||||
},
|
||||
'& .react-datepicker__month-dropdown, & .react-datepicker__year-dropdown': {
|
||||
padding: theme.spacing(2),
|
||||
border: 'none',
|
||||
borderRadius: 'var(--mui-shape-borderRadius)',
|
||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||
boxShadow: 'var(--mui-customShadows-lg)',
|
||||
'[data-skin="bordered"] &': {
|
||||
boxShadow: 'none',
|
||||
border: `1px solid var(--mui-palette-divider)`
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__month-option, & .react-datepicker__year-option': {
|
||||
...theme.typography.body1,
|
||||
padding: theme.spacing(1.5, 4),
|
||||
borderRadius: 'var(--mui-shape-borderRadius)',
|
||||
marginBlockEnd: theme.spacing(0.5),
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--mui-palette-action-hover)'
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__month-option.react-datepicker__month-option--selected_month': {
|
||||
color: 'var(--mui-palette-primary-main)',
|
||||
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--mui-palette-primary-lightOpacity)'
|
||||
},
|
||||
'& .react-datepicker__month-option--selected': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__year-option.react-datepicker__year-option--selected_year': {
|
||||
color: 'var(--mui-palette-primary-main)',
|
||||
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--mui-palette-primary-lightOpacity)'
|
||||
},
|
||||
'& .react-datepicker__year-option--selected': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__year-option': {
|
||||
// TODO: Remove some of the following styles for arrow in Year dropdown when react-datepicker give arrows in Year dropdown
|
||||
'& .react-datepicker__navigation--years-upcoming': {
|
||||
width: 9,
|
||||
height: 9,
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '3px 3px 0 0',
|
||||
transform: 'rotate(-45deg)',
|
||||
borderTopColor: 'var(--mui-palette-text-secondary)',
|
||||
borderRightColor: 'var(--mui-palette-text-secondary)',
|
||||
margin: `${theme.spacing(2.75)} auto ${theme.spacing(0)}`
|
||||
},
|
||||
'&:hover .react-datepicker__navigation--years-upcoming': {
|
||||
borderTopColor: 'var(--mui-palette-text-primary)',
|
||||
borderRightColor: 'var(--mui-palette-text-primary)'
|
||||
},
|
||||
'& .react-datepicker__navigation--years-previous': {
|
||||
width: 9,
|
||||
height: 9,
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '0 0 3px 3px',
|
||||
transform: 'rotate(-45deg)',
|
||||
borderLeftColor: 'var(--mui-palette-text-secondary)',
|
||||
borderBottomColor: 'var(--mui-palette-text-secondary)',
|
||||
margin: `${theme.spacing(0)} auto ${theme.spacing(2.75)}`
|
||||
},
|
||||
'&:hover .react-datepicker__navigation--years-previous': {
|
||||
borderLeftColor: 'var(--mui-palette-text-primary)',
|
||||
borderBottomColor: 'var(--mui-palette-text-primary)'
|
||||
}
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__week-number': {
|
||||
margin: 0,
|
||||
fontWeight: 500,
|
||||
width: '2.25rem',
|
||||
lineHeight: '2.25rem',
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
color: 'var(--mui-palette-text-primary)'
|
||||
},
|
||||
'& .react-datepicker__month-text, & .react-datepicker__year-text, & .react-datepicker__quarter-text': {
|
||||
margin: 0,
|
||||
alignItems: 'center',
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
lineHeight: '2rem',
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 'var(--mui-shape-borderRadius)',
|
||||
'&:focus, &:active': {
|
||||
outline: 0
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__year-wrapper': {
|
||||
maxWidth: 205,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
'& .react-datepicker__input-time-container': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
...(theme.direction === 'rtl' ? { flexDirection: 'row-reverse' } : {})
|
||||
},
|
||||
'& .react-datepicker__today-button': {
|
||||
borderTop: 0,
|
||||
borderRadius: '1rem',
|
||||
margin: theme.spacing(0, 4, 4),
|
||||
color: 'var(--mui-palette-common-white)',
|
||||
backgroundColor: 'var(--mui-palette-primary-main)'
|
||||
},
|
||||
const convertFormat = (format?: string) => {
|
||||
if (!format) return "YYYY/MM/DD";
|
||||
|
||||
// Time Picker
|
||||
'&:not(.react-datepicker--time-only)': {
|
||||
'& .react-datepicker__time-container': {
|
||||
borderLeftColor: 'var(--mui-palette-divider)',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
width: '5.5rem'
|
||||
},
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: '7.4375rem'
|
||||
}
|
||||
}
|
||||
},
|
||||
'&.react-datepicker--time-only': {
|
||||
width: '7.4375rem',
|
||||
'& .react-datepicker__time-container': {
|
||||
width: '7.4375rem'
|
||||
}
|
||||
},
|
||||
'& .react-datepicker__time-container': {
|
||||
padding: theme.spacing(0.75, 0),
|
||||
'& .react-datepicker-time__header': {
|
||||
...theme.typography.subtitle2,
|
||||
marginBottom: theme.spacing(3.5),
|
||||
marginTop: theme.spacing(3.5),
|
||||
color: 'var(--mui-palette-text-primary)'
|
||||
},
|
||||
return format
|
||||
.replace(/yyyy/g, "YYYY")
|
||||
.replace(/yy/g, "YY")
|
||||
.replace(/dd/g, "DD")
|
||||
.replace(/d/g, "D")
|
||||
.replace(/HH/g, "HH")
|
||||
.replace(/hh/g, "HH")
|
||||
.replace(/mm/g, "mm");
|
||||
};
|
||||
|
||||
'& .react-datepicker__time': {
|
||||
background: 'var(--mui-palette-background-paper)',
|
||||
'& .react-datepicker__time-box .react-datepicker__time-list-item--disabled': {
|
||||
pointerEvents: 'none',
|
||||
color: 'var(--mui-palette-text-disabled)',
|
||||
'&.react-datepicker__time-list-item--selected': {
|
||||
fontWeight: 'normal',
|
||||
backgroundColor: 'var(--mui-palette-action-disabledBackground)'
|
||||
}
|
||||
}
|
||||
},
|
||||
const toDate = (value: Value | null | undefined): Date | null => {
|
||||
if (!value) return null;
|
||||
if (value instanceof Date) return value;
|
||||
if (typeof value === "string" || typeof value === "number") {
|
||||
const parsed = new Date(value);
|
||||
|
||||
'& .react-datepicker__time-list-item': {
|
||||
height: 'auto !important',
|
||||
padding: `${theme.spacing(1.75, 0)} !important`,
|
||||
marginLeft: theme.spacing(4.25),
|
||||
marginRight: theme.spacing(2.2),
|
||||
...theme.typography.body1,
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
borderRadius: 'var(--mui-shape-borderRadius)',
|
||||
'&:focus, &:active': {
|
||||
outline: 0
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--mui-palette-action-hover) !important'
|
||||
},
|
||||
'&.react-datepicker__time-list-item--selected:not(.react-datepicker__time-list-item--disabled)': {
|
||||
fontWeight: 'normal',
|
||||
color: 'var(--mui-palette-common-white) !important',
|
||||
backgroundColor: 'var(--mui-palette-primary-main) !important',
|
||||
boxShadow: 'var(--mui-customShadows-xs)'
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
},
|
||||
if (Array.isArray(value)) return null;
|
||||
if (typeof (value as DateObject).toDate === "function")
|
||||
return (value as DateObject).toDate();
|
||||
|
||||
'& .react-datepicker__time-box': {
|
||||
width: '100%'
|
||||
},
|
||||
'& .react-datepicker__time-list': {
|
||||
'&::-webkit-scrollbar': {
|
||||
width: 8
|
||||
},
|
||||
return null;
|
||||
};
|
||||
|
||||
/* Track */
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: 'var(--mui-palette-background-paper)'
|
||||
const baseSx: BoxProps["sx"] = {
|
||||
"& .rmdp-container": {
|
||||
width: "100%",
|
||||
},
|
||||
|
||||
/* Handle */
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
borderRadius: 10,
|
||||
background: '#aaa'
|
||||
"& .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)",
|
||||
},
|
||||
|
||||
/* Handle on hover */
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
background: '#999'
|
||||
}
|
||||
}
|
||||
"& .rmdp-shadow": {
|
||||
boxShadow: "var(--mui-customShadows-md)",
|
||||
},
|
||||
'& .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)'
|
||||
"& .rmdp-top-class": {
|
||||
width: "100%",
|
||||
},
|
||||
'[data-skin="bordered"] &': {
|
||||
boxShadow: 'none',
|
||||
border: `1px solid var(--mui-palette-divider)`
|
||||
}
|
||||
"& .rmdp-header-values": {
|
||||
color: "var(--mui-palette-text-primary)",
|
||||
fontWeight: 700,
|
||||
},
|
||||
'& .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
|
||||
"& .rmdp-arrow-container:hover": {
|
||||
backgroundColor: "var(--mui-palette-primary-lightOpacity)",
|
||||
},
|
||||
"& .rmdp-arrow": {
|
||||
borderColor: "var(--mui-palette-text-secondary)",
|
||||
},
|
||||
"& .rmdp-week-day": {
|
||||
color: "var(--mui-palette-text-secondary)",
|
||||
fontWeight: 700,
|
||||
},
|
||||
"& .rmdp-day, & .rmdp-time-picker div input": {
|
||||
color: "var(--mui-palette-text-primary)",
|
||||
},
|
||||
"& .rmdp-day.rmdp-today span": {
|
||||
backgroundColor: "var(--mui-palette-primary-lightOpacity)",
|
||||
color: "var(--mui-palette-primary-main)",
|
||||
},
|
||||
"& .rmdp-day.rmdp-selected span:not(.highlight)": {
|
||||
backgroundColor: "var(--mui-palette-primary-main)",
|
||||
color: "var(--mui-palette-common-white)",
|
||||
},
|
||||
"& .rmdp-range, & .rmdp-range.start span, & .rmdp-range.end span": {
|
||||
backgroundColor: "var(--mui-palette-primary-main)",
|
||||
color: "var(--mui-palette-common-white)",
|
||||
},
|
||||
"& .rmdp-day.rmdp-disabled, & .rmdp-day.rmdp-deactive": {
|
||||
color: "var(--mui-palette-text-disabled)",
|
||||
},
|
||||
"& .rmdp-input": {
|
||||
width: "100%",
|
||||
minHeight: 42,
|
||||
padding: "10px 12px",
|
||||
borderRadius: "var(--mui-shape-borderRadius)",
|
||||
border: "1px solid var(--mui-palette-divider)",
|
||||
backgroundColor: "var(--mui-palette-background-paper)",
|
||||
color: "var(--mui-palette-text-primary)",
|
||||
font: "inherit",
|
||||
},
|
||||
"& .rmdp-input:focus": {
|
||||
borderColor: "var(--mui-palette-primary-main)",
|
||||
boxShadow: "0 0 0 2px var(--mui-palette-primary-lightOpacity)",
|
||||
outline: "none",
|
||||
},
|
||||
"& .rmdp-time-picker": {
|
||||
padding: "8px 12px",
|
||||
},
|
||||
"& .rmdp-time-picker div input": {
|
||||
border: "1px solid var(--mui-palette-divider)",
|
||||
borderRadius: 8,
|
||||
backgroundColor: "var(--mui-palette-background-paper)",
|
||||
},
|
||||
};
|
||||
|
||||
const AppReactDatepicker = (props: Props) => {
|
||||
// Props
|
||||
const { boxProps, ...rest } = props
|
||||
const {
|
||||
boxProps,
|
||||
selected,
|
||||
startDate,
|
||||
endDate,
|
||||
selectsRange,
|
||||
showTimeSelect,
|
||||
customInput,
|
||||
placeholderText,
|
||||
onChange,
|
||||
onSelect,
|
||||
inline,
|
||||
dateFormat,
|
||||
minDate,
|
||||
maxDate,
|
||||
shouldCloseOnSelect,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<StyledReactDatePicker {...boxProps}>
|
||||
<ReactDatePickerComponent popperPlacement='bottom-start' {...rest} />
|
||||
</StyledReactDatePicker>
|
||||
)
|
||||
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;
|
||||
}
|
||||
|
||||
export default AppReactDatepicker
|
||||
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 (
|
||||
<Box {...boxProps} sx={[baseSx, boxProps?.sx]}>
|
||||
{inline ? (
|
||||
<Calendar {...sharedProps} />
|
||||
) : (
|
||||
<DatePicker
|
||||
{...sharedProps}
|
||||
render={renderInput}
|
||||
inputClass="rmdp-input"
|
||||
placeholder={placeholderText}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppReactDatepicker;
|
||||
|
||||
@@ -1,74 +1,87 @@
|
||||
// React Imports
|
||||
import { useState, useEffect, forwardRef, useCallback } from 'react'
|
||||
import { useState, useEffect, forwardRef, useCallback } from "react";
|
||||
|
||||
// MUI Imports
|
||||
import Box from '@mui/material/Box'
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import Switch from '@mui/material/Switch'
|
||||
import Button from '@mui/material/Button'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import FormControl from '@mui/material/FormControl'
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import type { SelectChangeEvent } from '@mui/material/Select'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
import Box from "@mui/material/Box";
|
||||
import Drawer from "@mui/material/Drawer";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import Button from "@mui/material/Button";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import type { Theme } from "@mui/material/styles";
|
||||
|
||||
// Third-party Imports
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import PerfectScrollbar from "react-perfect-scrollbar";
|
||||
|
||||
// Type Imports
|
||||
import type { AddEventSidebarType, AddEventType } from '@/types/apps/calendarTypes'
|
||||
import type {
|
||||
AddEventSidebarType,
|
||||
AddEventType,
|
||||
} from "@/types/apps/calendarTypes";
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
import CustomTextField from "@core/components/mui/TextField";
|
||||
|
||||
// Styled Component Imports
|
||||
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
|
||||
import AppReactDatepicker from "@/libs/styles/AppReactDatepicker";
|
||||
|
||||
// Slice Imports
|
||||
import { createEventAsync, updateEventAsync, deleteEventAsync, selectedEvent, filterEvents } from '@/redux-store/slices/calendar'
|
||||
import {
|
||||
addEvent,
|
||||
deleteEvent,
|
||||
updateEvent,
|
||||
selectedEvent,
|
||||
filterEvents,
|
||||
} from "@/redux-store/slices/calendar";
|
||||
|
||||
interface PickerProps {
|
||||
label?: string
|
||||
error?: boolean
|
||||
registername?: string
|
||||
label?: string;
|
||||
error?: boolean;
|
||||
registername?: string;
|
||||
}
|
||||
|
||||
interface DefaultStateType {
|
||||
url: string
|
||||
title: string
|
||||
allDay: boolean
|
||||
calendar: string
|
||||
description: string
|
||||
endDate: Date
|
||||
startDate: Date
|
||||
guests: string[] | undefined
|
||||
url: string;
|
||||
title: string;
|
||||
allDay: boolean;
|
||||
calendar: string;
|
||||
description: string;
|
||||
endDate: Date;
|
||||
startDate: Date;
|
||||
guests: string[] | undefined;
|
||||
}
|
||||
|
||||
// Vars
|
||||
const capitalize = (string: string) => string && string[0].toUpperCase() + string.slice(1)
|
||||
const capitalize = (string: string) =>
|
||||
string && string[0].toUpperCase() + string.slice(1);
|
||||
|
||||
// Vars
|
||||
const defaultState: DefaultStateType = {
|
||||
url: '',
|
||||
title: '',
|
||||
url: "",
|
||||
title: "",
|
||||
guests: [],
|
||||
allDay: true,
|
||||
description: '',
|
||||
description: "",
|
||||
endDate: new Date(),
|
||||
calendar: 'Business',
|
||||
startDate: new Date()
|
||||
}
|
||||
calendar: "Business",
|
||||
startDate: new Date(),
|
||||
};
|
||||
|
||||
const AddEventSidebar = (props: AddEventSidebarType) => {
|
||||
// Props
|
||||
const { calendarStore, dispatch, addEventSidebarOpen, handleAddEventSidebarToggle } = props
|
||||
const {
|
||||
calendarStore,
|
||||
dispatch,
|
||||
addEventSidebarOpen,
|
||||
handleAddEventSidebarToggle,
|
||||
} = props;
|
||||
|
||||
// States
|
||||
const [values, setValues] = useState<DefaultStateType>(defaultState)
|
||||
const [values, setValues] = useState<DefaultStateType>(defaultState);
|
||||
|
||||
// Refs
|
||||
const PickersComponent = forwardRef(({ ...props }: PickerProps, ref) => {
|
||||
@@ -77,109 +90,108 @@ const AddEventSidebar = (props: AddEventSidebarType) => {
|
||||
inputRef={ref}
|
||||
fullWidth
|
||||
{...props}
|
||||
label={props.label || ''}
|
||||
className='is-full'
|
||||
label={props.label || ""}
|
||||
className="is-full"
|
||||
error={props.error}
|
||||
/>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Hooks
|
||||
const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
|
||||
const isBelowSmScreen = useMediaQuery((theme: Theme) =>
|
||||
theme.breakpoints.down("sm"),
|
||||
);
|
||||
|
||||
const {
|
||||
control,
|
||||
setValue,
|
||||
clearErrors,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm({ defaultValues: { title: '' } })
|
||||
formState: { errors },
|
||||
} = useForm({ defaultValues: { title: "" } });
|
||||
|
||||
const resetToStoredValues = useCallback(() => {
|
||||
if (calendarStore.selectedEvent !== null) {
|
||||
const event = calendarStore.selectedEvent
|
||||
const event = calendarStore.selectedEvent;
|
||||
|
||||
setValue('title', event.title || '')
|
||||
setValue("title", event.title || "");
|
||||
setValues({
|
||||
url: event.url || '',
|
||||
title: event.title || '',
|
||||
url: event.url || "",
|
||||
title: event.title || "",
|
||||
allDay: event.allDay,
|
||||
guests: event.extendedProps.guests || [],
|
||||
description: event.extendedProps.description || '',
|
||||
calendar: event.extendedProps.calendar || 'Business',
|
||||
description: event.extendedProps.description || "",
|
||||
calendar: event.extendedProps.calendar || "Business",
|
||||
endDate: event.end !== null ? event.end : event.start,
|
||||
startDate: event.start !== null ? event.start : new Date()
|
||||
})
|
||||
startDate: event.start !== null ? event.start : new Date(),
|
||||
});
|
||||
}
|
||||
}, [setValue, calendarStore.selectedEvent])
|
||||
}, [setValue, calendarStore.selectedEvent]);
|
||||
|
||||
const resetToEmptyValues = useCallback(() => {
|
||||
setValue('title', '')
|
||||
setValues(defaultState)
|
||||
}, [setValue])
|
||||
setValue("title", "");
|
||||
setValues(defaultState);
|
||||
}, [setValue]);
|
||||
|
||||
const handleSidebarClose = () => {
|
||||
setValues(defaultState)
|
||||
clearErrors()
|
||||
dispatch(selectedEvent(null))
|
||||
handleAddEventSidebarToggle()
|
||||
}
|
||||
setValues(defaultState);
|
||||
clearErrors();
|
||||
dispatch(selectedEvent(null));
|
||||
handleAddEventSidebarToggle();
|
||||
};
|
||||
|
||||
const onSubmit = (data: { title: string }) => {
|
||||
const modifiedEvent: AddEventType = {
|
||||
url: values.url,
|
||||
display: 'block',
|
||||
display: "block",
|
||||
title: data.title,
|
||||
end: values.endDate,
|
||||
allDay: values.allDay,
|
||||
start: values.startDate,
|
||||
extendedProps: {
|
||||
calendar: capitalize(values.calendar),
|
||||
guests: values.guests && values.guests.length ? values.guests : undefined,
|
||||
description: values.description.length ? values.description : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const eventData = {
|
||||
title: modifiedEvent.title || '',
|
||||
description: modifiedEvent.extendedProps?.description || '',
|
||||
calendar: (modifiedEvent.extendedProps?.calendar || 'Business') as 'Personal' | 'Business' | 'Family' | 'Holiday' | 'ETC',
|
||||
start: modifiedEvent.start ? new Date(modifiedEvent.start).toISOString() : new Date().toISOString(),
|
||||
end: modifiedEvent.end ? new Date(modifiedEvent.end).toISOString() : new Date().toISOString(),
|
||||
allDay: modifiedEvent.allDay || false,
|
||||
tags: modifiedEvent.extendedProps?.tags || [],
|
||||
deadline: modifiedEvent.extendedProps?.deadline || Math.floor(Date.now() / 1000),
|
||||
extendedProps: modifiedEvent.extendedProps || {}
|
||||
}
|
||||
guests:
|
||||
values.guests && values.guests.length ? values.guests : undefined,
|
||||
description: values.description.length ? values.description : undefined,
|
||||
},
|
||||
};
|
||||
|
||||
if (
|
||||
calendarStore.selectedEvent === null ||
|
||||
(calendarStore.selectedEvent !== null && !calendarStore.selectedEvent.title.length)
|
||||
(calendarStore.selectedEvent !== null &&
|
||||
!calendarStore.selectedEvent.title.length)
|
||||
) {
|
||||
dispatch(createEventAsync(eventData))
|
||||
dispatch(addEvent(modifiedEvent));
|
||||
} else {
|
||||
dispatch(updateEventAsync({ id: calendarStore.selectedEvent.id, data: eventData }))
|
||||
dispatch(
|
||||
updateEvent({ ...modifiedEvent, id: calendarStore.selectedEvent.id }),
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(filterEvents())
|
||||
dispatch(filterEvents());
|
||||
|
||||
handleSidebarClose()
|
||||
}
|
||||
handleSidebarClose();
|
||||
};
|
||||
|
||||
const handleDeleteButtonClick = () => {
|
||||
if (calendarStore.selectedEvent) {
|
||||
dispatch(deleteEventAsync(calendarStore.selectedEvent.id))
|
||||
dispatch(filterEvents())
|
||||
dispatch(deleteEvent(calendarStore.selectedEvent.id));
|
||||
dispatch(filterEvents());
|
||||
}
|
||||
|
||||
handleSidebarClose()
|
||||
}
|
||||
// calendarApi.getEventById(calendarStore.selectedEvent.id).remove()
|
||||
handleSidebarClose();
|
||||
};
|
||||
|
||||
const handleStartDate = (date: Date | null) => {
|
||||
if (date && date > values.endDate) {
|
||||
setValues({ ...values, startDate: new Date(date), endDate: new Date(date) })
|
||||
}
|
||||
setValues({
|
||||
...values,
|
||||
startDate: new Date(date),
|
||||
endDate: new Date(date),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const RenderSidebarFooter = () => {
|
||||
if (
|
||||
@@ -187,184 +199,180 @@ const AddEventSidebar = (props: AddEventSidebarType) => {
|
||||
(calendarStore.selectedEvent && !calendarStore.selectedEvent.title.length)
|
||||
) {
|
||||
return (
|
||||
<div className='flex gap-4'>
|
||||
<Button type='submit' variant='contained'>
|
||||
<div className="flex gap-4">
|
||||
<Button type="submit" variant="contained">
|
||||
Add
|
||||
</Button>
|
||||
<Button variant='outlined' color='secondary' onClick={resetToEmptyValues}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={resetToEmptyValues}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className='flex gap-4'>
|
||||
<Button type='submit' variant='contained'>
|
||||
<div className="flex gap-4">
|
||||
<Button type="submit" variant="contained">
|
||||
Update
|
||||
</Button>
|
||||
<Button variant='outlined' color='secondary' onClick={resetToStoredValues}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={resetToStoredValues}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const ScrollWrapper = isBelowSmScreen ? 'div' : PerfectScrollbar
|
||||
const ScrollWrapper = isBelowSmScreen ? "div" : PerfectScrollbar;
|
||||
|
||||
useEffect(() => {
|
||||
if (calendarStore.selectedEvent !== null) {
|
||||
resetToStoredValues()
|
||||
resetToStoredValues();
|
||||
} else {
|
||||
resetToEmptyValues()
|
||||
resetToEmptyValues();
|
||||
}
|
||||
}, [addEventSidebarOpen, resetToStoredValues, resetToEmptyValues, calendarStore.selectedEvent])
|
||||
}, [
|
||||
addEventSidebarOpen,
|
||||
resetToStoredValues,
|
||||
resetToEmptyValues,
|
||||
calendarStore.selectedEvent,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
anchor='right'
|
||||
anchor="right"
|
||||
open={addEventSidebarOpen}
|
||||
onClose={handleSidebarClose}
|
||||
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'>
|
||||
<Typography variant='h5'>
|
||||
{calendarStore.selectedEvent && calendarStore.selectedEvent.title.length ? 'Update Event' : 'Add Event'}
|
||||
<Box className="flex justify-between items-center sidebar-header plb-5 pli-6 border-be">
|
||||
<Typography variant="h5">
|
||||
{calendarStore.selectedEvent &&
|
||||
calendarStore.selectedEvent.title.length
|
||||
? "Update Event"
|
||||
: "Add Event"}
|
||||
</Typography>
|
||||
{calendarStore.selectedEvent && calendarStore.selectedEvent.title.length ? (
|
||||
<Box className='flex items-center' sx={{ gap: calendarStore.selectedEvent !== null ? 1 : 0 }}>
|
||||
<IconButton size='small' onClick={handleDeleteButtonClick}>
|
||||
<i className='tabler-trash text-2xl text-textPrimary' />
|
||||
{calendarStore.selectedEvent &&
|
||||
calendarStore.selectedEvent.title.length ? (
|
||||
<Box
|
||||
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 size='small' onClick={handleSidebarClose}>
|
||||
<i className='tabler-x text-2xl text-textPrimary' />
|
||||
<IconButton size="small" onClick={handleSidebarClose}>
|
||||
<i className="tabler-x text-2xl text-textPrimary" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
) : (
|
||||
<IconButton size='small' onClick={handleSidebarClose}>
|
||||
<i className='tabler-x text-2xl text-textPrimary' />
|
||||
<IconButton size="small" onClick={handleSidebarClose}>
|
||||
<i className="tabler-x text-2xl text-textPrimary" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
<ScrollWrapper
|
||||
{...(isBelowSmScreen
|
||||
? { className: 'bs-full overflow-y-auto overflow-x-hidden' }
|
||||
? { className: "bs-full overflow-y-auto overflow-x-hidden" }
|
||||
: { options: { wheelPropagation: false, suppressScrollX: true } })}
|
||||
>
|
||||
<Box className='sidebar-body plb-5 pli-6'>
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete='off' className='flex flex-col gap-6'>
|
||||
<Box className="sidebar-body plb-5 pli-6">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
autoComplete="off"
|
||||
className="flex flex-col gap-6"
|
||||
>
|
||||
<Controller
|
||||
name='title'
|
||||
name="title"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='عنوان'
|
||||
label="Title"
|
||||
value={value}
|
||||
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
|
||||
selectsStart
|
||||
id='event-start-date'
|
||||
id="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={<PickersComponent label='Start Date' registername='startDate' />}
|
||||
onChange={(date: Date | null) => date !== null && setValues({ ...values, startDate: new Date(date) })}
|
||||
dateFormat={!values.allDay ? "yyyy-MM-dd hh:mm" : "yyyy-MM-dd"}
|
||||
customInput={
|
||||
<PickersComponent label="Start Date" registername="startDate" />
|
||||
}
|
||||
onChange={(date: Date | null) =>
|
||||
date !== null &&
|
||||
setValues({ ...values, startDate: new Date(date) })
|
||||
}
|
||||
onSelect={handleStartDate}
|
||||
/>
|
||||
<AppReactDatepicker
|
||||
selectsEnd
|
||||
id='event-end-date'
|
||||
id="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={<PickersComponent label='End Date' registername='endDate' />}
|
||||
onChange={(date: Date | null) => date !== null && setValues({ ...values, endDate: new Date(date) })}
|
||||
dateFormat={!values.allDay ? "yyyy-MM-dd hh:mm" : "yyyy-MM-dd"}
|
||||
customInput={
|
||||
<PickersComponent label="End Date" registername="endDate" />
|
||||
}
|
||||
onChange={(date: Date | null) =>
|
||||
date !== null &&
|
||||
setValues({ ...values, endDate: new Date(date) })
|
||||
}
|
||||
/>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
label='All Day'
|
||||
label="All Day"
|
||||
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>
|
||||
<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
|
||||
rows={4}
|
||||
multiline
|
||||
fullWidth
|
||||
label='توضیحات'
|
||||
id='event-description'
|
||||
label="Description"
|
||||
id="event-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 />
|
||||
</div>
|
||||
</form>
|
||||
</Box>
|
||||
</ScrollWrapper>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default AddEventSidebar
|
||||
export default AddEventSidebar;
|
||||
|
||||
@@ -1,53 +1,68 @@
|
||||
// React Imports
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
// MUI Imports
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
// Third-party imports
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css'
|
||||
import type { Dispatch } from "@reduxjs/toolkit";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
import FullCalendar from '@fullcalendar/react'
|
||||
import faLocale from '@fullcalendar/core/locales/fa'
|
||||
import listPlugin from '@fullcalendar/list'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import type { CalendarOptions } from '@fullcalendar/core'
|
||||
import FullCalendar from "@fullcalendar/react";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import type { CalendarOptions } from "@fullcalendar/core";
|
||||
import faLocale from "@fullcalendar/core/locales/fa";
|
||||
|
||||
// Type Imports
|
||||
import type { AddEventType, CalendarColors, CalendarType } from '@/types/apps/calendarTypes'
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
import type {
|
||||
AddEventType,
|
||||
CalendarColors,
|
||||
CalendarType,
|
||||
} from "@/types/apps/calendarTypes";
|
||||
|
||||
// Slice Imports
|
||||
import { filterEvents, selectedEvent, updateEventAsync } from '@/redux-store/slices/calendar'
|
||||
import {
|
||||
filterEvents,
|
||||
selectedEvent,
|
||||
updateEvent,
|
||||
} from "@/redux-store/slices/calendar";
|
||||
|
||||
type CalenderProps = {
|
||||
calendarStore: CalendarType
|
||||
calendarApi: any
|
||||
setCalendarApi: (val: any) => void
|
||||
calendarsColor: CalendarColors
|
||||
dispatch?: AppDispatch
|
||||
handleLeftSidebarToggle?: () => void
|
||||
handleAddEventSidebarToggle?: () => void
|
||||
editable?: boolean
|
||||
showSidebarToggle?: boolean
|
||||
onDateClick?: (date: Date) => void
|
||||
onEventClick?: (event: any) => void
|
||||
}
|
||||
calendarStore: CalendarType;
|
||||
calendarApi: any;
|
||||
setCalendarApi: (val: any) => void;
|
||||
calendarsColor: CalendarColors;
|
||||
dispatch: Dispatch;
|
||||
handleLeftSidebarToggle: () => void;
|
||||
handleAddEventSidebarToggle: () => void;
|
||||
handleEventDetailsOpen: () => void;
|
||||
};
|
||||
|
||||
const blankEvent: AddEventType = {
|
||||
title: '',
|
||||
start: '',
|
||||
end: '',
|
||||
title: "",
|
||||
start: "",
|
||||
end: "",
|
||||
allDay: false,
|
||||
url: '',
|
||||
url: "",
|
||||
extendedProps: {
|
||||
calendar: '',
|
||||
calendar: "",
|
||||
guests: [],
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
description: "",
|
||||
},
|
||||
};
|
||||
|
||||
const formatPersianDay = (date: Date) =>
|
||||
new Intl.DateTimeFormat("fa-IR-u-ca-persian", { day: "numeric" }).format(
|
||||
date,
|
||||
);
|
||||
|
||||
const formatPersianWeekday = (date: Date) =>
|
||||
new Intl.DateTimeFormat("fa-IR-u-ca-persian", { weekday: "short" }).format(
|
||||
date,
|
||||
);
|
||||
|
||||
const Calendar = (props: CalenderProps) => {
|
||||
// Props
|
||||
@@ -58,110 +73,58 @@ const Calendar = (props: CalenderProps) => {
|
||||
calendarsColor,
|
||||
dispatch,
|
||||
handleAddEventSidebarToggle,
|
||||
handleEventDetailsOpen,
|
||||
handleLeftSidebarToggle,
|
||||
editable = true,
|
||||
showSidebarToggle = true,
|
||||
onDateClick,
|
||||
onEventClick
|
||||
} = props
|
||||
} = props;
|
||||
|
||||
// Refs
|
||||
const calendarRef = useRef()
|
||||
const calendarRef = useRef();
|
||||
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (calendarApi === null) {
|
||||
// @ts-ignore
|
||||
setCalendarApi(calendarRef.current?.getApi())
|
||||
setCalendarApi(calendarRef.current?.getApi());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
// calendarOptions(Props)
|
||||
const calendarOptions: CalendarOptions = {
|
||||
events: calendarStore.events,
|
||||
plugins: [interactionPlugin, dayGridPlugin, timeGridPlugin, listPlugin],
|
||||
initialView: 'dayGridMonth',
|
||||
locale: 'fa',
|
||||
locale: faLocale,
|
||||
initialView: "dayGridMonth",
|
||||
headerToolbar: {
|
||||
start: `${showSidebarToggle ? 'sidebarToggle,' : ''}prev,next,title`,
|
||||
end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth'
|
||||
start: "sidebarToggle, prev, next, title",
|
||||
end: "dayGridMonth,timeGridWeek,timeGridDay,listMonth",
|
||||
},
|
||||
buttonText: {
|
||||
today: "امروز",
|
||||
month: "ماه",
|
||||
week: "هفته",
|
||||
day: "روز",
|
||||
list: "فهرست",
|
||||
},
|
||||
views: {
|
||||
week: {
|
||||
titleFormat(arg: any) {
|
||||
const start = arg.start
|
||||
const end = arg.end
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
|
||||
if (!start && !end) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (start && !end) {
|
||||
return formatter.format(start instanceof Date ? start : new Date(start.marker ?? start))
|
||||
}
|
||||
|
||||
if (!start && end) {
|
||||
return formatter.format(end instanceof Date ? end : new Date(end.marker ?? end))
|
||||
}
|
||||
|
||||
const s = start instanceof Date ? start : new Date(start.marker ?? start)
|
||||
const e = end instanceof Date ? end : new Date(end.marker ?? end)
|
||||
|
||||
return `${formatter.format(s)} - ${formatter.format(e)}`
|
||||
}
|
||||
}
|
||||
titleFormat: { year: "numeric", month: "short", day: "numeric" },
|
||||
},
|
||||
},
|
||||
dayHeaderContent(arg) {
|
||||
const formatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { weekday: 'short' })
|
||||
|
||||
return formatter.format(arg.date)
|
||||
return formatPersianWeekday(arg.date);
|
||||
},
|
||||
titleFormat(arg: any) {
|
||||
const { start, end } = arg
|
||||
|
||||
const monthFormatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
})
|
||||
|
||||
// FullCalendar بعضی وقتها بهجای Date، DateMarker خودش را میفرستد؛
|
||||
// با new Date(...) مطمئن میشویم ورودی برای Intl معتبر است.
|
||||
const target = start || end
|
||||
|
||||
if (!target) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const asDate = target instanceof Date ? target : new Date(target as any)
|
||||
|
||||
if (Number.isNaN(asDate.getTime())) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return monthFormatter.format(asDate)
|
||||
},
|
||||
buttonText: {
|
||||
today: 'امروز',
|
||||
month: 'ماه',
|
||||
week: 'هفته',
|
||||
day: 'روز',
|
||||
list: 'لیست'
|
||||
dayCellContent(arg) {
|
||||
return formatPersianDay(arg.date);
|
||||
},
|
||||
|
||||
/*
|
||||
Enable dragging and resizing event
|
||||
? Docs: https://fullcalendar.io/docs/editable
|
||||
*/
|
||||
editable,
|
||||
editable: true,
|
||||
|
||||
/*
|
||||
Enable resizing event from start
|
||||
@@ -189,28 +152,30 @@ const Calendar = (props: CalenderProps) => {
|
||||
|
||||
eventClassNames({ event: calendarEvent }: any) {
|
||||
// @ts-ignore
|
||||
const colorName = calendarsColor[calendarEvent._def.extendedProps.calendar]
|
||||
const colorName =
|
||||
calendarsColor[calendarEvent._def.extendedProps.calendar];
|
||||
|
||||
return [
|
||||
// Background Color
|
||||
`event-bg-${colorName}`
|
||||
]
|
||||
`event-bg-${colorName}`,
|
||||
];
|
||||
},
|
||||
|
||||
eventClick({ event: clickedEvent, jsEvent }: any) {
|
||||
jsEvent.preventDefault()
|
||||
jsEvent.preventDefault();
|
||||
|
||||
onEventClick?.(clickedEvent)
|
||||
|
||||
if (dispatch && handleAddEventSidebarToggle) {
|
||||
dispatch(selectedEvent(clickedEvent))
|
||||
handleAddEventSidebarToggle()
|
||||
}
|
||||
|
||||
if (clickedEvent.url) {
|
||||
// Open the URL in a new tab
|
||||
window.open(clickedEvent.url, '_blank')
|
||||
}
|
||||
dispatch(
|
||||
selectedEvent({
|
||||
id: clickedEvent.id,
|
||||
url: clickedEvent.url,
|
||||
title: clickedEvent.title,
|
||||
allDay: clickedEvent.allDay,
|
||||
end: clickedEvent.end,
|
||||
start: clickedEvent.start,
|
||||
extendedProps: clickedEvent.extendedProps,
|
||||
}),
|
||||
);
|
||||
handleEventDetailsOpen();
|
||||
|
||||
//* Only grab required field otherwise it goes in infinity loop
|
||||
//! Always grab all fields rendered by form (even if it get `undefined`)
|
||||
@@ -220,28 +185,22 @@ const Calendar = (props: CalenderProps) => {
|
||||
|
||||
customButtons: {
|
||||
sidebarToggle: {
|
||||
icon: 'tabler tabler-menu-2',
|
||||
icon: "tabler tabler-menu-2",
|
||||
click() {
|
||||
handleLeftSidebarToggle?.()
|
||||
}
|
||||
}
|
||||
handleLeftSidebarToggle();
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
dateClick(info: any) {
|
||||
onDateClick?.(info.date)
|
||||
const ev = { ...blankEvent };
|
||||
|
||||
if (!dispatch || !handleAddEventSidebarToggle) {
|
||||
return
|
||||
}
|
||||
ev.start = info.date;
|
||||
ev.end = info.date;
|
||||
ev.allDay = true;
|
||||
|
||||
const ev = { ...blankEvent }
|
||||
|
||||
ev.start = info.date
|
||||
ev.end = info.date
|
||||
ev.allDay = true
|
||||
|
||||
dispatch(selectedEvent(ev))
|
||||
handleAddEventSidebarToggle()
|
||||
dispatch(selectedEvent(ev));
|
||||
handleAddEventSidebarToggle();
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -250,18 +209,8 @@ const Calendar = (props: CalenderProps) => {
|
||||
? We can use `eventDragStop` but it doesn't return updated event so we have to use `eventDrop` which returns updated event
|
||||
*/
|
||||
eventDrop({ event: droppedEvent }: any) {
|
||||
if (!dispatch) {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert FullCalendar event to API format
|
||||
const eventData = {
|
||||
start: droppedEvent.start ? new Date(droppedEvent.start).toISOString() : '',
|
||||
end: droppedEvent.end ? new Date(droppedEvent.end).toISOString() : '',
|
||||
allDay: droppedEvent.allDay || false
|
||||
}
|
||||
dispatch(updateEventAsync({ id: droppedEvent.id, data: eventData }))
|
||||
dispatch(filterEvents())
|
||||
dispatch(updateEvent(droppedEvent));
|
||||
dispatch(filterEvents());
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -269,27 +218,17 @@ const Calendar = (props: CalenderProps) => {
|
||||
? Docs: https://fullcalendar.io/docs/eventResize
|
||||
*/
|
||||
eventResize({ event: resizedEvent }: any) {
|
||||
if (!dispatch) {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert FullCalendar event to API format
|
||||
const eventData = {
|
||||
start: resizedEvent.start ? new Date(resizedEvent.start).toISOString() : '',
|
||||
end: resizedEvent.end ? new Date(resizedEvent.end).toISOString() : '',
|
||||
allDay: resizedEvent.allDay || false
|
||||
}
|
||||
dispatch(updateEventAsync({ id: resizedEvent.id, data: eventData }))
|
||||
dispatch(filterEvents())
|
||||
dispatch(updateEvent(resizedEvent));
|
||||
dispatch(filterEvents());
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
ref: calendarRef,
|
||||
|
||||
direction: theme.direction
|
||||
}
|
||||
direction: theme.direction,
|
||||
};
|
||||
|
||||
return <FullCalendar {...calendarOptions} />
|
||||
}
|
||||
return <FullCalendar {...calendarOptions} />;
|
||||
};
|
||||
|
||||
export default Calendar
|
||||
export default Calendar;
|
||||
|
||||
@@ -1,58 +1,79 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import { useMediaQuery } from '@mui/material'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
// Type Imports
|
||||
import type { CalendarColors, CalendarType } from '@/types/apps/calendarTypes'
|
||||
|
||||
// Component Imports
|
||||
import Calendar from './Calendar'
|
||||
import SidebarLeft from './SidebarLeft'
|
||||
import AddEventSidebar from './AddEventSidebar'
|
||||
|
||||
// CalendarColors Object
|
||||
const calendarsColor: CalendarColors = {
|
||||
Personal: 'error',
|
||||
Business: 'primary',
|
||||
Family: 'warning',
|
||||
Holiday: 'success',
|
||||
ETC: 'info'
|
||||
}
|
||||
|
||||
const AppCalendar = () => {
|
||||
return <></>
|
||||
// // States
|
||||
// const [calendarApi, setCalendarApi] = useState<null | any>(null)
|
||||
// const [leftSidebarOpen, setLeftSidebarOpen] = useState<boolean>(false)
|
||||
// const [addEventSidebarOpen, setAddEventSidebarOpen] = useState<boolean>(false)
|
||||
// States
|
||||
const [calendarApi, setCalendarApi] = useState<null | any>(null)
|
||||
const [leftSidebarOpen, setLeftSidebarOpen] = useState<boolean>(false)
|
||||
const [addEventSidebarOpen, setAddEventSidebarOpen] = useState<boolean>(false)
|
||||
|
||||
// // Hooks
|
||||
// const dispatch = useAppDispatch()
|
||||
// const calendarStore = useSelector((state: { calendarReducer: CalendarType & { loading: boolean; error: string | null } }) => state.calendarReducer)
|
||||
// const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md'))
|
||||
// Hooks
|
||||
const dispatch = useDispatch()
|
||||
const calendarStore = useSelector((state: { calendarReducer: CalendarType }) => state.calendarReducer)
|
||||
const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md'))
|
||||
|
||||
// // Fetch events on mount
|
||||
// useEffect(() => {
|
||||
// dispatch(fetchEvents())
|
||||
// }, [dispatch])
|
||||
const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen)
|
||||
|
||||
// const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen)
|
||||
const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen)
|
||||
|
||||
// const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen)
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <SidebarLeft
|
||||
// mdAbove={mdAbove}
|
||||
// dispatch={dispatch}
|
||||
// calendarApi={calendarApi}
|
||||
// calendarStore={calendarStore}
|
||||
// calendarsColor={calendarsColor}
|
||||
// leftSidebarOpen={leftSidebarOpen}
|
||||
// handleLeftSidebarToggle={handleLeftSidebarToggle}
|
||||
// handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||
// />
|
||||
// <div className='p-6 pbe-0 flex-grow overflow-visible bg-backgroundPaper rounded'>
|
||||
// <Calendar
|
||||
// dispatch={dispatch}
|
||||
// calendarApi={calendarApi}
|
||||
// calendarStore={calendarStore}
|
||||
// setCalendarApi={setCalendarApi}
|
||||
// calendarsColor={calendarsColor}
|
||||
// handleLeftSidebarToggle={handleLeftSidebarToggle}
|
||||
// handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||
// />
|
||||
// </div>
|
||||
// <AddEventSidebar
|
||||
// dispatch={dispatch}
|
||||
// calendarApi={calendarApi}
|
||||
// calendarStore={calendarStore}
|
||||
// addEventSidebarOpen={addEventSidebarOpen}
|
||||
// handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||
// />
|
||||
// </>
|
||||
// )
|
||||
return (
|
||||
<>
|
||||
<SidebarLeft
|
||||
mdAbove={mdAbove}
|
||||
dispatch={dispatch}
|
||||
calendarApi={calendarApi}
|
||||
calendarStore={calendarStore}
|
||||
calendarsColor={calendarsColor}
|
||||
leftSidebarOpen={leftSidebarOpen}
|
||||
handleLeftSidebarToggle={handleLeftSidebarToggle}
|
||||
handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||
/>
|
||||
<div className='p-6 pbe-0 flex-grow overflow-visible bg-backgroundPaper rounded'>
|
||||
<Calendar
|
||||
dispatch={dispatch}
|
||||
calendarApi={calendarApi}
|
||||
calendarStore={calendarStore}
|
||||
setCalendarApi={setCalendarApi}
|
||||
calendarsColor={calendarsColor}
|
||||
handleLeftSidebarToggle={handleLeftSidebarToggle}
|
||||
handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||
/>
|
||||
</div>
|
||||
<AddEventSidebar
|
||||
dispatch={dispatch}
|
||||
calendarApi={calendarApi}
|
||||
calendarStore={calendarStore}
|
||||
addEventSidebarOpen={addEventSidebarOpen}
|
||||
handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
import type { SidebarLeftProps, CalendarFiltersType } from '@/types/apps/calendarTypes'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Styled Component Imports
|
||||
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
|
||||
|
||||
// Slice Imports
|
||||
import { filterAllCalendarLabels, filterCalendarLabel, selectedEvent } from '@/redux-store/slices/calendar'
|
||||
|
||||
const SidebarLeft = (props: SidebarLeftProps) => {
|
||||
// Props
|
||||
@@ -15,205 +32,106 @@ const SidebarLeft = (props: SidebarLeftProps) => {
|
||||
handleAddEventSidebarToggle
|
||||
} = props
|
||||
|
||||
// // Vars
|
||||
// const colorsArr = calendarsColor ? Object.entries(calendarsColor) : []
|
||||
// Vars
|
||||
const colorsArr = calendarsColor ? Object.entries(calendarsColor) : []
|
||||
|
||||
// const renderFilters = colorsArr.length
|
||||
// ? colorsArr.map(([key, value]: string[]) => {
|
||||
// return (
|
||||
// <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 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()
|
||||
// }
|
||||
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' />
|
||||
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' />
|
||||
<AppReactDatepicker
|
||||
inline
|
||||
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
|
||||
// }
|
||||
// // 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 <></>
|
||||
<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
|
||||
}
|
||||
}
|
||||
|
||||
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