diff --git a/.cursor/skills/danak-admin/SKILL.md b/.cursor/skills/danak-admin/SKILL.md
new file mode 100644
index 0000000..6a52769
--- /dev/null
+++ b/.cursor/skills/danak-admin/SKILL.md
@@ -0,0 +1,109 @@
+---
+name: danak-admin
+description: Documents danak-admin React admin panel conventions — page structure, routing, TanStack Query hooks, axios services, sidebar, and Persian i18n. Use when adding pages or features, wiring API calls, updating navigation, or working in this repository.
+---
+
+# Danak Admin
+
+Persian (RTL) admin panel: React 19, Vite, TypeScript, React Router 7, TanStack Query, Formik/Yup, Tailwind, iconsax-react.
+
+## Stack
+
+| Layer | Location / tool |
+|-------|-----------------|
+| Routes (paths) | `src/config/Pages.ts` |
+| Route components | `src/router/Main.tsx` |
+| Auth routes | `src/router/Auth.tsx` |
+| HTTP client | `src/config/axios.ts` — `import axios from "../../../config/axios"` |
+| Shared UI | `src/components/` |
+| Layout | `src/shared/` (Header, SideBar, Footer) |
+| i18n | `src/langs/fa.json`, namespace `global`, `useTranslation('global')` |
+| Env | `VITE_BASE_URL`, `VITE_TOKEN_NAME`, `VITE_REFRESH_TOKEN_NAME` |
+
+## Feature folder layout
+
+Each domain lives under `src/pages/{feature}/`:
+
+```
+{feature}/
+├── List.tsx # index table view
+├── Create.tsx # create form
+├── Update.tsx # edit form (uses :id param)
+├── hooks/use{Feature}Data.ts
+├── service/{Feature}Service.ts
+├── types/{Feature}Types.ts
+└── components/ # feature-specific UI (Delete, filters, etc.)
+```
+
+Mirror an existing feature (e.g. `customer/`, `ads/`) before inventing new patterns.
+
+## Data layer
+
+**Service** — plain async functions, one API call each:
+
+```typescript
+import axios from "../../../config/axios";
+import { CreateCustomerType } from "../types/CustomerTypes";
+
+export const getCustomers = async (page: number) => {
+ const { data } = await axios.get(`/users/customers?page=${page}&paginate=1`);
+ return data;
+};
+
+export const createCustomer = async (params: CreateCustomerType) => {
+ const { data } = await axios.post(`/users/customers`, params);
+ return data;
+};
+```
+
+**Hooks** — TanStack Query wrappers in `hooks/use{Feature}Data.ts`:
+
+- Lists: `useQuery({ queryKey: ["customers", page], queryFn: () => api.getCustomers(page) })`
+- Detail: add `enabled: !!id`
+- Mutations: `useMutation` + `queryClient.invalidateQueries({ queryKey: ["customers"] })`
+- Export hooks as `useGetX`, `useCreateX`, `useUpdateX`, `useDeleteX`
+
+**Types** — `{Feature}ItemType`, `Create{Feature}Type` in `types/`.
+
+## Page patterns
+
+**List** — header + optional filters + `PageLoading` while pending + `
` with `Td` + `Pagination`. Link actions via `Pages.{feature}.*`.
+
+**Create / Update** — Formik + Yup validation, `toast.success(t('success'))` on success, `toast.error(error.response?.data?.error.message[0])` on error (`ErrorType` from `src/helpers/types.ts`). Navigate back to list with `useNavigate()` + `Pages.*`.
+
+**Delete** — small component using `ModalConfrim` + mutation hook; call `refetch()` or rely on query invalidation.
+
+## Adding a feature
+
+Follow the checklist in [add-feature.md](add-feature.md). Minimum steps:
+
+1. Add paths to `src/config/Pages.ts`
+2. Create page files under `src/pages/{feature}/`
+3. Register `` entries in `src/router/Main.tsx` (static imports, no lazy loading)
+4. Add sidebar entry in `src/shared/SideBar.tsx` if user-facing
+5. Add translation keys to `src/langs/fa.json`
+
+Dynamic routes use string concat: `Pages.customer.update + ":id"`.
+
+## Sidebar & submenu
+
+- Main items: `SideBarItem` in `SideBar.tsx` with `link`, `isActive`, `activeName`
+- Features with sub-nav: add segment to `SideBarItemHasSubMenu` in `src/config/SideBarSubMenu.ts`, create `{Feature}SubMenu.tsx` in `src/shared/components/`, render it in the `subMenuName === "..."` block at bottom of `SideBar.tsx`
+- Submenu items use `SubMenuItem` with `Pages.*` links
+- `subMenuName` is derived from first URL segment (`location.pathname.split("/")[1]`)
+
+## Conventions
+
+- Components: `const MyPage: FC = () => { ... }; export default MyPage`
+- Styling: Tailwind utility classes; `clx()` from `src/helpers/utils` for conditional classes
+- Icons: `iconsax-react` (Outline/Bold variants, `#8C90A3` inactive / `black` active)
+- Do not add lazy-loaded routes — project uses direct imports in `Main.tsx`
+- Keep API paths relative to `VITE_BASE_URL`; auth token is attached automatically by axios interceptor
+- Prefer reusing `Button`, `Input`, `Select`, `Textarea`, `Pagination`, `PageLoading`, `ModalConfrim` from `src/components/`
+
+## Do not
+
+- Commit `.env` or secrets
+- Introduce new state libraries (Zustand is only used in `sharedStore` for layout)
+- Add English-only UI strings — add keys to `fa.json`
+- Change axios/auth/token refresh logic unless explicitly requested
diff --git a/.cursor/skills/danak-admin/add-feature.md b/.cursor/skills/danak-admin/add-feature.md
new file mode 100644
index 0000000..2501acd
--- /dev/null
+++ b/.cursor/skills/danak-admin/add-feature.md
@@ -0,0 +1,83 @@
+# Add Feature Checklist
+
+Copy and track when adding a new admin section (e.g. `coupons`):
+
+```
+- [ ] Pages.ts paths
+- [ ] types/{Feature}Types.ts
+- [ ] service/{Feature}Service.ts
+- [ ] hooks/use{Feature}Data.ts
+- [ ] List.tsx (+ Create/Update if CRUD)
+- [ ] components/ (Delete, filters, etc.)
+- [ ] Main.tsx routes + imports
+- [ ] SideBar.tsx (+ SubMenu if multi-page)
+- [ ] fa.json translations
+```
+
+## 1. Pages.ts
+
+```typescript
+coupons: {
+ list: "/coupons/list",
+ create: "/coupons/create",
+ detail: "/coupons/detail/",
+},
+```
+
+Use kebab-case URL segments consistent with existing routes.
+
+## 2. Service + hooks + types
+
+Copy structure from `src/pages/customer/` or `src/pages/ads/`. Adjust API paths to match backend.
+
+## 3. Main.tsx
+
+Add import at top, then route(s):
+
+```tsx
+import CouponsList from "../pages/coupons/List";
+// ...
+} />
+} />
+} />
+```
+
+## 4. Sidebar (optional)
+
+**Single-page feature** — add `SideBarItem`:
+
+```tsx
+}
+ title={t("sidebar.coupons")}
+ isActive={isActive("coupons")}
+ link={Pages.coupons.list}
+ activeName="coupons"
+/>
+```
+
+**Multi-page feature** — also:
+
+1. Add `"coupons"` to `SideBarItemHasSubMenu` in `src/config/SideBarSubMenu.ts`
+2. Create `src/shared/components/CouponsSubMenu.tsx` (copy `CustomerSubMenu.tsx`)
+3. Import and add branch in `SideBar.tsx`: `subMenuName === "coupons" ? : ...`
+4. Point main sidebar link to list path; first URL segment must match `activeName` / submenu key
+
+## 5. Translations
+
+Add keys under logical groups in `src/langs/fa.json`:
+
+```json
+"sidebar": { "coupons": "کوپنها" },
+"submenu": { "coupon_list": "لیست کوپنها" },
+"coupon": { "coupon_list": "لیست کوپنها", "add_coupon": "افزودن کوپن" }
+```
+
+Use `t('coupon.add_coupon')` in components.
+
+## 6. Verify
+
+- `npm run build` passes
+- List loads, create/update navigate correctly
+- Sidebar highlights on correct routes
+- Mutation invalidates list query cache
diff --git a/src/config/SideBarSubMenu.ts b/src/config/SideBarSubMenu.ts
index f734f50..bcdeacf 100644
--- a/src/config/SideBarSubMenu.ts
+++ b/src/config/SideBarSubMenu.ts
@@ -8,4 +8,5 @@ export const SideBarItemHasSubMenu = [
"blog",
"support",
"accessLogs",
+ "taskmanager",
];
diff --git a/src/langs/fa.json b/src/langs/fa.json
index 53a0510..b12c21a 100644
--- a/src/langs/fa.json
+++ b/src/langs/fa.json
@@ -143,7 +143,11 @@
"guide": "راهنمای سرویس",
"access_logs_list": "لیست لاگها",
"access_logs_stats": "آمار لاگها",
- "access_logs_errors": "لاگهای خطا"
+ "access_logs_errors": "لاگهای خطا",
+ "taskmanager_workspace": "فضای کاری",
+ "taskmanager_create_workspace": "ساخت فضای کاری جدید",
+ "taskmanager_project_list": "لیست پروژهها",
+ "taskmanager_create_project": "افزودن پروژه"
},
"guide": {
"list_guide": "لیست راهنمای سرویس",
@@ -972,6 +976,7 @@
"submit_workspace": "ثبت فضای کاری",
"workspacename": "نام",
"workspacetype": "نوع",
+ "workspace_info": "اطلاعات",
"workspace_description": "توضیحات",
"workspace_status": "وضعیت فضای کار",
"choose_color": "انتخاب رنگ دلخواه",
diff --git a/src/pages/taskmanager/workspace/Create.tsx b/src/pages/taskmanager/workspace/Create.tsx
index 28f1198..f3261b4 100644
--- a/src/pages/taskmanager/workspace/Create.tsx
+++ b/src/pages/taskmanager/workspace/Create.tsx
@@ -1,20 +1,71 @@
+import { FC, useState } from "react";
import { TickCircle } from "iconsax-react";
+import { useFormik } from "formik";
import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import { toast } from "react-toastify";
+import * as Yup from "yup";
import Button from "../../../components/Button";
import Input from "../../../components/Input";
-import Select from "../../../components/Select";
import Textarea from "../../../components/Textarea";
-import CreateWorkspaceSidebar from "./components/CreateWorkspaceSidebar";
+import { Pages } from "../../../config/Pages";
+import { ErrorType } from "../../../helpers/types";
+import { useCreateWorkspace } from "./hooks/useWorkspaceData";
+import { CreateWorkspaceType } from "./types/WorkspaceTypes";
+import CreateWorkspaceSidebar, {
+ SelectedUser,
+} from "./components/CreateWorkspaceSidebar";
-const CreateWorkspace = () => {
+const CreateWorkspace: FC = () => {
const { t } = useTranslation("global");
+ const navigate = useNavigate();
+ const createWorkspace = useCreateWorkspace();
+
+ const [isActive, setIsActive] = useState(true);
+ const [selectedColor, setSelectedColor] = useState("#A8E6CF");
+ const [selectedUsers, setSelectedUsers] = useState([]);
+
+ const handleCreateWorkspace = (values: CreateWorkspaceType) => {
+ createWorkspace.mutate(values, {
+ onSuccess: () => {
+ toast.success(t("success"));
+ navigate(Pages.taskmanager.workspace + "dpage");
+ },
+ onError: (error: ErrorType) => {
+ toast.error(error.response?.data?.error.message[0]);
+ },
+ });
+ };
+
+ const formik = useFormik>({
+ initialValues: {
+ name: "",
+ description: "",
+ },
+ validationSchema: Yup.object({
+ name: Yup.string().required(t("errors.required")),
+ description: Yup.string(),
+ }),
+ onSubmit: (values) => {
+ handleCreateWorkspace({
+ ...values,
+ color: selectedColor,
+ isActive,
+ userIds: selectedUsers.map((user) => user.id),
+ });
+ },
+ });
return (
{t("taskmanager.new_workspace")}
-
@@ -69,7 +81,7 @@ const CreateWorkspaceSidebar: FC = () => {
diff --git a/src/pages/taskmanager/workspace/hooks/useWorkspaceData.ts b/src/pages/taskmanager/workspace/hooks/useWorkspaceData.ts
new file mode 100644
index 0000000..44df8ab
--- /dev/null
+++ b/src/pages/taskmanager/workspace/hooks/useWorkspaceData.ts
@@ -0,0 +1,16 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import * as api from "../service/WorkspaceService";
+import { CreateWorkspaceType } from "../types/WorkspaceTypes";
+
+export const useCreateWorkspace = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (variables: CreateWorkspaceType) => api.createWorkspace(variables),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: ["workspaces"],
+ });
+ },
+ });
+};
diff --git a/src/pages/taskmanager/workspace/service/WorkspaceService.ts b/src/pages/taskmanager/workspace/service/WorkspaceService.ts
new file mode 100644
index 0000000..5529c62
--- /dev/null
+++ b/src/pages/taskmanager/workspace/service/WorkspaceService.ts
@@ -0,0 +1,7 @@
+import axios from "../../../../config/axios";
+import { CreateWorkspaceType } from "../types/WorkspaceTypes";
+
+export const createWorkspace = async (params: CreateWorkspaceType) => {
+ const { data } = await axios.post(`/task-manager/workspaces`, params);
+ return data;
+};
diff --git a/src/pages/taskmanager/workspace/types/WorkspaceTypes.ts b/src/pages/taskmanager/workspace/types/WorkspaceTypes.ts
new file mode 100644
index 0000000..4c790e7
--- /dev/null
+++ b/src/pages/taskmanager/workspace/types/WorkspaceTypes.ts
@@ -0,0 +1,7 @@
+export type CreateWorkspaceType = {
+ name: string;
+ description: string;
+ color: string;
+ isActive: boolean;
+ userIds: string[];
+};
diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx
index bcffa25..a1b8225 100644
--- a/src/shared/SideBar.tsx
+++ b/src/shared/SideBar.tsx
@@ -1,5 +1,6 @@
import {
Activity,
+ Add,
ArrowDown2,
BagTick,
Building,
@@ -51,6 +52,7 @@ import LearningSubMenu from "./components/LearningSubMenu";
import ReceiptSubMenu from "./components/ReceiptSubMenu";
import ServicesSubMenu from "./components/ServicesSubMenu";
import SupportSubMenu from "./components/SupportSubMenu";
+import TaskManagerSubMenu from "./components/TaskManagerSubMenu";
import TicketSubMenu from "./components/TicketSubMenu";
import UsersSubMenu from "./components/UsersSubMenu";
import SideBarItem from "./SideBarItem";
@@ -69,10 +71,18 @@ const SideBar: FC = () => {
useEffect(() => {
const split = location.pathname.split("/");
+ const isTaskManagerRoute =
+ location.pathname.startsWith(Pages.taskmanager.workspace) ||
+ location.pathname === Pages.taskmanager.createWorkspace ||
+ location.pathname === Pages.taskmanager.createProject ||
+ location.pathname === Pages.taskmanager.projectList;
if (SideBarItemHasSubMenu.includes(split[1])) {
setSubMenuName(split[1]);
setSubtMenu(true);
+ } else if (isTaskManagerRoute) {
+ setSubMenuName("taskmanager");
+ setSubtMenu(true);
} else {
setSubMenuName("");
setSubtMenu(false);
@@ -459,18 +469,64 @@ const SideBar: FC = () => {
}
-
+
}
- title={t("sidebar.taskmanager")}
- isActive={isActive("taskmanager")}
+ title={t("submenu.taskmanager_workspace")}
+ isActive={location.pathname.startsWith(Pages.taskmanager.workspace)}
link={Pages.taskmanager.workspace + "dpage"}
+ name="taskmanager"
+ activeName=""
+ />
+
+
+ }
+ title={t("submenu.taskmanager_create_workspace")}
+ isActive={location.pathname === Pages.taskmanager.createWorkspace}
+ link={Pages.taskmanager.createWorkspace}
+ name="taskmanager"
+ activeName=""
+ />
+
+
+ }
+ title={t("submenu.taskmanager_project_list")}
+ isActive={location.pathname === Pages.taskmanager.projectList}
+ link={Pages.taskmanager.projectList}
+ name="taskmanager"
+ activeName=""
+ />
+
+
+ }
+ title={t("submenu.taskmanager_create_project")}
+ isActive={location.pathname === Pages.taskmanager.createProject}
+ link={Pages.taskmanager.createProject}
+ name="taskmanager"
activeName=""
/>
@@ -948,6 +1004,8 @@ const SideBar: FC = () => {
) : subMenuName === "accessLogs" ? (
+ ) : subMenuName === "taskmanager" ? (
+
) : null}
)}
diff --git a/src/shared/components/TaskManagerSubMenu.tsx b/src/shared/components/TaskManagerSubMenu.tsx
new file mode 100644
index 0000000..37eac21
--- /dev/null
+++ b/src/shared/components/TaskManagerSubMenu.tsx
@@ -0,0 +1,50 @@
+import { Add, Category, Task } from "iconsax-react";
+import { FC } from "react";
+import { useTranslation } from "react-i18next";
+import { useLocation } from "react-router-dom";
+import { Pages } from "../../config/Pages";
+import SubMenuItem from "./SubMenuItem";
+
+const TaskManagerSubMenu: FC = () => {
+ const { t } = useTranslation("global");
+ const location = useLocation();
+
+ const isWorkspaceActive = location.pathname.startsWith(Pages.taskmanager.workspace);
+ const isCreateWorkspaceActive = location.pathname === Pages.taskmanager.createWorkspace;
+ const isProjectListActive = location.pathname === Pages.taskmanager.projectList;
+ const isCreateProjectActive = location.pathname === Pages.taskmanager.createProject;
+
+ return (
+
+
+
+
{t("sidebar.taskmanager")}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default TaskManagerSubMenu;