From 890599b0e7a56353c23c7b1bd6a1d97a53f0caa0 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Fri, 20 Feb 2026 20:24:24 +0330 Subject: [PATCH] Remove API documentation file and update navigation constants with new entries for farm dashboard, water data, soil data, and data section. Enhance sensor hub functionality by adding new sensor payload structure and integrating plant type and name selection in the sensor form. Refactor calendar components to streamline code and improve maintainability. --- API_DOCUMENTATION.md | 1724 ----------------- declarations.d.ts | 2 + messages/fa.json | 6 + package-lock.json | 91 + package.json | 6 + .../(private)/dashboard/soil-data/page.tsx | 8 + .../(private)/dashboard/water-data/page.tsx | 8 + src/components/MapDraw/MapDraw.tsx | 172 ++ src/components/MapDraw/index.ts | 2 + .../layout/vertical/VerticalMenu.tsx | 22 +- src/constants/navigation.ts | 4 + src/libs/api/services/sensorHubService.ts | 16 + src/views/apps/calendar/CalendarWrapper.tsx | 126 +- src/views/apps/calendar/SidebarLeft.tsx | 307 +-- .../farm/SoilDataDashboardWrapper.tsx | 85 + .../farm/WaterDataDashboardWrapper.tsx | 82 + src/views/sensorHub/FormSensorHub.tsx | 86 +- 17 files changed, 822 insertions(+), 1925 deletions(-) delete mode 100644 API_DOCUMENTATION.md create mode 100644 src/app/(dashboard)/(private)/dashboard/soil-data/page.tsx create mode 100644 src/app/(dashboard)/(private)/dashboard/water-data/page.tsx create mode 100644 src/components/MapDraw/MapDraw.tsx create mode 100644 src/components/MapDraw/index.ts create mode 100644 src/views/dashboards/farm/SoilDataDashboardWrapper.tsx create mode 100644 src/views/dashboards/farm/WaterDataDashboardWrapper.tsx 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)} متر مربع`} + + )} +