diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md deleted file mode 100644 index f7568b1..0000000 --- a/API_DOCUMENTATION.md +++ /dev/null @@ -1,1724 +0,0 @@ -# مستندات API های صفحات منوی عمودی - -این مستندات شامل تمام API های مورد نیاز برای صفحات موجود در VerticalMenu است. - -**توجهات مهم:** -- صفحات User Management و Roles & Permissions فقط برای ادمین قابل دسترسی است -- صفحات Calendar، Kanban، و Todo باید به یکدیگر متصل باشند و تسک‌های غیرروتین در هر سه قابل نمایش باشند -- AI Chat باید به تسک‌های Calendar، Kanban، و Todo دسترسی داشته باشد -- AI Chat باید نسبت به برخی پیام‌ها حساس باشد (sensitive message handling) -- سیستم Authentication جزو این مستندات نیست - ---- - -## فهرست مطالب - -1. [Chat](#1-chat) -2. [AI Chat](#2-ai-chat) -3. [Calendar](#3-calendar) -4. [Kanban](#4-kanban) -5. [Todo](#5-todo) -6. [User Management](#6-user-management) -7. [Roles & Permissions](#7-roles--permissions) - ---- - -## 1. Chat - -**Route:** `/apps/chat` - -**دسترسی:** همه کاربران - -### API های مورد نیاز - -#### 1.1. دریافت لیست مخاطبین - -**Endpoint:** `GET /api/chat/contacts` - -**Response:** -```json -{ - "contacts": [ - { - "id": number, - "fullName": "string", - "role": "string", - "about": "string", - "avatar": "string", - "avatarColor": "primary" | "success" | "error" | "warning" | "info", - "status": "busy" | "away" | "online" | "offline" - } - ] -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `contacts[].id` | number | شناسه یکتا مخاطب | -| `contacts[].fullName` | string | نام کامل | -| `contacts[].role` | string | نقش کاربر | -| `contacts[].about` | string | درباره کاربر | -| `contacts[].avatar` | string | آواتار (URL) | -| `contacts[].avatarColor` | ThemeColor | رنگ آواتار (در صورت نبودن تصویر) | -| `contacts[].status` | StatusType | وضعیت آنلاین/آفلاین | - -#### 1.2. دریافت چت‌های کاربر - -**Endpoint:** `GET /api/chat/conversations` - -**Response:** -```json -{ - "chats": [ - { - "id": number, - "userId": number, - "unseenMsgs": number, - "lastMessage": { - "message": "string", - "time": "string", - "senderId": number, - "msgStatus": { - "isSent": boolean, - "isDelivered": boolean, - "isSeen": boolean - } - } - } - ] -} -``` - -#### 1.3. دریافت پیام‌های چت - -**Endpoint:** `GET /api/chat/conversations/:conversationId/messages` - -**Path Parameters:** -- `conversationId` (number, required): شناسه مکالمه - -**Query Parameters:** -- `page?` (number, optional): شماره صفحه -- `limit?` (number, optional): تعداد پیام در هر صفحه - -**Response:** -```json -{ - "messages": [ - { - "message": "string", - "time": "string", - "senderId": number, - "msgStatus": { - "isSent": boolean, - "isDelivered": boolean, - "isSeen": boolean - } - } - ], - "pagination": { - "page": number, - "limit": number, - "total": number, - "totalPages": number - } -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `messages[].message` | string | متن پیام | -| `messages[].time` | string | زمان ارسال (ISO 8601) | -| `messages[].senderId` | number | شناسه فرستنده | -| `messages[].msgStatus.isSent` | boolean | وضعیت ارسال | -| `messages[].msgStatus.isDelivered` | boolean | وضعیت تحویل | -| `messages[].msgStatus.isSeen` | boolean | وضعیت مشاهده | - -#### 1.4. ارسال پیام جدید - -**Endpoint:** `POST /api/chat/conversations/:conversationId/messages` - -**Path Parameters:** -- `conversationId` (number, required): شناسه مکالمه - -**Request Body:** -```json -{ - "message": "string" -} -``` - -**Response:** -```json -{ - "message": { - "id": number, - "message": "string", - "time": "string", - "senderId": number, - "msgStatus": { - "isSent": boolean, - "isDelivered": boolean, - "isSeen": boolean - } - } -} -``` - -#### 1.5. به‌روزرسانی وضعیت مشاهده پیام - -**Endpoint:** `PUT /api/chat/messages/:messageId/read` - -**Path Parameters:** -- `messageId` (number, required): شناسه پیام - -**Response:** -```json -{ - "success": boolean -} -``` - -#### 1.6. دریافت اطلاعات پروفایل کاربر - -**Endpoint:** `GET /api/chat/profile` - -**Response:** -```json -{ - "profileUser": { - "id": number, - "role": "string", - "about": "string", - "avatar": "string", - "fullName": "string", - "status": "busy" | "away" | "online" | "offline", - "settings": { - "isNotificationsOn": boolean, - "isTwoStepAuthVerificationEnabled": boolean - } - } -} -``` - ---- - -## 2. AI Chat - -**Route:** `/apps/ai-chat` - -**دسترسی:** همه کاربران - -**نکات مهم:** -- AI Chat باید به تسک‌های Calendar، Kanban، و Todo دسترسی داشته باشد -- AI Chat باید نسبت به برخی پیام‌ها حساس باشد و پردازش ویژه انجام دهد - -### API های مورد نیاز - -#### 2.1. ارسال پیام به AI - -**Endpoint:** `POST /api/ai-chat/messages` - -**Request Body:** -```json -{ - "content": "string", - "images": ["string"], - "files": [ - { - "name": "string", - "data": "string", - "type": "string", - "size": number - } - ], - "model": "gpt-4" | "gpt-3.5" | "claude" | "gemini", - "conversationId": "string" -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `content` | string | متن پیام | -| `images` | string[] | آرایه URL تصاویر (base64 data URLs) | -| `files[].name` | string | نام فایل | -| `files[].data` | string | داده فایل (base64 data URL) | -| `files[].type` | string | نوع MIME فایل | -| `files[].size` | number | حجم فایل (بایت) | -| `model` | string | مدل AI مورد استفاده | -| `conversationId` | string | شناسه مکالمه (اختیاری) | - -**Response:** -```json -{ - "message": { - "id": "string", - "role": "user" | "assistant", - "content": "string", - "images": ["string"], - "files": [ - { - "name": "string", - "data": "string", - "type": "string", - "size": number - } - ], - "timestamp": "string", - "isSensitive": boolean, - "sensitiveReason": "string" - }, - "assistantResponse": { - "id": "string", - "role": "assistant", - "content": "string", - "timestamp": "string", - "referencedTasks": [ - { - "taskId": "string", - "source": "calendar" | "kanban" | "todo", - "title": "string" - } - ] - } -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `message.isSensitive` | boolean | آیا پیام حساس است | -| `message.sensitiveReason` | string | دلیل حساس بودن پیام | -| `assistantResponse.referencedTasks[].taskId` | string | شناسه تسک | -| `assistantResponse.referencedTasks[].source` | string | منبع تسک (calendar/kanban/todo) | -| `assistantResponse.referencedTasks[].title` | string | عنوان تسک | - -#### 2.2. دریافت تاریخچه مکالمه - -**Endpoint:** `GET /api/ai-chat/conversations/:conversationId/messages` - -**Path Parameters:** -- `conversationId` (string, required): شناسه مکالمه - -**Response:** -```json -{ - "messages": [ - { - "id": "string", - "role": "user" | "assistant", - "content": "string", - "images": ["string"], - "files": [ - { - "name": "string", - "data": "string", - "type": "string", - "size": number - } - ], - "timestamp": "string" - } - ] -} -``` - -#### 2.3. دریافت لیست مکالمات - -**Endpoint:** `GET /api/ai-chat/conversations` - -**Response:** -```json -{ - "conversations": [ - { - "id": "string", - "title": "string", - "lastMessage": "string", - "timestamp": "string", - "messageCount": number - } - ] -} -``` - -#### 2.4. دریافت تسک‌های مرتبط (برای AI Chat) - -**Endpoint:** `GET /api/ai-chat/tasks` - -**Query Parameters:** -- `source?` (string, optional): منبع تسک (calendar, kanban, todo) -- `routine?` (number, optional): نوع روتین (0: NONE, 1: DAILY, 2: WEEKLY, 3: MONTHLY, 4: YEARLY) - -**Response:** -```json -{ - "tasks": [ - { - "id": "string", - "title": "string", - "description": "string", - "deadline": number, - "routine": 0 | 1 | 2 | 3 | 4, - "tags": ["string"], - "source": "calendar" | "kanban" | "todo", - "status": "string", - "priority": "high" | "medium" | "low" - } - ] -} -``` - -**نکته:** این API تسک‌هایی که routine = 0 دارند را برمی‌گرداند تا در Calendar، Kanban، و Todo مشترک باشند. - ---- - -## 3. Calendar - -**Route:** `/apps/calendar` - -**دسترسی:** همه کاربران - -**نکات مهم:** -- Calendar باید با Kanban و Todo متصل باشد -- تسک‌هایی که routine = 0 دارند باید در هر سه قابل نمایش باشند - -### API های مورد نیاز - -#### 3.1. دریافت Event ها - -**Endpoint:** `GET /api/events` - -**Query Parameters:** -- `start?` (string, optional): تاریخ شروع (ISO 8601) -- `end?` (string, optional): تاریخ پایان (ISO 8601) -- `calendar?` (string, optional): فیلتر بر اساس تقویم (Personal, Business, Family, Holiday, ETC) - -**Response:** -```json -{ - "events": [ - { - "id": "string", - "title": "string", - "description": "string", - "deadline": number, - "tags": ["string"], - "author": { - "name": "string", - "image": "string" - }, - "calendar": "Personal" | "Business" | "Family" | "Holiday" | "ETC", - "start": "string", - "end": "string", - "allDay": boolean, - "extendedProps": {} - } - ] -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `events[].id` | string | شناسه یکتا رویداد | -| `events[].title` | string | عنوان رویداد | -| `events[].description` | string | توضیحات رویداد | -| `events[].deadline` | number | مهلت (Unix timestamp) | -| `events[].tags` | string[] | برچسب‌ها | -| `events[].author.name` | string | نام نویسنده | -| `events[].author.image` | string | تصویر نویسنده (URL) | -| `events[].calendar` | string | نوع تقویم | -| `events[].start` | string | زمان شروع (ISO 8601) | -| `events[].end` | string | زمان پایان (ISO 8601) | -| `events[].allDay` | boolean | رویداد تمام روز | - -#### 3.2. ایجاد Event جدید - -**Endpoint:** `POST /api/events` - -**Request Body:** -```json -{ - "title": "string", - "description": "string", - "deadline": number, - "tags": ["string"], - "calendar": "Personal" | "Business" | "Family" | "Holiday" | "ETC", - "start": "string", - "end": "string", - "allDay": boolean, - "extendedProps": {} -} -``` - -**Response:** -```json -{ - "event": { - "id": "string", - "title": "string", - "description": "string", - "deadline": number, - "tags": ["string"], - "author": { - "name": "string", - "image": "string" - }, - "calendar": "string", - "start": "string", - "end": "string", - "allDay": boolean - } -} -``` - -#### 3.3. به‌روزرسانی Event - -**Endpoint:** `PUT /api/events/:eventId` - -**Path Parameters:** -- `eventId` (string, required): شناسه رویداد - -**Request Body:** -```json -{ - "title": "string", - "description": "string", - "deadline": number, - "tags": ["string"], - "calendar": "string", - "start": "string", - "end": "string", - "allDay": boolean -} -``` - -**Response:** -```json -{ - "event": { - "id": "string", - "title": "string", - "description": "string", - "deadline": number, - "tags": ["string"], - "author": { - "name": "string", - "image": "string" - }, - "calendar": "string", - "start": "string", - "end": "string", - "allDay": boolean - } -} -``` - -#### 3.4. حذف Event - -**Endpoint:** `DELETE /api/events/:eventId` - -**Path Parameters:** -- `eventId` (string, required): شناسه رویداد - -**Response:** -```json -{ - "success": boolean -} -``` - -#### 3.5. دریافت تسک‌های غیرروتین (مشترک با Kanban و Todo) - -**Endpoint:** `GET /api/tasks/non-routine` - -**Response:** -```json -{ - "tasks": [ - { - "id": "string", - "title": "string", - "description": "string", - "deadline": number, - "routine": 0, - "tags": ["string"], - "author": { - "name": "string", - "image": "string" - }, - "source": "calendar" | "kanban" | "todo", - "status": "string", - "priority": "high" | "medium" | "low" - } - ] -} -``` - -**نکته:** این API فقط تسک‌هایی با `routine = 0` (NONE) را برمی‌گرداند که باید در Calendar، Kanban، و Todo نمایش داده شوند. - ---- - -## 4. Kanban - -**Route:** `/apps/kanban` - -**دسترسی:** همه کاربران - -**نکات مهم:** -- Kanban باید با Calendar و Todo متصل باشد -- تسک‌هایی که routine = 0 دارند باید در هر سه قابل نمایش باشند - -### API های مورد نیاز - -#### 4.1. دریافت Board Kanban - -**Endpoint:** `GET /api/kanban/board` - -**Response:** -```json -{ - "columns": [ - { - "id": number, - "title": "string", - "taskIds": [number] - } - ], - "tasks": [ - { - "id": number, - "title": "string", - "badgeText": ["string"], - "attachments": number, - "comments": number, - "assigned": [ - { - "src": "string", - "name": "string" - } - ], - "image": "string", - "dueDate": "string", - "routine": 0 | 1 | 2 | 3 | 4, - "description": "string", - "tags": ["string"], - "source": "kanban" | "calendar" | "todo" - } - ] -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `columns[].id` | number | شناسه یکتا ستون | -| `columns[].title` | string | عنوان ستون | -| `columns[].taskIds` | number[] | شناسه‌های تسک‌های موجود در ستون | -| `tasks[].id` | number | شناسه یکتا تسک | -| `tasks[].title` | string | عنوان تسک | -| `tasks[].badgeText` | string[] | برچسب‌های متن | -| `tasks[].attachments` | number | تعداد پیوست‌ها | -| `tasks[].comments` | number | تعداد کامنت‌ها | -| `tasks[].assigned[].src` | string | تصویر اختصاص یافته (URL) | -| `tasks[].assigned[].name` | string | نام اختصاص یافته | -| `tasks[].image` | string | تصویر تسک (URL) | -| `tasks[].dueDate` | string | تاریخ مهلت (ISO 8601) | -| `tasks[].routine` | number | نوع روتین (0: NONE, 1: DAILY, ...) | -| `tasks[].description` | string | توضیحات تسک | -| `tasks[].tags` | string[] | برچسب‌ها | -| `tasks[].source` | string | منبع تسک | - -#### 4.2. ایجاد ستون جدید - -**Endpoint:** `POST /api/kanban/columns` - -**Request Body:** -```json -{ - "title": "string" -} -``` - -**Response:** -```json -{ - "column": { - "id": number, - "title": "string", - "taskIds": [] - } -} -``` - -#### 4.3. به‌روزرسانی ستون - -**Endpoint:** `PUT /api/kanban/columns/:columnId` - -**Path Parameters:** -- `columnId` (number, required): شناسه ستون - -**Request Body:** -```json -{ - "title": "string", - "taskIds": [number] -} -``` - -**Response:** -```json -{ - "column": { - "id": number, - "title": "string", - "taskIds": [number] - } -} -``` - -#### 4.4. حذف ستون - -**Endpoint:** `DELETE /api/kanban/columns/:columnId` - -**Path Parameters:** -- `columnId` (number, required): شناسه ستون - -**Response:** -```json -{ - "success": boolean -} -``` - -#### 4.5. ایجاد تسک جدید - -**Endpoint:** `POST /api/kanban/tasks` - -**Request Body:** -```json -{ - "columnId": number, - "title": "string", - "description": "string", - "badgeText": ["string"], - "attachments": number, - "comments": number, - "assigned": [ - { - "src": "string", - "name": "string" - } - ], - "image": "string", - "dueDate": "string", - "routine": 0 | 1 | 2 | 3 | 4, - "tags": ["string"] -} -``` - -**Response:** -```json -{ - "task": { - "id": number, - "title": "string", - "badgeText": ["string"], - "attachments": number, - "comments": number, - "assigned": [ - { - "src": "string", - "name": "string" - } - ], - "image": "string", - "dueDate": "string", - "routine": number, - "description": "string", - "tags": ["string"], - "source": "kanban" - } -} -``` - -#### 4.6. به‌روزرسانی تسک - -**Endpoint:** `PUT /api/kanban/tasks/:taskId` - -**Path Parameters:** -- `taskId` (number, required): شناسه تسک - -**Request Body:** -```json -{ - "title": "string", - "description": "string", - "badgeText": ["string"], - "attachments": number, - "comments": number, - "assigned": [ - { - "src": "string", - "name": "string" - } - ], - "image": "string", - "dueDate": "string", - "routine": number, - "tags": ["string"], - "columnId": number -} -``` - -**Response:** -```json -{ - "task": { - "id": number, - "title": "string", - "badgeText": ["string"], - "attachments": number, - "comments": number, - "assigned": [ - { - "src": "string", - "name": "string" - } - ], - "image": "string", - "dueDate": "string", - "routine": number, - "description": "string", - "tags": ["string"], - "source": "string" - } -} -``` - -#### 4.7. حذف تسک - -**Endpoint:** `DELETE /api/kanban/tasks/:taskId` - -**Path Parameters:** -- `taskId` (number, required): شناسه تسک - -**Response:** -```json -{ - "success": boolean -} -``` - -#### 4.8. جابجایی تسک بین ستون‌ها - -**Endpoint:** `PUT /api/kanban/tasks/:taskId/move` - -**Path Parameters:** -- `taskId` (number, required): شناسه تسک - -**Request Body:** -```json -{ - "fromColumnId": number, - "toColumnId": number, - "position": number -} -``` - -**Response:** -```json -{ - "success": boolean, - "columns": [ - { - "id": number, - "title": "string", - "taskIds": [number] - } - ] -} -``` - ---- - -## 5. Todo - -**Route:** `/todo/all` - -**دسترسی:** همه کاربران - -**نکات مهم:** -- Todo باید با Calendar و Kanban متصل باشد -- تسک‌هایی که routine = 0 دارند باید در هر سه قابل نمایش باشند - -### API های مورد نیاز - -#### 5.1. دریافت لیست Todo - -**Endpoint:** `GET /api/todos` - -**Query Parameters:** -- `status?` (string, optional): وضعیت (pending, in-progress, completed) -- `priority?` (string, optional): اولویت (high, medium, low) -- `label?` (string, optional): برچسب -- `filter?` (string, optional): فیلتر (all, starred, important, completed, trashed) -- `search?` (string, optional): جستجو در عنوان و توضیحات - -**Response:** -```json -{ - "todos": [ - { - "id": number, - "title": "string", - "description": "string", - "status": "pending" | "in-progress" | "completed", - "priority": "high" | "medium" | "low", - "startDate": "string", - "dueDate": "string", - "createdDate": "string", - "labels": ["string"], - "isStarred": boolean, - "isImportant": boolean, - "isTrashed": boolean, - "routine": 0 | 1 | 2 | 3 | 4, - "source": "todo" | "calendar" | "kanban" - } - ], - "total": number -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `todos[].id` | number | شناسه یکتا todo | -| `todos[].title` | string | عنوان todo | -| `todos[].description` | string | توضیحات todo | -| `todos[].status` | string | وضعیت (pending, in-progress, completed) | -| `todos[].priority` | string | اولویت (high, medium, low) | -| `todos[].startDate` | string | تاریخ شروع (ISO 8601) | -| `todos[].dueDate` | string | تاریخ مهلت (ISO 8601) | -| `todos[].createdDate` | string | تاریخ ایجاد (ISO 8601) | -| `todos[].labels` | string[] | برچسب‌ها | -| `todos[].isStarred` | boolean | نشان شده | -| `todos[].isImportant` | boolean | مهم | -| `todos[].isTrashed` | boolean | حذف شده | -| `todos[].routine` | number | نوع روتین (0: NONE, 1: DAILY, ...) | -| `todos[].source` | string | منبع todo | - -#### 5.2. ایجاد Todo جدید - -**Endpoint:** `POST /api/todos` - -**Request Body:** -```json -{ - "title": "string", - "description": "string", - "status": "pending" | "in-progress" | "completed", - "priority": "high" | "medium" | "low", - "startDate": "string", - "dueDate": "string", - "labels": ["string"], - "isStarred": boolean, - "isImportant": boolean, - "routine": 0 | 1 | 2 | 3 | 4 -} -``` - -**Response:** -```json -{ - "todo": { - "id": number, - "title": "string", - "description": "string", - "status": "string", - "priority": "string", - "startDate": "string", - "dueDate": "string", - "createdDate": "string", - "labels": ["string"], - "isStarred": boolean, - "isImportant": boolean, - "isTrashed": boolean, - "routine": number, - "source": "todo" - } -} -``` - -#### 5.3. به‌روزرسانی Todo - -**Endpoint:** `PUT /api/todos/:todoId` - -**Path Parameters:** -- `todoId` (number, required): شناسه todo - -**Request Body:** -```json -{ - "title": "string", - "description": "string", - "status": "pending" | "in-progress" | "completed", - "priority": "high" | "medium" | "low", - "startDate": "string", - "dueDate": "string", - "labels": ["string"], - "isStarred": boolean, - "isImportant": boolean, - "isTrashed": boolean, - "routine": number -} -``` - -**Response:** -```json -{ - "todo": { - "id": number, - "title": "string", - "description": "string", - "status": "string", - "priority": "string", - "startDate": "string", - "dueDate": "string", - "createdDate": "string", - "labels": ["string"], - "isStarred": boolean, - "isImportant": boolean, - "isTrashed": boolean, - "routine": number, - "source": "string" - } -} -``` - -#### 5.4. حذف Todo - -**Endpoint:** `DELETE /api/todos/:todoId` - -**Path Parameters:** -- `todoId` (number, required): شناسه todo - -**Response:** -```json -{ - "success": boolean -} -``` - -#### 5.5. دریافت لیست برچسب‌های موجود - -**Endpoint:** `GET /api/todos/labels` - -**Response:** -```json -{ - "labels": ["string"] -} -``` - ---- - -## 6. User Management - -**Route:** `/apps/user/list`, `/apps/user/view` - -**دسترسی:** فقط برای ادمین (Admin) - -### 6.1. User List - -**Route:** `/apps/user/list` - -#### API های مورد نیاز - -##### 6.1.1. دریافت لیست کاربران - -**Endpoint:** `GET /api/admin/users` - -**Query Parameters:** -- `role?` (string, optional): فیلتر بر اساس نقش (admin, author, editor, maintainer, subscriber) -- `status?` (string, optional): فیلتر بر اساس وضعیت (active, pending, inactive) -- `search?` (string, optional): جستجو در نام، ایمیل، نام کاربری -- `page?` (number, optional): شماره صفحه -- `limit?` (number, optional): تعداد آیتم در هر صفحه - -**Response:** -```json -{ - "users": [ - { - "id": number, - "role": "string", - "email": "string", - "status": "active" | "pending" | "inactive", - "avatar": "string", - "company": "string", - "country": "string", - "contact": "string", - "fullName": "string", - "username": "string", - "currentPlan": "string", - "avatarColor": "primary" | "success" | "error" | "warning" | "info", - "billing": "string" - } - ], - "pagination": { - "page": number, - "limit": number, - "total": number, - "totalPages": number - }, - "stats": { - "total": number, - "active": number, - "pending": number, - "inactive": number - } -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `users[].id` | number | شناسه یکتا کاربر | -| `users[].role` | string | نقش کاربر | -| `users[].email` | string | ایمیل کاربر | -| `users[].status` | string | وضعیت کاربر | -| `users[].avatar` | string | آواتار کاربر (URL) | -| `users[].company` | string | نام شرکت | -| `users[].country` | string | کشور | -| `users[].contact` | string | شماره تماس | -| `users[].fullName` | string | نام کامل | -| `users[].username` | string | نام کاربری | -| `users[].currentPlan` | string | پلن فعلی | -| `users[].avatarColor` | ThemeColor | رنگ آواتار (در صورت نبودن تصویر) | -| `users[].billing` | string | روش پرداخت | - -##### 6.1.2. ایجاد کاربر جدید - -**Endpoint:** `POST /api/admin/users` - -**Request Body:** -```json -{ - "role": "string", - "email": "string", - "fullName": "string", - "username": "string", - "contact": "string", - "company": "string", - "country": "string", - "status": "active" | "pending" | "inactive", - "currentPlan": "string", - "avatar": "string" -} -``` - -**Response:** -```json -{ - "user": { - "id": number, - "role": "string", - "email": "string", - "status": "string", - "avatar": "string", - "company": "string", - "country": "string", - "contact": "string", - "fullName": "string", - "username": "string", - "currentPlan": "string", - "avatarColor": "string", - "billing": "string" - } -} -``` - -##### 6.1.3. به‌روزرسانی کاربر - -**Endpoint:** `PUT /api/admin/users/:userId` - -**Path Parameters:** -- `userId` (number, required): شناسه کاربر - -**Request Body:** -```json -{ - "role": "string", - "email": "string", - "fullName": "string", - "username": "string", - "contact": "string", - "company": "string", - "country": "string", - "status": "active" | "pending" | "inactive", - "currentPlan": "string", - "avatar": "string" -} -``` - -**Response:** -```json -{ - "user": { - "id": number, - "role": "string", - "email": "string", - "status": "string", - "avatar": "string", - "company": "string", - "country": "string", - "contact": "string", - "fullName": "string", - "username": "string", - "currentPlan": "string", - "avatarColor": "string", - "billing": "string" - } -} -``` - -##### 6.1.4. حذف کاربر - -**Endpoint:** `DELETE /api/admin/users/:userId` - -**Path Parameters:** -- `userId` (number, required): شناسه کاربر - -**Response:** -```json -{ - "success": boolean -} -``` - -### 6.2. User View - -**Route:** `/apps/user/view` - -#### API های مورد نیاز - -##### 6.2.1. دریافت جزئیات کاربر - -**Endpoint:** `GET /api/admin/users/:userId` - -**Path Parameters:** -- `userId` (number, required): شناسه کاربر - -**Response:** -```json -{ - "user": { - "id": number, - "role": "string", - "email": "string", - "status": "string", - "avatar": "string", - "company": "string", - "country": "string", - "contact": "string", - "fullName": "string", - "username": "string", - "currentPlan": "string", - "avatarColor": "string", - "billing": "string", - "joinDate": "string", - "lastLogin": "string", - "twoStepVerification": boolean, - "recentDevices": [ - { - "id": "string", - "device": "string", - "browser": "string", - "location": "string", - "lastActive": "string", - "ip": "string" - } - ], - "activityTimeline": [ - { - "id": "string", - "title": "string", - "description": "string", - "time": "string", - "icon": "string", - "color": "string" - } - ], - "projects": [ - { - "id": "string", - "name": "string", - "startDate": "string", - "deadline": "string", - "status": "string", - "budget": number, - "spent": number - } - ], - "invoices": [ - { - "id": "string", - "total": number, - "issuedDate": "string", - "status": "string", - "balance": number - } - ], - "connections": [ - { - "id": "string", - "app": "string", - "username": "string", - "avatar": "string", - "connected": boolean, - "connectedAt": "string" - } - ], - "notifications": { - "email": { - "newComment": boolean, - "newAnswer": boolean, - "followMe": boolean, - "answerOnForm": boolean, - "productUpdate": boolean, - "productNewFeature": boolean, - "productAnnouncement": boolean - }, - "phone": { - "newComment": boolean, - "newAnswer": boolean, - "followMe": boolean, - "answerOnForm": boolean - } - } - } -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `user.joinDate` | string | تاریخ عضویت (ISO 8601) | -| `user.lastLogin` | string | آخرین ورود (ISO 8601) | -| `user.twoStepVerification` | boolean | فعال‌سازی احراز هویت دو مرحله‌ای | -| `user.recentDevices[].id` | string | شناسه دستگاه | -| `user.recentDevices[].device` | string | نام دستگاه | -| `user.recentDevices[].browser` | string | مرورگر | -| `user.recentDevices[].location` | string | موقعیت جغرافیایی | -| `user.recentDevices[].lastActive` | string | آخرین فعالیت (ISO 8601) | -| `user.recentDevices[].ip` | string | آدرس IP | -| `user.activityTimeline[].id` | string | شناسه فعالیت | -| `user.activityTimeline[].title` | string | عنوان فعالیت | -| `user.activityTimeline[].description` | string | توضیحات فعالیت | -| `user.activityTimeline[].time` | string | زمان فعالیت (ISO 8601) | -| `user.activityTimeline[].icon` | string | آیکون | -| `user.activityTimeline[].color` | string | رنگ | -| `user.projects[].id` | string | شناسه پروژه | -| `user.projects[].name` | string | نام پروژه | -| `user.projects[].startDate` | string | تاریخ شروع (ISO 8601) | -| `user.projects[].deadline` | string | تاریخ مهلت (ISO 8601) | -| `user.projects[].status` | string | وضعیت پروژه | -| `user.projects[].budget` | number | بودجه | -| `user.projects[].spent` | number | هزینه شده | -| `user.invoices[].id` | string | شناسه فاکتور | -| `user.invoices[].total` | number | مبلغ کل | -| `user.invoices[].issuedDate` | string | تاریخ صدور (ISO 8601) | -| `user.invoices[].status` | string | وضعیت فاکتور | -| `user.invoices[].balance` | number | مانده حساب | -| `user.connections[].id` | string | شناسه اتصال | -| `user.connections[].app` | string | نام اپلیکیشن | -| `user.connections[].username` | string | نام کاربری | -| `user.connections[].avatar` | string | آواتار (URL) | -| `user.connections[].connected` | boolean | وضعیت اتصال | -| `user.connections[].connectedAt` | string | تاریخ اتصال (ISO 8601) | -| `user.notifications.email.*` | boolean | تنظیمات اطلاع‌رسانی ایمیل | -| `user.notifications.phone.*` | boolean | تنظیمات اطلاع‌رسانی تلفن | - -##### 6.2.2. به‌روزرسانی تنظیمات امنیتی کاربر - -**Endpoint:** `PUT /api/admin/users/:userId/security` - -**Path Parameters:** -- `userId` (number, required): شناسه کاربر - -**Request Body:** -```json -{ - "twoStepVerification": boolean, - "currentPassword": "string", - "newPassword": "string" -} -``` - -**Response:** -```json -{ - "success": boolean -} -``` - -##### 6.2.3. به‌روزرسانی تنظیمات اطلاع‌رسانی - -**Endpoint:** `PUT /api/admin/users/:userId/notifications` - -**Path Parameters:** -- `userId` (number, required): شناسه کاربر - -**Request Body:** -```json -{ - "email": { - "newComment": boolean, - "newAnswer": boolean, - "followMe": boolean, - "answerOnForm": boolean, - "productUpdate": boolean, - "productNewFeature": boolean, - "productAnnouncement": boolean - }, - "phone": { - "newComment": boolean, - "newAnswer": boolean, - "followMe": boolean, - "answerOnForm": boolean - } -} -``` - -**Response:** -```json -{ - "success": boolean -} -``` - ---- - -## 7. Roles & Permissions - -**Route:** `/apps/roles`, `/apps/permissions` - -**دسترسی:** فقط برای ادمین (Admin) - -### 7.1. Roles - -**Route:** `/apps/roles` - -#### API های مورد نیاز - -##### 7.1.1. دریافت لیست نقش‌ها - -**Endpoint:** `GET /api/admin/roles` - -**Response:** -```json -{ - "roles": [ - { - "id": "string", - "name": "string", - "totalUsers": number, - "avatars": ["string"], - "description": "string" - } - ], - "roleStats": { - "administrator": number, - "author": number, - "editor": number, - "maintainer": number, - "subscriber": number - } -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `roles[].id` | string | شناسه یکتا نقش | -| `roles[].name` | string | نام نقش | -| `roles[].totalUsers` | number | تعداد کاربران دارای این نقش | -| `roles[].avatars` | string[] | آواتارهای کاربران (URL) | -| `roles[].description` | string | توضیحات نقش | -| `roleStats.*` | number | آمار کاربران هر نقش | - -##### 7.1.2. دریافت کاربران بر اساس نقش - -**Endpoint:** `GET /api/admin/roles/:roleName/users` - -**Path Parameters:** -- `roleName` (string, required): نام نقش - -**Response:** -```json -{ - "users": [ - { - "id": number, - "fullName": "string", - "username": "string", - "email": "string", - "avatar": "string", - "role": "string", - "status": "string" - } - ] -} -``` - -##### 7.1.3. ایجاد نقش جدید - -**Endpoint:** `POST /api/admin/roles` - -**Request Body:** -```json -{ - "name": "string", - "description": "string", - "permissions": ["string"] -} -``` - -**Response:** -```json -{ - "role": { - "id": "string", - "name": "string", - "description": "string", - "totalUsers": number, - "avatars": [], - "permissions": ["string"] - } -} -``` - -##### 7.1.4. به‌روزرسانی نقش - -**Endpoint:** `PUT /api/admin/roles/:roleId` - -**Path Parameters:** -- `roleId` (string, required): شناسه نقش - -**Request Body:** -```json -{ - "name": "string", - "description": "string", - "permissions": ["string"] -} -``` - -**Response:** -```json -{ - "role": { - "id": "string", - "name": "string", - "description": "string", - "totalUsers": number, - "avatars": ["string"], - "permissions": ["string"] - } -} -``` - -##### 7.1.5. حذف نقش - -**Endpoint:** `DELETE /api/admin/roles/:roleId` - -**Path Parameters:** -- `roleId` (string, required): شناسه نقش - -**Response:** -```json -{ - "success": boolean -} -``` - -### 7.2. Permissions - -**Route:** `/apps/permissions` - -#### API های مورد نیاز - -##### 7.2.1. دریافت لیست Permission ها - -**Endpoint:** `GET /api/admin/permissions` - -**Query Parameters:** -- `search?` (string, optional): جستجو در نام permission - -**Response:** -```json -{ - "permissions": [ - { - "id": number, - "name": "string", - "createdDate": "string", - "assignedTo": "string" | ["string"] - } - ], - "total": number -} -``` - -**توضیحات فیلدها:** - -| فیلد | نوع | توضیحات | -|------|-----|---------| -| `permissions[].id` | number | شناسه یکتا permission | -| `permissions[].name` | string | نام permission | -| `permissions[].createdDate` | string | تاریخ ایجاد (ISO 8601) | -| `permissions[].assignedTo` | string \| string[] | نقش یا نقش‌های اختصاص یافته | - -##### 7.2.2. ایجاد Permission جدید - -**Endpoint:** `POST /api/admin/permissions` - -**Request Body:** -```json -{ - "name": "string", - "assignedTo": "string" | ["string"] -} -``` - -**Response:** -```json -{ - "permission": { - "id": number, - "name": "string", - "createdDate": "string", - "assignedTo": "string" | ["string"] - } -} -``` - -##### 7.2.3. به‌روزرسانی Permission - -**Endpoint:** `PUT /api/admin/permissions/:permissionId` - -**Path Parameters:** -- `permissionId` (number, required): شناسه permission - -**Request Body:** -```json -{ - "name": "string", - "assignedTo": "string" | ["string"] -} -``` - -**Response:** -```json -{ - "permission": { - "id": number, - "name": "string", - "createdDate": "string", - "assignedTo": "string" | ["string"] - } -} -``` - -##### 7.2.4. حذف Permission - -**Endpoint:** `DELETE /api/admin/permissions/:permissionId` - -**Path Parameters:** -- `permissionId` (number, required): شناسه permission - -**Response:** -```json -{ - "success": boolean -} -``` - ---- - -## خلاصه نکات مهم - -1. **User Management & Roles & Permissions**: فقط برای ادمین قابل دسترسی هستند -2. **Calendar, Kanban, Todo**: این سه صفحه باید به یکدیگر متصل باشند و تسک‌هایی با `routine = 0` (NONE) در هر سه قابل نمایش باشند -3. **AI Chat**: باید به تسک‌های Calendar، Kanban، و Todo دسترسی داشته باشد و نسبت به برخی پیام‌ها حساس باشد -4. **Authentication**: سیستم Authentication جزو این مستندات نیست - ---- - -## Enum ها و Type های مشترک - -### Routine Enum -```typescript -enum Routine { - NONE = 0, - DAILY = 1, - WEEKLY = 2, - MONTHLY = 3, - YEARLY = 4 -} -``` - -### ThemeColor Type -```typescript -type ThemeColor = "primary" | "success" | "error" | "warning" | "info" -``` - -### Status Type (Chat) -```typescript -type StatusType = "busy" | "away" | "online" | "offline" -``` - -### Todo Status Type -```typescript -type TodoStatusType = "pending" | "in-progress" | "completed" -``` - -### Todo Priority Type -```typescript -type TodoPriorityType = "high" | "medium" | "low" -``` - -### Calendar Type -```typescript -type CalendarFiltersType = "Personal" | "Business" | "Family" | "Holiday" | "ETC" -``` - -### User Role Type -```typescript -type UserRoleType = "admin" | "author" | "editor" | "maintainer" | "subscriber" -``` - -### User Status Type -```typescript -type UserStatusType = "active" | "pending" | "inactive" -``` - ---- - -**تاریخ ایجاد مستندات:** 2024 -**نسخه:** 1.0.0 - diff --git a/declarations.d.ts b/declarations.d.ts index 8c9893e..7592645 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -1 +1,3 @@ declare module 'tailwindcss-logical' +declare module 'leaflet/dist/leaflet.css' +declare module 'leaflet-draw/dist/leaflet.draw.css' diff --git a/messages/fa.json b/messages/fa.json index 43d1292..b1ec7fd 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -33,6 +33,10 @@ }, "navigation": { "dashboards": "داشبوردها", + "farm": "داشبورد مزرعه", + "waterData": "دیتاهای آب", + "soilData": "اطلاعات خاک", + "dataSection": "بخش داده‌ها", "crm": "مدیریت ارتباط با مشتری", "analytics": "تحلیل‌ها", "eCommerce": "فروشگاه", @@ -267,6 +271,8 @@ "sensorUuid": "شناسه سنسور (UUID)", "placeholderName": "نام سنسور را وارد کنید", "placeholderUuid": "شناسه سنسور را وارد کنید", + "plantType": "نوع گیاه", + "plantName": "اسم گیاه", "saveSensor": "ذخیره سنسور", "saving": "در حال ذخیره...", "errorSave": "خطا در ذخیره سنسور", diff --git a/package-lock.json b/package-lock.json index 4d4b4e4..f15db1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,8 @@ "@tiptap/pm": "^2.10.4", "@tiptap/react": "^2.10.4", "@tiptap/starter-kit": "^2.10.4", + "@types/leaflet": "^1.9.21", + "@types/leaflet-draw": "^1.0.13", "apexcharts": "3.49.0", "bootstrap-icons": "1.11.3", "classnames": "2.5.1", @@ -51,6 +53,8 @@ "fs-extra": "11.2.0", "input-otp": "1.4.1", "keen-slider": "6.8.6", + "leaflet": "^1.9.4", + "leaflet-draw": "^1.0.4", "mapbox-gl": "3.9.0", "negotiator": "1.0.0", "next": "15.1.2", @@ -63,7 +67,9 @@ "react-dom": "18.3.1", "react-dropzone": "14.3.5", "react-hook-form": "7.54.1", + "react-leaflet": "^4.2.1", "react-map-gl": "7.1.8", + "react-multi-date-picker": "^4.5.2", "react-perfect-scrollbar": "1.5.8", "react-player": "2.16.0", "react-redux": "9.2.0", @@ -2552,6 +2558,17 @@ } } }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://mirror-npm.runflare.com/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.5.0", "resolved": "https://mirror-npm.runflare.com/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", @@ -3184,6 +3201,24 @@ "@types/node": "*" } }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://mirror-npm.runflare.com/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/leaflet-draw": { + "version": "1.0.13", + "resolved": "https://mirror-npm.runflare.com/@types/leaflet-draw/-/leaflet-draw-1.0.13.tgz", + "integrity": "sha512-YU82kilOaU+wPNbqKCCDfHH3hqepN6XilrBwG/mSeZ/z4ewumaRCOah44s3FMxSu/Aa0SVa3PPJvhIZDUA09mw==", + "license": "MIT", + "dependencies": { + "@types/leaflet": "^1.9" + } + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "license": "MIT" @@ -7154,6 +7189,18 @@ "node": ">=0.10" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://mirror-npm.runflare.com/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/leaflet-draw": { + "version": "1.0.4", + "resolved": "https://mirror-npm.runflare.com/leaflet-draw/-/leaflet-draw-1.0.4.tgz", + "integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "dev": true, @@ -8650,6 +8697,16 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-element-popper": { + "version": "2.1.7", + "resolved": "https://mirror-npm.runflare.com/react-element-popper/-/react-element-popper-2.1.7.tgz", + "integrity": "sha512-tuM2OxKlW32h+6uFSK6EENHPeZ2OGgOipHfOAl+VLWEv9/j3QkSGbD+ADX3A9uJlmq24i37n28RjJmAbGTfpEg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-fast-compare": { "version": "3.2.2", "license": "MIT" @@ -8672,6 +8729,20 @@ "version": "19.0.0", "license": "MIT" }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://mirror-npm.runflare.com/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-map-gl": { "version": "7.1.8", "license": "MIT", @@ -8694,6 +8765,26 @@ } } }, + "node_modules/react-multi-date-picker": { + "version": "4.5.2", + "resolved": "https://mirror-npm.runflare.com/react-multi-date-picker/-/react-multi-date-picker-4.5.2.tgz", + "integrity": "sha512-FgWjZB3Z6IA6XpcWiLPk85PwcRUhOiYhKK42o5k672gD/n2I6rzPfQ8bUrldOIiF/Z7FfOCdH7a6FeubzqteLg==", + "license": "MIT", + "dependencies": { + "react-date-object": "^2.1.8", + "react-element-popper": "^2.1.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-multi-date-picker/node_modules/react-date-object": { + "version": "2.1.9", + "resolved": "https://mirror-npm.runflare.com/react-date-object/-/react-date-object-2.1.9.tgz", + "integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==", + "license": "MIT" + }, "node_modules/react-onclickoutside": { "version": "6.13.1", "license": "MIT", diff --git a/package.json b/package.json index 0b0e53e..40c3459 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ "@tiptap/pm": "^2.10.4", "@tiptap/react": "^2.10.4", "@tiptap/starter-kit": "^2.10.4", + "@types/leaflet": "^1.9.21", + "@types/leaflet-draw": "^1.0.13", "apexcharts": "3.49.0", "bootstrap-icons": "1.11.3", "classnames": "2.5.1", @@ -56,6 +58,8 @@ "fs-extra": "11.2.0", "input-otp": "1.4.1", "keen-slider": "6.8.6", + "leaflet": "^1.9.4", + "leaflet-draw": "^1.0.4", "mapbox-gl": "3.9.0", "negotiator": "1.0.0", "next": "15.1.2", @@ -68,7 +72,9 @@ "react-dom": "18.3.1", "react-dropzone": "14.3.5", "react-hook-form": "7.54.1", + "react-leaflet": "^4.2.1", "react-map-gl": "7.1.8", + "react-multi-date-picker": "^4.5.2", "react-perfect-scrollbar": "1.5.8", "react-player": "2.16.0", "react-redux": "9.2.0", diff --git a/src/app/(dashboard)/(private)/dashboard/soil-data/page.tsx b/src/app/(dashboard)/(private)/dashboard/soil-data/page.tsx new file mode 100644 index 0000000..5de1076 --- /dev/null +++ b/src/app/(dashboard)/(private)/dashboard/soil-data/page.tsx @@ -0,0 +1,8 @@ +// Components Imports +import SoilDataDashboardWrapper from '@views/dashboards/farm/SoilDataDashboardWrapper' + +const SoilDataPage = async () => { + return +} + +export default SoilDataPage diff --git a/src/app/(dashboard)/(private)/dashboard/water-data/page.tsx b/src/app/(dashboard)/(private)/dashboard/water-data/page.tsx new file mode 100644 index 0000000..8d80a82 --- /dev/null +++ b/src/app/(dashboard)/(private)/dashboard/water-data/page.tsx @@ -0,0 +1,8 @@ +// Components Imports +import WaterDataDashboardWrapper from '@views/dashboards/farm/WaterDataDashboardWrapper' + +const WaterDataPage = async () => { + return +} + +export default WaterDataPage diff --git a/src/components/MapDraw/MapDraw.tsx b/src/components/MapDraw/MapDraw.tsx new file mode 100644 index 0000000..7ac438b --- /dev/null +++ b/src/components/MapDraw/MapDraw.tsx @@ -0,0 +1,172 @@ +'use client' + +import { useEffect, useRef } from 'react' +import type L from 'leaflet' +import 'leaflet/dist/leaflet.css' +import 'leaflet-draw/dist/leaflet.draw.css' + +export type MapDrawGeoJSON = Record | null + +type MapDrawProps = { + center?: [number, number] + zoom?: number + height?: string | number + onAreaChange?: (geojson: MapDrawGeoJSON, area?: number) => void + initialGeoJson?: MapDrawGeoJSON + singleShape?: boolean +} + +export default function MapDraw({ + center = [35.6892, 51.389], + zoom = 13, + height = '400px', + onAreaChange, + initialGeoJson, + singleShape = true +}: MapDrawProps) { + const mapRef = useRef(null) + const mapInstanceRef = useRef(null) + const drawnItemsRef = useRef(null) + const drawControlRef = useRef(null) + + useEffect(() => { + if (typeof window === 'undefined' || !mapRef.current) return + + let isMounted = true + let cleanupFn: (() => void) | null = null + + const initMap = async () => { + const L = (await import('leaflet')).default + await import('leaflet-draw') + + if (!isMounted || !mapRef.current || mapInstanceRef.current) return + + const map = L.map(mapRef.current).setView(center, zoom) + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap' + }).addTo(map) + + const drawnItems = L.featureGroup().addTo(map) + drawnItemsRef.current = drawnItems + + if (initialGeoJson && drawnItems) { + try { + const layer = L.geoJSON(initialGeoJson as never).eachLayer(l => { + drawnItems.addLayer(l) + }) + if (layer.getBounds && layer.getBounds().isValid()) { + map.fitBounds(layer.getBounds()) + } + } catch { + // ignore invalid geojson + } + } + + const drawControl = new L.Control.Draw({ + position: 'topright', + draw: { + polygon: { shapeOptions: { color: '#3388ff' } }, + rectangle: { shapeOptions: { color: '#3388ff' } }, + circle: false, + circlemarker: false, + marker: false, + polyline: false + }, + edit: { featureGroup: drawnItems, remove: true } + }) + + map.addControl(drawControl) + drawControlRef.current = drawControl + + const getGeoJsonFromDrawn = (): MapDrawGeoJSON => { + const geojson = drawnItems.toGeoJSON() + + if (geojson.type === 'FeatureCollection' && geojson.features.length > 0) { + return geojson.features[0] as unknown as MapDrawGeoJSON ?? null + } + + return null + } + + const getArea = (layer: L.Layer): number | undefined => { + const poly = layer as L.Polygon + if (poly.getLatLngs && typeof L.GeometryUtil?.geodesicArea === 'function') { + const latlngs = poly.getLatLngs() as L.LatLng[] + return L.GeometryUtil.geodesicArea(Array.isArray(latlngs[0]) ? latlngs[0] : latlngs) + } + + return undefined + } + + const emitChange = () => { + const geojson = getGeoJsonFromDrawn() + let area: number | undefined + + drawnItems.eachLayer(layer => { + const a = getArea(layer) + if (a !== undefined) area = (area ?? 0) + a + }) + + onAreaChange?.(geojson, area) + } + + const onCreated = (e: L.LeafletEvent) => { + const event = e as L.DrawEvents.Created + const layer = event.layer + + if (singleShape) { + drawnItems.clearLayers() + } + drawnItems.addLayer(layer) + emitChange() + } + + const onEdited = () => emitChange() + const onDeleted = () => emitChange() + + map.on(L.Draw.Event.CREATED, onCreated) + map.on(L.Draw.Event.EDITED, onEdited) + map.on(L.Draw.Event.DELETED, onDeleted) + + mapInstanceRef.current = map + + if (initialGeoJson && drawnItems.getLayers().length > 0) { + emitChange() + } + + cleanupFn = () => { + map.off(L.Draw.Event.CREATED, onCreated) + map.off(L.Draw.Event.EDITED, onEdited) + map.off(L.Draw.Event.DELETED, onDeleted) + map.removeControl(drawControl) + map.remove() + mapInstanceRef.current = null + drawnItemsRef.current = null + drawControlRef.current = null + } + } + + initMap() + + return () => { + isMounted = false + if (cleanupFn) { + cleanupFn() + } else if (mapInstanceRef.current) { + mapInstanceRef.current.remove() + mapInstanceRef.current = null + drawnItemsRef.current = null + drawControlRef.current = null + } + } + }, []) + + return ( +
+ ) +} diff --git a/src/components/MapDraw/index.ts b/src/components/MapDraw/index.ts new file mode 100644 index 0000000..a3dfcd1 --- /dev/null +++ b/src/components/MapDraw/index.ts @@ -0,0 +1,2 @@ +export { default as MapDraw } from './MapDraw' +export type { MapDrawGeoJSON } from './MapDraw' diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index e6bded9..d33b2ef 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -64,13 +64,13 @@ const VerticalMenu = ({ scrollMenu }: Props) => { scrollMenu(container, false), - } + className: "bs-full overflow-y-auto overflow-x-hidden", + onScroll: (container) => scrollMenu(container, false), + } : { - options: { wheelPropagation: false, suppressScrollX: true }, - onScrollY: (container) => scrollMenu(container, true), - })} + options: { wheelPropagation: false, suppressScrollX: true }, + onScrollY: (container) => scrollMenu(container, true), + })} > {/* Incase you also want to scroll NavHeader to scroll with Vertical Menu, remove NavHeader from above and paste it below this comment */} {/* Vertical Menu */} @@ -94,10 +94,16 @@ const VerticalMenu = ({ scrollMenu }: Props) => { > {t('dashboards')} + + }> + {t('waterData')} + + }> + {t('soilData')} + + - - {/* */} {/* + area_m2?: number +} + export const sensorHubService = { + /** + * Add a new sensor + */ + async addSensor(payload: AddSensorPayload): Promise { + const response = await apiClient.post<{ data?: Sensor }>('/api/sensor-hub/', payload) + + return (response?.data as Sensor) ?? (payload as unknown as Sensor) + }, + /** * Get list of sensors */ diff --git a/src/views/apps/calendar/CalendarWrapper.tsx b/src/views/apps/calendar/CalendarWrapper.tsx index 7ea05ab..07551d9 100644 --- a/src/views/apps/calendar/CalendarWrapper.tsx +++ b/src/views/apps/calendar/CalendarWrapper.tsx @@ -1,90 +1,58 @@ 'use client' -// React Imports -import { useState, useEffect } from 'react' - -// MUI Imports -import { useMediaQuery } from '@mui/material' -import type { Theme } from '@mui/material/styles' - -// Third-party Imports -import { useSelector } from 'react-redux' - -// Type Imports -import type { CalendarColors, CalendarType } from '@/types/apps/calendarTypes' - -// Redux Imports -import { useAppDispatch } from '@/redux-store' - -// Slice Imports -import { fetchEvents } from '@/redux-store/slices/calendar' - -// 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 = () => { - // States - const [calendarApi, setCalendarApi] = useState(null) - const [leftSidebarOpen, setLeftSidebarOpen] = useState(false) - const [addEventSidebarOpen, setAddEventSidebarOpen] = useState(false) + return <> + // // States + // const [calendarApi, setCalendarApi] = useState(null) + // const [leftSidebarOpen, setLeftSidebarOpen] = useState(false) + // const [addEventSidebarOpen, setAddEventSidebarOpen] = useState(false) - // Hooks - const dispatch = useAppDispatch() - const calendarStore = useSelector((state: { calendarReducer: CalendarType & { loading: boolean; error: string | null } }) => state.calendarReducer) - const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')) + // // Hooks + // const dispatch = useAppDispatch() + // const calendarStore = useSelector((state: { calendarReducer: CalendarType & { loading: boolean; error: string | null } }) => state.calendarReducer) + // const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')) - // Fetch events on mount - useEffect(() => { - dispatch(fetchEvents()) - }, [dispatch]) + // // Fetch events on mount + // useEffect(() => { + // dispatch(fetchEvents()) + // }, [dispatch]) - const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen) + // const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen) - const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen) + // const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen) - return ( - <> - -
- -
- - - ) + // return ( + // <> + // + //
+ // + //
+ // + // + // ) } export default AppCalendar diff --git a/src/views/apps/calendar/SidebarLeft.tsx b/src/views/apps/calendar/SidebarLeft.tsx index 8e2f0a7..5a31780 100644 --- a/src/views/apps/calendar/SidebarLeft.tsx +++ b/src/views/apps/calendar/SidebarLeft.tsx @@ -1,23 +1,6 @@ -// 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 AppJalaliDatepicker from '@/libs/styles/AppJalaliDatepicker' - -// Slice Imports -import { filterAllCalendarLabels, filterCalendarLabel, selectedEvent } from '@/redux-store/slices/calendar' const SidebarLeft = (props: SidebarLeftProps) => { // Props @@ -32,105 +15,205 @@ const SidebarLeft = (props: SidebarLeftProps) => { handleAddEventSidebarToggle } = props - // Vars - const colorsArr = calendarsColor ? Object.entries(calendarsColor) : [] + // // Vars + // const colorsArr = calendarsColor ? Object.entries(calendarsColor) : [] - const renderFilters = colorsArr.length - ? colorsArr.map(([key, value]: string[]) => { - return ( - -1} - onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))} - /> - } - /> - ) - }) - : null + // const renderFilters = colorsArr.length + // ? colorsArr.map(([key, value]: string[]) => { + // return ( + // -1} + // onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))} + // /> + // } + // /> + // ) + // }) + // : null - const handleSidebarToggleSidebar = () => { - dispatch(selectedEvent(null)) - handleAddEventSidebarToggle() - } + // const handleSidebarToggleSidebar = () => { + // dispatch(selectedEvent(null)) + // handleAddEventSidebarToggle() + // } - if (renderFilters) { - return ( - -
- -
- - calendarApi?.gotoDate(date)} - boxProps={{ - className: 'flex justify-center is-full', - sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } } - }} - /> - + // if (renderFilters) { + // return ( + // + //
+ // + //
+ // + // calendarApi?.gotoDate(date)} + // boxProps={{ + // className: 'flex justify-center is-full', + // sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } } + // }} + // /> + // -
- - Event Filters - - dispatch(filterAllCalendarLabels(e.target.checked))} - /> - } - /> - {renderFilters} -
-
- ) - } else { - return null - } + //
+ // + // Event Filters + // + // dispatch(filterAllCalendarLabels(e.target.checked))} + // /> + // } + // /> + // {renderFilters} + //
+ //
+ // ) + // } else { + // return null + // } + // // Vars + // const colorsArr = calendarsColor ? Object.entries(calendarsColor) : [] + + // const renderFilters = colorsArr.length + // ? colorsArr.map(([key, value]: string[]) => { + // return ( + // -1} + // onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))} + // /> + // } + // /> + // ) + // }) + // : null + + // const handleSidebarToggleSidebar = () => { + // dispatch(selectedEvent(null)) + // handleAddEventSidebarToggle() + // } + + // if (renderFilters) { + // return ( + // + //
+ // + //
+ // + // calendarApi?.gotoDate(date)} + // boxProps={{ + // className: 'flex justify-center is-full', + // sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } } + // }} + // /> + // + + //
+ // + // Event Filters + // + // dispatch(filterAllCalendarLabels(e.target.checked))} + // /> + // } + // /> + // {renderFilters} + //
+ //
+ // ) + // } else { + // return null + // } + return <> } export default SidebarLeft diff --git a/src/views/dashboards/farm/SoilDataDashboardWrapper.tsx b/src/views/dashboards/farm/SoilDataDashboardWrapper.tsx new file mode 100644 index 0000000..d3bb7a2 --- /dev/null +++ b/src/views/dashboards/farm/SoilDataDashboardWrapper.tsx @@ -0,0 +1,85 @@ +'use client' + +// React Imports +import { useEffect, useState } from 'react' + +// MUI Imports +import Grid from '@mui/material/Grid2' +import Box from '@mui/material/Box' +import CircularProgress from '@mui/material/CircularProgress' + +// Component Imports +import SoilMoistureHeatmap from '@views/dashboards/farm/SoilMoistureHeatmap' +import SensorValuesList from '@views/dashboards/farm/SensorValuesList' +import SensorComparisonChart from '@views/dashboards/farm/SensorComparisonChart' +import SensorRadarChart from '@views/dashboards/farm/SensorRadarChart' +import AnomalyDetectionCard from '@views/dashboards/farm/AnomalyDetectionCard' + +// Service +import { farmDashboardService } from '@/libs/api/services/farmDashboardService' +import type { CardId } from '@views/dashboards/farm/farmDashboardConfig' +import { CARD_GRID_SIZE } from '@views/dashboards/farm/farmDashboardConfig' + +const SOIL_CARD_IDS: CardId[] = [ + 'soilMoistureHeatmap', + 'sensorValuesList', + 'sensorRadarChart', + 'sensorComparisonChart', + 'anomalyDetectionCard' +] + +const cardRowSx = { + display: 'flex', + flexDirection: 'column', + '& > *': { flex: 1, minHeight: 0 } +} + +const CARD_COMPONENTS: Partial }>>> = { + soilMoistureHeatmap: SoilMoistureHeatmap, + sensorValuesList: SensorValuesList, + sensorComparisonChart: SensorComparisonChart, + sensorRadarChart: SensorRadarChart, + anomalyDetectionCard: AnomalyDetectionCard +} + +const SoilDataDashboardWrapper = () => { + const [cardsData, setCardsData] = useState>>>({}) + const [loading, setLoading] = useState(true) + + useEffect(() => { + farmDashboardService + .getAllCards() + .then(cards => setCardsData(cards ?? {})) + .catch(() => setCardsData({})) + .finally(() => setLoading(false)) + }, []) + + if (loading) { + return ( + + + + ) + } + + return ( + + + + {SOIL_CARD_IDS.map(cardId => { + const size = CARD_GRID_SIZE[cardId] + const Component = CARD_COMPONENTS[cardId] + if (!Component) return null + return ( + + + + ) + })} + + + + ) +} + +export default SoilDataDashboardWrapper diff --git a/src/views/dashboards/farm/WaterDataDashboardWrapper.tsx b/src/views/dashboards/farm/WaterDataDashboardWrapper.tsx new file mode 100644 index 0000000..f953973 --- /dev/null +++ b/src/views/dashboards/farm/WaterDataDashboardWrapper.tsx @@ -0,0 +1,82 @@ +'use client' + +// React Imports +import { useEffect, useState } from 'react' + +// MUI Imports +import Grid from '@mui/material/Grid2' +import Box from '@mui/material/Box' +import CircularProgress from '@mui/material/CircularProgress' + +// Component Imports +import FarmWeatherCard from '@views/dashboards/farm/FarmWeatherCard' +import FarmAlertsTimeline from '@views/dashboards/farm/FarmAlertsTimeline' +import WaterNeedPrediction from '@views/dashboards/farm/WaterNeedPrediction' +import SensorValuesList from '@views/dashboards/farm/SensorValuesList' + +// Service +import { farmDashboardService } from '@/libs/api/services/farmDashboardService' +import type { CardId } from '@views/dashboards/farm/farmDashboardConfig' +import { CARD_GRID_SIZE } from '@views/dashboards/farm/farmDashboardConfig' + +const WATER_CARD_IDS: CardId[] = [ + 'farmWeatherCard', + 'farmAlertsTimeline', + 'waterNeedPrediction', + 'sensorValuesList' +] + +const cardRowSx = { + display: 'flex', + flexDirection: 'column', + '& > *': { flex: 1, minHeight: 0 } +} + +const CARD_COMPONENTS: Partial }>>> = { + farmWeatherCard: FarmWeatherCard, + farmAlertsTimeline: FarmAlertsTimeline, + waterNeedPrediction: WaterNeedPrediction, + sensorValuesList: SensorValuesList +} + +const WaterDataDashboardWrapper = () => { + const [cardsData, setCardsData] = useState>>>({}) + const [loading, setLoading] = useState(true) + + useEffect(() => { + farmDashboardService + .getAllCards() + .then(cards => setCardsData(cards ?? {})) + .catch(() => setCardsData({})) + .finally(() => setLoading(false)) + }, []) + + if (loading) { + return ( + + + + ) + } + + return ( + + + + {WATER_CARD_IDS.map(cardId => { + const size = CARD_GRID_SIZE[cardId] + const Component = CARD_COMPONENTS[cardId] + if (!Component) return null + return ( + + + + ) + })} + + + + ) +} + +export default WaterDataDashboardWrapper diff --git a/src/views/sensorHub/FormSensorHub.tsx b/src/views/sensorHub/FormSensorHub.tsx index 4a5797d..d9b3090 100644 --- a/src/views/sensorHub/FormSensorHub.tsx +++ b/src/views/sensorHub/FormSensorHub.tsx @@ -1,7 +1,8 @@ 'use client' // React Imports -import { useState } from 'react' +import { useState, useCallback } from 'react' +import dynamic from 'next/dynamic' import { useTranslations } from 'next-intl' // MUI Imports @@ -9,30 +10,72 @@ import Grid from '@mui/material/Grid2' import Button from '@mui/material/Button' import CircularProgress from '@mui/material/CircularProgress' import Alert from '@mui/material/Alert' +import Typography from '@mui/material/Typography' // API Imports import { sensorHubService } from '@/libs/api' // Component Imports import CustomTextField from '@core/components/mui/TextField' +import CustomAutocomplete from '@core/components/mui/Autocomplete' +import type { MapDrawGeoJSON } from '@/components/MapDraw' + +const MapDraw = dynamic(() => import('@/components/MapDraw').then(mod => mod.MapDraw), { + ssr: false, + loading: () => ( +
+ +
+ ) +}) type FormSensorHubProps = { onBack: () => void } +const PLANT_TYPES = ['گندم', 'جو', 'ذرت', 'برنج', 'پنبه', 'چغندر قند', 'سیب‌زمینی', 'گوجه‌فرنگی', 'پیاز', 'سبزیجات'] + +const PLANT_NAMES_BY_TYPE: Record = { + گندم: ['رقم آذر', 'رقم شریف', 'رقم مروارید', 'رقم بهار', 'رقم الوند'], + جو: ['رقم سرداری', 'رقم زاگرس', 'رقم کرج', 'رقم ریجاب'], + ذرت: ['رقم سینگل کراس ۷۰۴', 'رقم سینگل کراس ۷۰۷', 'رقم ماکزیما'], + برنج: ['رقم فجر', 'رقم خزر', 'رقم طارم'], + پنبه: ['رقم ورامین', 'رقم ساحل', 'رقم سپید'], + 'چغندر قند': ['رقم اکباتان', 'رقم شیرین', 'رقم پاییزه'], + سیب‌زمینی: ['رقم آگریا', 'رقم مارفونا', 'رقم ساوالان'], + گوجه‌فرنگی: ['رقم چری', 'رقم روتگرز', 'رقم پامیس'], + پیاز: ['رقم قرمز آذرشهر', 'رقم سفید قم', 'رقم زرد'], + سبزیجات: ['کاهو', 'جعفری', 'شوید', 'تره', 'ریحان'] +} + const FormSensorHub = ({ onBack }: FormSensorHubProps) => { const t = useTranslations('sensorHub') const [name, setName] = useState('') const [uuidSensor, setUuidSensor] = useState('') + const [plantType, setPlantType] = useState(null) + const [plantName, setPlantName] = useState(null) + const [areaGeoJson, setAreaGeoJson] = useState(null) + const [areaM2, setAreaM2] = useState(undefined) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const handleAreaChange = useCallback((geojson: MapDrawGeoJSON, area?: number) => { + setAreaGeoJson(geojson) + setAreaM2(area) + }, []) + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError(null) setLoading(true) + console.log(areaGeoJson) try { - await sensorHubService.addSensor({ name, uuid_sensor: uuidSensor }) + await sensorHubService.addSensor({ + name, + uuid_sensor: uuidSensor, + ...(areaGeoJson && { area_geojson: areaGeoJson }), + ...(areaM2 !== undefined && { area_m2: areaM2 }) + }) onBack() } catch (err: unknown) { const message = err && typeof err === 'object' && 'message' in err ? String((err as { message: string }).message) : t('errorSave') @@ -83,6 +126,45 @@ const FormSensorHub = ({ onBack }: FormSensorHubProps) => { onChange={e => setUuidSensor(e.target.value)} /> + + { + setPlantType(v) + setPlantName(null) + }} + renderInput={params => } + /> + + + setPlantName(v)} + renderInput={params => } + /> + + + + {t('mapAreaDescription')} + + + {areaM2 !== undefined && areaM2 > 0 && ( + + مساحت تقریبی: {areaM2 >= 10000 ? `${(areaM2 / 10000).toFixed(2)} هکتار` : `${areaM2.toFixed(0)} متر مربع`} + + )} +