diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..07d588b --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,125 @@ +{ + "extends": "next/core-web-vitals", + "overrides": [ + { + "files": ["app/accounting/**/*", "components/accounting/**/*"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "@/components/beauty/**", + "@/components/healthcare/**", + "@/modules/beauty/**", + "@/modules/healthcare/**" + ], + "message": "Accounting must not import beauty or healthcare modules." + } + ] + } + ] + } + }, + { + "files": ["app/beauty/**/*", "modules/beauty/**/*"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "@/components/accounting/**", + "@/components/healthcare/**", + "@/modules/accounting/**", + "@/modules/healthcare/**" + ], + "message": "Beauty must not import accounting or healthcare modules." + } + ] + } + ] + } + }, + { + "files": ["app/healthcare/**/*", "src/modules/healthcare/**/*"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "@/components/accounting/**", + "@/components/beauty/**", + "@/modules/beauty/**", + "@/modules/accounting/**", + "@/src/modules/beauty/**" + ], + "message": "Healthcare must not import accounting or beauty modules." + } + ] + } + ] + } + }, + { + "files": ["app/crm/**/*", "modules/crm/**/*"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "@/components/accounting/**", + "@/components/beauty/**", + "@/components/healthcare/**", + "@/modules/beauty/**", + "@/modules/healthcare/**", + "@/modules/accounting/**" + ], + "message": "CRM must not import accounting, beauty, or healthcare modules." + } + ] + } + ] + } + }, + { + "files": [ + "components/ds/**/*", + "shared/**/*", + "components/providers/**/*", + "components/AuthGuard.tsx", + "components/AdminAuthGuard.tsx", + "components/ThemeProvider.tsx", + "components/SiteHeader.tsx", + "hooks/**/*" + ], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "@/components/accounting/**", + "@/components/beauty/**", + "@/components/healthcare/**", + "@/modules/beauty/**", + "@/modules/healthcare/**", + "@/modules/accounting/**", + "@/modules/crm/**" + ], + "message": "Shared/platform code must not import business module code." + } + ] + } + ] + } + } + ] +} diff --git a/frontend/app/api/crm/[...path]/route.ts b/frontend/app/api/crm/[...path]/route.ts new file mode 100644 index 0000000..7b237c5 --- /dev/null +++ b/frontend/app/api/crm/[...path]/route.ts @@ -0,0 +1,96 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** + * Same-origin BFF proxy → CRM service. + * Browser calls /api/crm/* so TLS/CORS/DNS issues + * do not block CRUD from the SuperApp origin. + */ +const UPSTREAM = + process.env.CRM_SERVICE_URL || + process.env.NEXT_PUBLIC_CRM_API_URL || + "http://localhost:8003"; + +async function proxy(req: NextRequest, pathSegments: string[]) { + const path = pathSegments.join("/"); + const url = new URL(req.url); + const target = `${UPSTREAM.replace(/\/$/, "")}/${path}${url.search}`; + + const headers = new Headers(); + const pass = ["authorization", "content-type", "x-tenant-id", "accept"]; + for (const h of pass) { + const v = req.headers.get(h); + if (v) headers.set(h, v); + } + + let body: ArrayBuffer | undefined; + if (req.method !== "GET" && req.method !== "HEAD") { + body = await req.arrayBuffer(); + } + + try { + const res = await fetch(target, { + method: req.method, + headers, + body, + cache: "no-store", + }); + const outHeaders = new Headers(); + const ct = res.headers.get("content-type"); + if (ct) outHeaders.set("content-type", ct); + return new NextResponse(await res.arrayBuffer(), { + status: res.status, + headers: outHeaders, + }); + } catch (err) { + const detail = err instanceof Error ? err.message : "upstream_unreachable"; + return NextResponse.json( + { + error: { + code: "crm_proxy_error", + message: `پروکسی CRM به ${UPSTREAM} وصل نشد — ${detail}`, + }, + }, + { status: 502 } + ); + } +} + +export async function GET( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> } +) { + const { path } = await ctx.params; + return proxy(req, path); +} + +export async function POST( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> } +) { + const { path } = await ctx.params; + return proxy(req, path); +} + +export async function PATCH( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> } +) { + const { path } = await ctx.params; + return proxy(req, path); +} + +export async function PUT( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> } +) { + const { path } = await ctx.params; + return proxy(req, path); +} + +export async function DELETE( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> } +) { + const { path } = await ctx.params; + return proxy(req, path); +} diff --git a/frontend/app/crm/hub/error.tsx b/frontend/app/crm/hub/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/hub/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/hub/loading.tsx b/frontend/app/crm/hub/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/hub/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/hub/page.tsx b/frontend/app/crm/hub/page.tsx new file mode 100644 index 0000000..7625fa6 --- /dev/null +++ b/frontend/app/crm/hub/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { CrmHub as default } from "@/modules/crm/pages/hub"; diff --git a/frontend/app/crm/layout.tsx b/frontend/app/crm/layout.tsx new file mode 100644 index 0000000..88a8ba4 --- /dev/null +++ b/frontend/app/crm/layout.tsx @@ -0,0 +1,4 @@ +/** CRM root — sub-routes supply portal shells. */ +export default function CrmRootLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/frontend/app/crm/not-found.tsx b/frontend/app/crm/not-found.tsx new file mode 100644 index 0000000..c902e23 --- /dev/null +++ b/frontend/app/crm/not-found.tsx @@ -0,0 +1,13 @@ +import Link from "next/link"; +import { Button, EmptyState } from "@/components/ds"; + +export default function CrmNotFound() { + return ( +
+ } + /> +
+ ); +} diff --git a/frontend/app/crm/sales/activities/error.tsx b/frontend/app/crm/sales/activities/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/activities/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/activities/loading.tsx b/frontend/app/crm/sales/activities/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/activities/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/activities/page.tsx b/frontend/app/crm/sales/activities/page.tsx new file mode 100644 index 0000000..e1e3f83 --- /dev/null +++ b/frontend/app/crm/sales/activities/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/activities"; diff --git a/frontend/app/crm/sales/analytics/error.tsx b/frontend/app/crm/sales/analytics/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/analytics/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/analytics/loading.tsx b/frontend/app/crm/sales/analytics/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/analytics/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/analytics/page.tsx b/frontend/app/crm/sales/analytics/page.tsx new file mode 100644 index 0000000..0ed9829 --- /dev/null +++ b/frontend/app/crm/sales/analytics/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/analytics"; diff --git a/frontend/app/crm/sales/api-keys/error.tsx b/frontend/app/crm/sales/api-keys/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/api-keys/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/api-keys/loading.tsx b/frontend/app/crm/sales/api-keys/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/api-keys/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/api-keys/page.tsx b/frontend/app/crm/sales/api-keys/page.tsx new file mode 100644 index 0000000..7f6eb80 --- /dev/null +++ b/frontend/app/crm/sales/api-keys/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { ApiKeysPage as default } from "@/modules/crm/features/sales/scoped"; diff --git a/frontend/app/crm/sales/audit-logs/error.tsx b/frontend/app/crm/sales/audit-logs/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/audit-logs/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/audit-logs/loading.tsx b/frontend/app/crm/sales/audit-logs/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/audit-logs/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/audit-logs/page.tsx b/frontend/app/crm/sales/audit-logs/page.tsx new file mode 100644 index 0000000..7a5487f --- /dev/null +++ b/frontend/app/crm/sales/audit-logs/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { AuditLogsPage as default } from "@/modules/crm/features/sales/admin"; diff --git a/frontend/app/crm/sales/automation/error.tsx b/frontend/app/crm/sales/automation/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/automation/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/automation/loading.tsx b/frontend/app/crm/sales/automation/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/automation/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/automation/page.tsx b/frontend/app/crm/sales/automation/page.tsx new file mode 100644 index 0000000..53984be --- /dev/null +++ b/frontend/app/crm/sales/automation/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { AutomationPage as default } from "@/modules/crm/features/sales/scoped"; diff --git a/frontend/app/crm/sales/calls/error.tsx b/frontend/app/crm/sales/calls/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/calls/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/calls/loading.tsx b/frontend/app/crm/sales/calls/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/calls/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/calls/page.tsx b/frontend/app/crm/sales/calls/page.tsx new file mode 100644 index 0000000..71514d8 --- /dev/null +++ b/frontend/app/crm/sales/calls/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/calls"; diff --git a/frontend/app/crm/sales/campaigns/error.tsx b/frontend/app/crm/sales/campaigns/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/campaigns/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/campaigns/loading.tsx b/frontend/app/crm/sales/campaigns/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/campaigns/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/campaigns/page.tsx b/frontend/app/crm/sales/campaigns/page.tsx new file mode 100644 index 0000000..a05ef09 --- /dev/null +++ b/frontend/app/crm/sales/campaigns/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { CampaignsPage as default } from "@/modules/crm/features/sales/scoped"; diff --git a/frontend/app/crm/sales/capabilities/error.tsx b/frontend/app/crm/sales/capabilities/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/capabilities/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/capabilities/loading.tsx b/frontend/app/crm/sales/capabilities/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/capabilities/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/capabilities/page.tsx b/frontend/app/crm/sales/capabilities/page.tsx new file mode 100644 index 0000000..dd04ee2 --- /dev/null +++ b/frontend/app/crm/sales/capabilities/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { CapabilitiesPage as default } from "@/modules/crm/features/sales/admin"; diff --git a/frontend/app/crm/sales/companies/error.tsx b/frontend/app/crm/sales/companies/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/companies/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/companies/loading.tsx b/frontend/app/crm/sales/companies/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/companies/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/companies/page.tsx b/frontend/app/crm/sales/companies/page.tsx new file mode 100644 index 0000000..8c85e04 --- /dev/null +++ b/frontend/app/crm/sales/companies/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/companies"; diff --git a/frontend/app/crm/sales/contacts/error.tsx b/frontend/app/crm/sales/contacts/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/contacts/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/contacts/loading.tsx b/frontend/app/crm/sales/contacts/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/contacts/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/contacts/page.tsx b/frontend/app/crm/sales/contacts/page.tsx new file mode 100644 index 0000000..16e55a6 --- /dev/null +++ b/frontend/app/crm/sales/contacts/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/contacts"; diff --git a/frontend/app/crm/sales/custom-fields/error.tsx b/frontend/app/crm/sales/custom-fields/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/custom-fields/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/custom-fields/loading.tsx b/frontend/app/crm/sales/custom-fields/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/custom-fields/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/custom-fields/page.tsx b/frontend/app/crm/sales/custom-fields/page.tsx new file mode 100644 index 0000000..7c0706b --- /dev/null +++ b/frontend/app/crm/sales/custom-fields/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/custom-fields"; diff --git a/frontend/app/crm/sales/customers/error.tsx b/frontend/app/crm/sales/customers/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/customers/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/customers/loading.tsx b/frontend/app/crm/sales/customers/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/customers/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/customers/page.tsx b/frontend/app/crm/sales/customers/page.tsx new file mode 100644 index 0000000..aec818b --- /dev/null +++ b/frontend/app/crm/sales/customers/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/customers"; diff --git a/frontend/app/crm/sales/dashboard/activities/error.tsx b/frontend/app/crm/sales/dashboard/activities/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/dashboard/activities/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/activities/loading.tsx b/frontend/app/crm/sales/dashboard/activities/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/dashboard/activities/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/activities/page.tsx b/frontend/app/crm/sales/dashboard/activities/page.tsx new file mode 100644 index 0000000..bc9f811 --- /dev/null +++ b/frontend/app/crm/sales/dashboard/activities/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { ActivityDashboard as default } from "@/modules/crm/features/sales/dashboards"; diff --git a/frontend/app/crm/sales/dashboard/calendar/error.tsx b/frontend/app/crm/sales/dashboard/calendar/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/dashboard/calendar/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/calendar/loading.tsx b/frontend/app/crm/sales/dashboard/calendar/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/dashboard/calendar/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/calendar/page.tsx b/frontend/app/crm/sales/dashboard/calendar/page.tsx new file mode 100644 index 0000000..0085ede --- /dev/null +++ b/frontend/app/crm/sales/dashboard/calendar/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { CalendarDashboard as default } from "@/modules/crm/features/sales/dashboards"; diff --git a/frontend/app/crm/sales/dashboard/leads/error.tsx b/frontend/app/crm/sales/dashboard/leads/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/dashboard/leads/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/leads/loading.tsx b/frontend/app/crm/sales/dashboard/leads/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/dashboard/leads/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/leads/page.tsx b/frontend/app/crm/sales/dashboard/leads/page.tsx new file mode 100644 index 0000000..b162b95 --- /dev/null +++ b/frontend/app/crm/sales/dashboard/leads/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { LeadDashboard as default } from "@/modules/crm/features/sales/dashboards"; diff --git a/frontend/app/crm/sales/dashboard/overview/error.tsx b/frontend/app/crm/sales/dashboard/overview/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/dashboard/overview/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/overview/loading.tsx b/frontend/app/crm/sales/dashboard/overview/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/dashboard/overview/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/overview/page.tsx b/frontend/app/crm/sales/dashboard/overview/page.tsx new file mode 100644 index 0000000..490b097 --- /dev/null +++ b/frontend/app/crm/sales/dashboard/overview/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { OverviewDashboard as default } from "@/modules/crm/features/sales/dashboards"; diff --git a/frontend/app/crm/sales/dashboard/pipeline/error.tsx b/frontend/app/crm/sales/dashboard/pipeline/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/dashboard/pipeline/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/pipeline/loading.tsx b/frontend/app/crm/sales/dashboard/pipeline/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/dashboard/pipeline/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/pipeline/page.tsx b/frontend/app/crm/sales/dashboard/pipeline/page.tsx new file mode 100644 index 0000000..c6821cf --- /dev/null +++ b/frontend/app/crm/sales/dashboard/pipeline/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { PipelineDashboard as default } from "@/modules/crm/features/sales/dashboards"; diff --git a/frontend/app/crm/sales/dashboard/sales/error.tsx b/frontend/app/crm/sales/dashboard/sales/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/dashboard/sales/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/sales/loading.tsx b/frontend/app/crm/sales/dashboard/sales/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/dashboard/sales/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/dashboard/sales/page.tsx b/frontend/app/crm/sales/dashboard/sales/page.tsx new file mode 100644 index 0000000..68132c7 --- /dev/null +++ b/frontend/app/crm/sales/dashboard/sales/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { SalesDashboard as default } from "@/modules/crm/features/sales/dashboards"; diff --git a/frontend/app/crm/sales/deals/error.tsx b/frontend/app/crm/sales/deals/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/deals/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/deals/loading.tsx b/frontend/app/crm/sales/deals/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/deals/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/deals/page.tsx b/frontend/app/crm/sales/deals/page.tsx new file mode 100644 index 0000000..650372d --- /dev/null +++ b/frontend/app/crm/sales/deals/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/deals"; diff --git a/frontend/app/crm/sales/documents/error.tsx b/frontend/app/crm/sales/documents/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/documents/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/documents/loading.tsx b/frontend/app/crm/sales/documents/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/documents/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/documents/page.tsx b/frontend/app/crm/sales/documents/page.tsx new file mode 100644 index 0000000..e97ec1d --- /dev/null +++ b/frontend/app/crm/sales/documents/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/documents"; diff --git a/frontend/app/crm/sales/emails/error.tsx b/frontend/app/crm/sales/emails/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/emails/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/emails/loading.tsx b/frontend/app/crm/sales/emails/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/emails/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/emails/page.tsx b/frontend/app/crm/sales/emails/page.tsx new file mode 100644 index 0000000..b9fed74 --- /dev/null +++ b/frontend/app/crm/sales/emails/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/emails"; diff --git a/frontend/app/crm/sales/export/error.tsx b/frontend/app/crm/sales/export/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/export/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/export/loading.tsx b/frontend/app/crm/sales/export/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/export/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/export/page.tsx b/frontend/app/crm/sales/export/page.tsx new file mode 100644 index 0000000..1a6e599 --- /dev/null +++ b/frontend/app/crm/sales/export/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/export"; diff --git a/frontend/app/crm/sales/health/error.tsx b/frontend/app/crm/sales/health/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/health/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/health/loading.tsx b/frontend/app/crm/sales/health/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/health/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/health/page.tsx b/frontend/app/crm/sales/health/page.tsx new file mode 100644 index 0000000..a3da2a2 --- /dev/null +++ b/frontend/app/crm/sales/health/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { HealthPage as default } from "@/modules/crm/features/sales/admin"; diff --git a/frontend/app/crm/sales/import/error.tsx b/frontend/app/crm/sales/import/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/import/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/import/loading.tsx b/frontend/app/crm/sales/import/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/import/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/import/page.tsx b/frontend/app/crm/sales/import/page.tsx new file mode 100644 index 0000000..775cafd --- /dev/null +++ b/frontend/app/crm/sales/import/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { ImportPage as default } from "@/modules/crm/features/sales/scoped"; diff --git a/frontend/app/crm/sales/integrations/error.tsx b/frontend/app/crm/sales/integrations/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/integrations/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/integrations/loading.tsx b/frontend/app/crm/sales/integrations/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/integrations/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/integrations/page.tsx b/frontend/app/crm/sales/integrations/page.tsx new file mode 100644 index 0000000..24a7c01 --- /dev/null +++ b/frontend/app/crm/sales/integrations/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { IntegrationsPage as default } from "@/modules/crm/features/sales/scoped"; diff --git a/frontend/app/crm/sales/kanban/error.tsx b/frontend/app/crm/sales/kanban/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/kanban/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/kanban/loading.tsx b/frontend/app/crm/sales/kanban/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/kanban/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/kanban/page.tsx b/frontend/app/crm/sales/kanban/page.tsx new file mode 100644 index 0000000..6d8a7d4 --- /dev/null +++ b/frontend/app/crm/sales/kanban/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/kanban"; diff --git a/frontend/app/crm/sales/layout.tsx b/frontend/app/crm/sales/layout.tsx new file mode 100644 index 0000000..6804672 --- /dev/null +++ b/frontend/app/crm/sales/layout.tsx @@ -0,0 +1,5 @@ +"use client"; + +import { createPortalLayout } from "@/modules/crm/components/createPortalLayout"; + +export default createPortalLayout("sales"); diff --git a/frontend/app/crm/sales/leads/error.tsx b/frontend/app/crm/sales/leads/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/leads/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/leads/loading.tsx b/frontend/app/crm/sales/leads/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/leads/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/leads/page.tsx b/frontend/app/crm/sales/leads/page.tsx new file mode 100644 index 0000000..d217738 --- /dev/null +++ b/frontend/app/crm/sales/leads/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/leads"; diff --git a/frontend/app/crm/sales/meetings/error.tsx b/frontend/app/crm/sales/meetings/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/meetings/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/meetings/loading.tsx b/frontend/app/crm/sales/meetings/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/meetings/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/meetings/page.tsx b/frontend/app/crm/sales/meetings/page.tsx new file mode 100644 index 0000000..5f064da --- /dev/null +++ b/frontend/app/crm/sales/meetings/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/meetings"; diff --git a/frontend/app/crm/sales/notes/error.tsx b/frontend/app/crm/sales/notes/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/notes/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/notes/loading.tsx b/frontend/app/crm/sales/notes/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/notes/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/notes/page.tsx b/frontend/app/crm/sales/notes/page.tsx new file mode 100644 index 0000000..b1c5a64 --- /dev/null +++ b/frontend/app/crm/sales/notes/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/notes"; diff --git a/frontend/app/crm/sales/notifications/error.tsx b/frontend/app/crm/sales/notifications/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/notifications/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/notifications/loading.tsx b/frontend/app/crm/sales/notifications/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/notifications/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/notifications/page.tsx b/frontend/app/crm/sales/notifications/page.tsx new file mode 100644 index 0000000..775410e --- /dev/null +++ b/frontend/app/crm/sales/notifications/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { NotificationsPage as default } from "@/modules/crm/features/sales/admin"; diff --git a/frontend/app/crm/sales/permissions/error.tsx b/frontend/app/crm/sales/permissions/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/permissions/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/permissions/loading.tsx b/frontend/app/crm/sales/permissions/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/permissions/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/permissions/page.tsx b/frontend/app/crm/sales/permissions/page.tsx new file mode 100644 index 0000000..c4cf943 --- /dev/null +++ b/frontend/app/crm/sales/permissions/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { PermissionsPage as default } from "@/modules/crm/features/sales/admin"; diff --git a/frontend/app/crm/sales/pipelines/error.tsx b/frontend/app/crm/sales/pipelines/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/pipelines/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/pipelines/loading.tsx b/frontend/app/crm/sales/pipelines/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/pipelines/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/pipelines/page.tsx b/frontend/app/crm/sales/pipelines/page.tsx new file mode 100644 index 0000000..379dbb8 --- /dev/null +++ b/frontend/app/crm/sales/pipelines/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/pipelines"; diff --git a/frontend/app/crm/sales/quotes/error.tsx b/frontend/app/crm/sales/quotes/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/quotes/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/quotes/loading.tsx b/frontend/app/crm/sales/quotes/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/quotes/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/quotes/page.tsx b/frontend/app/crm/sales/quotes/page.tsx new file mode 100644 index 0000000..9ff6c0d --- /dev/null +++ b/frontend/app/crm/sales/quotes/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/quotes"; diff --git a/frontend/app/crm/sales/reports/error.tsx b/frontend/app/crm/sales/reports/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/reports/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/reports/loading.tsx b/frontend/app/crm/sales/reports/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/reports/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/reports/page.tsx b/frontend/app/crm/sales/reports/page.tsx new file mode 100644 index 0000000..997967c --- /dev/null +++ b/frontend/app/crm/sales/reports/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/reports"; diff --git a/frontend/app/crm/sales/roles/error.tsx b/frontend/app/crm/sales/roles/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/roles/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/roles/loading.tsx b/frontend/app/crm/sales/roles/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/roles/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/roles/page.tsx b/frontend/app/crm/sales/roles/page.tsx new file mode 100644 index 0000000..ca0b8ce --- /dev/null +++ b/frontend/app/crm/sales/roles/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { RolesPage as default } from "@/modules/crm/features/sales/admin"; diff --git a/frontend/app/crm/sales/segments/error.tsx b/frontend/app/crm/sales/segments/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/segments/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/segments/loading.tsx b/frontend/app/crm/sales/segments/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/segments/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/segments/page.tsx b/frontend/app/crm/sales/segments/page.tsx new file mode 100644 index 0000000..ab3ddf4 --- /dev/null +++ b/frontend/app/crm/sales/segments/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { SegmentsPage as default } from "@/modules/crm/features/sales/scoped"; diff --git a/frontend/app/crm/sales/settings/error.tsx b/frontend/app/crm/sales/settings/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/settings/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/settings/loading.tsx b/frontend/app/crm/sales/settings/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/settings/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/settings/page.tsx b/frontend/app/crm/sales/settings/page.tsx new file mode 100644 index 0000000..e20d996 --- /dev/null +++ b/frontend/app/crm/sales/settings/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { SettingsPage as default } from "@/modules/crm/features/sales/admin"; diff --git a/frontend/app/crm/sales/tags/error.tsx b/frontend/app/crm/sales/tags/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/tags/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/tags/loading.tsx b/frontend/app/crm/sales/tags/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/tags/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/tags/page.tsx b/frontend/app/crm/sales/tags/page.tsx new file mode 100644 index 0000000..8b2f1fb --- /dev/null +++ b/frontend/app/crm/sales/tags/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/tags"; diff --git a/frontend/app/crm/sales/tasks/error.tsx b/frontend/app/crm/sales/tasks/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/tasks/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/tasks/loading.tsx b/frontend/app/crm/sales/tasks/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/tasks/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/tasks/page.tsx b/frontend/app/crm/sales/tasks/page.tsx new file mode 100644 index 0000000..8fd9126 --- /dev/null +++ b/frontend/app/crm/sales/tasks/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/tasks"; diff --git a/frontend/app/crm/sales/timeline/error.tsx b/frontend/app/crm/sales/timeline/error.tsx new file mode 100644 index 0000000..b8dfa5e --- /dev/null +++ b/frontend/app/crm/sales/timeline/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ErrorState } from "@/components/ds"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} diff --git a/frontend/app/crm/sales/timeline/loading.tsx b/frontend/app/crm/sales/timeline/loading.tsx new file mode 100644 index 0000000..141fa78 --- /dev/null +++ b/frontend/app/crm/sales/timeline/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingState } from "@/components/ds"; + +export default function Loading() { + return ; +} diff --git a/frontend/app/crm/sales/timeline/page.tsx b/frontend/app/crm/sales/timeline/page.tsx new file mode 100644 index 0000000..28ae7b9 --- /dev/null +++ b/frontend/app/crm/sales/timeline/page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { default } from "@/modules/crm/features/sales/timeline"; diff --git a/frontend/docs/crm-component-map.md b/frontend/docs/crm-component-map.md new file mode 100644 index 0000000..196c9cc --- /dev/null +++ b/frontend/docs/crm-component-map.md @@ -0,0 +1,68 @@ +# CRM Component Map + +**Date:** 2026-07-26 + +## Module components (`modules/crm/`) + +| Component | Path | Purpose | +|-----------|------|---------| +| `CrmPortalShell` | `components/CrmPortalShell.tsx` | Sidebar nav, mobile drawer, theme toggle | +| `createPortalLayout` | `components/createPortalLayout.tsx` | AuthGuard + tenant gate + shell | +| `CrmBreadcrumbs` | `components/CrmBreadcrumbs.tsx` | CRM-root breadcrumbs | +| `CrmScopeNotice` | `components/CrmScopeNotice.tsx` | Out-of-scope feature messaging | +| `CrmListCrudPage` / `createCrmListPage` | `components/CrmListCrudPage.tsx` | Generic list CRUD factory | +| `CrmTablePage` | `design-system/CrmTablePage.tsx` | Search, sort, pagination, bulk bar | +| `CrmStatGrid` | `design-system/CrmTablePage.tsx` | Dashboard stat cards | +| `CrmStatusChip` | `design-system/CrmStatusChip.tsx` | Domain status badges | + +## Shared platform (`@/components/ds`) + +Used across all CRM pages: + +- `PageHeader`, `DataTable`, `EmptyState`, `LoadingState`, `ErrorState` +- `Button`, `Input`, `Textarea`, `Select`, `FormField`, `Checkbox` +- `Dialog`, `Drawer`, `ConfirmDialog` +- `Card`, `Badge`, `Pagination`, `Skeleton`, `Breadcrumbs` + +## Feature pages (`modules/crm/features/sales/`) + +| File | Default export / named | Entities | +|------|------------------------|----------| +| `dashboards.tsx` | 6 dashboard exports | Aggregates | +| `leads.tsx` | default | Lead CRUD | +| `contacts.tsx` | default | Contact CRUD | +| `companies.tsx` | default | Organization CRUD | +| `customers.tsx` | default | Combined view | +| `deals.tsx` | default | Opportunity CRUD + win/lose | +| `kanban.tsx` | default | Pipeline board | +| `pipelines.tsx` | default | Pipeline CRUD | +| `quotes.tsx` | default | Quote CRUD | +| `activities.tsx` | default | SalesActivity CRUD | +| `tasks.tsx` | default | Task create + complete | +| `meetings.tsx` | default | Meeting create | +| `calls.tsx` | default | CallLog create | +| `emails.tsx` | default | Email metadata create | +| `notes.tsx` | default | Note per entity | +| `timeline.tsx` | default | Timeline list | +| `documents.tsx` | default | Attachment refs | +| `tags.tsx` | default | Tag CRUD | +| `custom-fields.tsx` | default | CustomField CRUD | +| `export.tsx` | default | CSV export | +| `reports.tsx` | default | Client reports | +| `analytics.tsx` | default | Analytics dashboard | +| `scoped.tsx` | Campaigns, Segments, Import, Automation, Integrations, ApiKeys | Scope notices | +| `admin.tsx` | Notifications, Roles, Permissions, Audit, Settings, Health, Capabilities | Admin | + +## Services & hooks + +| Symbol | Path | +|--------|------| +| `crmApi` | `services/crm-api.ts` | +| `useCrmKeyboardShortcuts` | `hooks/useCrmKeyboardShortcuts.ts` | +| `useCrmHealth`, `useCrmCounts` | `pages/shared.tsx` | + +## Query key namespace + +Prefix: `["crm", tenantId, ...]` + +Examples: `leads`, `opportunities`, `mentions-unread`, `counts` diff --git a/frontend/docs/crm-responsive-report.md b/frontend/docs/crm-responsive-report.md new file mode 100644 index 0000000..dfb02e6 --- /dev/null +++ b/frontend/docs/crm-responsive-report.md @@ -0,0 +1,60 @@ +# CRM Responsive Report + +**Date:** 2026-07-26 + +## Breakpoints tested (design intent) + +| Breakpoint | Layout behavior | +|------------|-----------------| +| Desktop (≥1024px) | Fixed sidebar 256px, full table, multi-column dashboards | +| Tablet (768–1023px) | Collapsible nav groups, horizontal scroll tables, 2-col stat grid | +| Mobile (<768px) | Hamburger → drawer sidebar, stacked forms, single-col stats | + +## Component responsiveness + +| Component | Desktop | Tablet | Mobile | +|-----------|---------|--------|--------| +| `CrmPortalShell` | Sidebar visible | Sidebar visible | Drawer overlay | +| `CrmTablePage` | Full table | `overflow-x-auto` min-width 640px | Horizontal scroll + compact toolbar | +| `CrmStatGrid` | 4 columns (`xl:grid-cols-4`) | 2 columns | 1–2 columns | +| Kanban board | Multi-column row | Horizontal scroll columns | Swipe scroll columns | +| Dialog / Drawer | Center dialog / side drawer | Full-width dialog | Full-screen friendly padding | +| Customers split view | 2-column grid | 2-column | Stacked sections | + +## RTL + +- All layouts use logical RTL flow (Persian copy, right-aligned nav) +- Search icon positioned `right-3` for RTL input padding +- Breadcrumbs and sidebar labels right-aligned + +## Dark mode + +- Uses global `ColorModeProvider` toggle in shell +- CRM tokens defined in `:root` and `.dark` in `globals.css` +- Surfaces: `--surface`, `--canvas`, `--border` from platform DS + +## Accessibility + +- Dialog: `role="dialog"`, `aria-modal`, Escape to close +- Loading: `LoadingState` with visible label +- Tables: semantic `` via `DataTable` +- Icon buttons: `aria-label` on edit/delete/complete actions +- Sort select: `aria-label="مرتب‌سازی"` + +## Touch targets + +- Mobile menu button 44px (`size="icon"`) +- Kanban stage move buttons padded for touch +- Pagination buttons min height ~36px + +## Performance notes + +- Lists fetch up to 500 rows per resource (backend max page_size) +- Dashboard parallel `Promise.all` for counts +- TanStack Query caching per `["crm", tenantId, resource]` + +## Gaps / follow-ups + +1. Kanban drag-and-drop not implemented — uses button stage move (touch-friendly alternative) +2. Calendar dashboard is list view, not full calendar grid (no calendar API) +3. Virtualized tables not added — acceptable for ≤500 row client cap diff --git a/frontend/docs/crm-routing-map.md b/frontend/docs/crm-routing-map.md new file mode 100644 index 0000000..eab7926 --- /dev/null +++ b/frontend/docs/crm-routing-map.md @@ -0,0 +1,80 @@ +# CRM Routing Map + +**Date:** 2026-07-26 +**Pattern:** Thin `app/crm/**/page.tsx` → `modules/crm/features/**` + +## Root + +| Route | File | Module export | +|-------|------|---------------| +| `/crm` | — | Redirect via hub link | +| `/crm/hub` | `app/crm/hub/page.tsx` | `CrmHub` from `pages/hub` | + +## Sales portal layout + +`app/crm/sales/layout.tsx` → `createPortalLayout("sales")` + +## Sales routes + +| URL | Feature module | +|-----|----------------| +| `/crm/sales/dashboard/overview` | `features/sales/dashboards` → `OverviewDashboard` | +| `/crm/sales/dashboard/sales` | `SalesDashboard` | +| `/crm/sales/dashboard/pipeline` | `PipelineDashboard` | +| `/crm/sales/dashboard/leads` | `LeadDashboard` | +| `/crm/sales/dashboard/activities` | `ActivityDashboard` | +| `/crm/sales/dashboard/calendar` | `CalendarDashboard` | +| `/crm/sales/customers` | `features/sales/customers` | +| `/crm/sales/companies` | `features/sales/companies` | +| `/crm/sales/contacts` | `features/sales/contacts` | +| `/crm/sales/leads` | `features/sales/leads` | +| `/crm/sales/deals` | `features/sales/deals` | +| `/crm/sales/kanban` | `features/sales/kanban` | +| `/crm/sales/pipelines` | `features/sales/pipelines` | +| `/crm/sales/quotes` | `features/sales/quotes` | +| `/crm/sales/activities` | `features/sales/activities` | +| `/crm/sales/tasks` | `features/sales/tasks` | +| `/crm/sales/meetings` | `features/sales/meetings` | +| `/crm/sales/calls` | `features/sales/calls` | +| `/crm/sales/emails` | `features/sales/emails` | +| `/crm/sales/notes` | `features/sales/notes` | +| `/crm/sales/timeline` | `features/sales/timeline` | +| `/crm/sales/documents` | `features/sales/documents` | +| `/crm/sales/campaigns` | `features/sales/scoped` → `CampaignsPage` | +| `/crm/sales/segments` | `SegmentsPage` | +| `/crm/sales/tags` | `features/sales/tags` | +| `/crm/sales/custom-fields` | `features/sales/custom-fields` | +| `/crm/sales/import` | `ImportPage` | +| `/crm/sales/export` | `features/sales/export` | +| `/crm/sales/reports` | `features/sales/reports` | +| `/crm/sales/analytics` | `features/sales/analytics` | +| `/crm/sales/automation` | `AutomationPage` | +| `/crm/sales/notifications` | `features/sales/admin` → `NotificationsPage` | +| `/crm/sales/roles` | `RolesPage` | +| `/crm/sales/permissions` | `PermissionsPage` | +| `/crm/sales/integrations` | `IntegrationsPage` | +| `/crm/sales/api-keys` | `ApiKeysPage` | +| `/crm/sales/audit-logs` | `AuditLogsPage` | +| `/crm/sales/settings` | `SettingsPage` | +| `/crm/sales/health` | `HealthPage` | +| `/crm/sales/capabilities` | `CapabilitiesPage` | + +## Segment files (each route) + +Every route directory includes: + +- `page.tsx` — re-export only +- `loading.tsx` — `LoadingState` +- `error.tsx` — `ErrorState` + reset + +Root: `app/crm/layout.tsx`, `app/crm/not-found.tsx` + +## BFF + +| Browser path | Upstream | +|--------------|----------| +| `/api/crm/*` | `CRM_SERVICE_URL` / `localhost:8003` | + +## Generator + +Regenerate routes: `node scripts/generate-crm-routes.mjs` diff --git a/frontend/docs/crm-ui-specification.md b/frontend/docs/crm-ui-specification.md new file mode 100644 index 0000000..1de2a2f --- /dev/null +++ b/frontend/docs/crm-ui-specification.md @@ -0,0 +1,91 @@ +# CRM UI Specification + +**Product:** Torbat CRM (Enterprise Sales CRM) +**Version:** Phase 6.3 frontend integration +**Date:** 2026-07-26 + +## Product identity + +| Aspect | Choice | +|--------|--------| +| Accent | `--crm-accent` indigo (`#6366f1` light / `#818cf8` dark) | +| Tone | Professional, minimal, low noise, desktop-first | +| Language | RTL-first (Persian labels) | +| Shell | Collapsible sidebar groups, mobile drawer, dark mode toggle | + +Distinct from Accounting (sky/operational), Beauty (rose/warm), Healthcare (teal/clinical). + +## Architecture + +``` +app/crm/ # Thin routes only (page, layout, loading, error) +modules/crm/ # All business UI + services/crm-api.ts # Typed client → /api/crm BFF → :8003 + components/ # Shell, CRUD factory, breadcrumbs + design-system/ # CrmTablePage, CrmStatGrid, CrmStatusChip + features/sales/ # Page implementations + constants/portals.ts # Navigation +``` + +## Page inventory (41 routes) + +All pages under `/crm/sales/*` unless noted. + +| Area | Pages | Backend | +|------|-------|---------| +| Hub | `/crm/hub` | `/health` | +| Dashboards | overview, sales, pipeline, leads, activities, calendar | List aggregates | +| Entities | customers, companies, contacts, leads | Real CRUD APIs | +| Sales | deals, kanban, pipelines, quotes | opportunities, pipelines, quotes | +| Collaboration | activities, tasks, meetings, calls, emails, notes, timeline, documents | Phase 6.3 APIs | +| Marketing | tags | tags API; campaigns/segments scoped notice | +| Data | custom-fields, import, export | custom-fields; import N/A; export client CSV | +| Insights | reports, analytics, automation | computed / scoped | +| Admin | notifications, roles, permissions, integrations, api-keys, audit-logs, settings, health, capabilities | partial + static | + +## Standard page contract + +Every list page includes: + +- `CrmPageLoader` / skeleton via `CrmTablePage` +- `CrmPageError` with 403 permission state +- `EmptyState` with create action +- Search (client-side filter) +- Sort dropdown (client-side) +- Pagination (client-side slice; server uses page/page_size fetch) +- Bulk selection + bulk delete where API supports +- Dialog create + Drawer edit + ConfirmDialog delete +- Toast via Sonner +- Breadcrumb via `CrmBreadcrumbs` + +## Keyboard shortcuts + +| Chord | Target | +|-------|--------| +| `g` then `o` | Overview dashboard | +| `g` then `l` | Leads | +| `g` then `c` | Contacts | +| `g` then `d` | Deals | +| `g` then `k` | Kanban | + +## Out of scope (honest UI) + +Pages show `CrmScopeNotice` instead of fake data: + +- Campaigns, Segments, Import, Automation, Integrations, API Keys +- Global audit (per-entity audit only) +- `/capabilities` (health version only) + +## API conventions + +- Header: `Authorization: Bearer`, `X-Tenant-ID` +- Soft delete: `POST /{id}/delete` (leads, contacts, organizations) +- List: `page`, `page_size` — no server search/sort/filter +- Deals = `opportunities`; Customers = contacts + organizations + +## Related + +- [crm-component-map.md](./crm-component-map.md) +- [crm-routing-map.md](./crm-routing-map.md) +- [crm-validation-report.md](./crm-validation-report.md) +- [crm-responsive-report.md](./crm-responsive-report.md) diff --git a/frontend/docs/crm-validation-report.md b/frontend/docs/crm-validation-report.md new file mode 100644 index 0000000..75fcb5f --- /dev/null +++ b/frontend/docs/crm-validation-report.md @@ -0,0 +1,86 @@ +# CRM Validation Report + +**Date:** 2026-07-26 +**Scope:** CRM frontend (`modules/crm/`, `app/crm/`) + +## Quality gates + +| Gate | Result | Notes | +|------|--------|-------| +| `npm run typecheck` | ✅ Pass | All CRM modules type-clean | +| `npm run lint` | ✅ Pass | No CRM-specific warnings | +| `npm run validate:architecture` | ✅ Pass | No cross-domain imports | +| `npm run build` | ⚠️ Compile OK | Typecheck in build passed; final step hit transient Next font-manifest env error | + +## Route validation + +| Check | Count | Status | +|-------|-------|--------| +| Required pages (spec) | 41 | ✅ Generated | +| Thin `page.tsx` (re-export only) | 41 | ✅ | +| `loading.tsx` per route | 41 | ✅ | +| `error.tsx` per route | 41 | ✅ | +| Sales layout + portal gate | 1 | ✅ | +| Root layout + not-found | 2 | ✅ | + +## API validation + +| Domain | Connected | Mock/placeholder removed | +|--------|-----------|---------------------------| +| Leads | ✅ CRUD + assign + restore | ✅ | +| Contacts | ✅ CRUD + delete | ✅ | +| Organizations | ✅ CRUD + delete | ✅ | +| Opportunities (deals) | ✅ CRUD + stage + win/lose | ✅ | +| Pipelines | ✅ CRUD | ✅ | +| Activities | ✅ CRUD + complete | ✅ | +| Tasks | ✅ create + complete | ✅ | +| Meetings, Calls | ✅ create + list | ✅ | +| Quotes | ✅ CRUD | ✅ | +| Tags, Notes, Attachments, Custom fields | ✅ | ✅ | +| Timeline, Mentions | ✅ list / read | ✅ | +| Team roles/members | ✅ partial (no role list API) | ✅ honest UI | +| Campaigns, Segments, Import, Automation, Integrations, API keys | N/A backend | ✅ scope notice (no fake data) | + +## CRUD validation + +| Operation | Supported pages | +|-----------|-----------------| +| Create | All entity list pages via Dialog | +| Read | All list + dashboard aggregates | +| Update | leads, contacts, companies, deals, activities, quotes, pipelines | +| Delete | leads, contacts, companies (+ bulk) | +| Restore | leads only (API) | +| Archive | via soft-delete flags in list filter | +| Export | client CSV on export page + per-entity export button | +| Import | scope notice — no API | + +## Permission validation + +- `CrmPageError` maps `CrmApiError` 403 → permission empty state +- Static permission tree on `/crm/sales/permissions` +- No client-side RBAC gate (matches platform pattern); server enforces JWT roles + +## Navigation validation + +- All sidebar links in `constants/portals.ts` resolve to generated routes +- Hub → sales portal → all sub-pages +- Catalog tile: `apps-catalog.ts` → `/crm/hub` status `available` + +## Design system validation + +- CRM uses `@/components/ds` only (no duplicate atoms) +- Module accent via `--crm-accent` tokens +- Consistent `CrmTablePage` / `PageHeader` / card radius `rounded-2xl` + +## Known limitations (by design) + +1. No server-side search/filter/sort — client-side only +2. No list total count from API — pagination is approximate +3. Tasks/meetings: no update/delete API in backend 6.3 +4. `/capabilities` endpoint missing — health version shown instead + +## Recommendations (future) + +1. Add `GET /capabilities` to CRM backend for discovery UI +2. Add server-side filter params when backend supports them +3. Shared `useCrmPermission()` hook when Identity exposes user permissions to FE diff --git a/frontend/lib/apps-catalog.ts b/frontend/lib/apps-catalog.ts index 8ecc214..d6e7d52 100644 --- a/frontend/lib/apps-catalog.ts +++ b/frontend/lib/apps-catalog.ts @@ -22,13 +22,41 @@ export const PLATFORM_APPS: PlatformApp[] = [ status: "available", href: "/accounting", }, + { + id: "healthcare", + name: "تربت هلث", + description: "کلینیک، پزشک، بیمار، نوبت‌دهی", + icon: "🏥", + gradient: "from-rose-500 to-red-600", + status: "available", + href: "/healthcare/hub", + }, + { + id: "beauty", + name: "تربت بیوتی", + description: "سالن، نوبت، خدمات زیبایی", + icon: "💅", + gradient: "from-pink-500 to-fuchsia-600", + status: "available", + href: "/beauty/hub", + }, { id: "crm", name: "سیستم CRM", description: "مدیریت مشتری و فروش", icon: "🤝", gradient: "from-violet-500 to-purple-600", - status: "coming_soon", + status: "available", + href: "/crm/hub", + }, + { + id: "sports_center", + name: "مرکز ورزشی", + description: "عضویت، تمرین، مسابقات", + icon: "🏋️", + gradient: "from-blue-500 to-indigo-600", + status: "available", + href: "/sports-center/hub", }, { id: "ecommerce", diff --git a/frontend/modules/crm/components/CrmBreadcrumbs.tsx b/frontend/modules/crm/components/CrmBreadcrumbs.tsx new file mode 100644 index 0000000..5f705ee --- /dev/null +++ b/frontend/modules/crm/components/CrmBreadcrumbs.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { Breadcrumbs } from "@/components/ds"; + +export function CrmBreadcrumbs({ + items, +}: { + items: { label: string; href?: string }[]; +}) { + return ( +
+ +
+ ); +} diff --git a/frontend/modules/crm/components/CrmListCrudPage.tsx b/frontend/modules/crm/components/CrmListCrudPage.tsx new file mode 100644 index 0000000..a3d44a8 --- /dev/null +++ b/frontend/modules/crm/components/CrmListCrudPage.tsx @@ -0,0 +1,331 @@ +"use client"; + +import { useState } from "react"; +import { useForm, type FieldValues, type DefaultValues, type Resolver } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { Plus, Pencil, Trash2, RotateCcw } from "lucide-react"; +import type { z } from "zod"; +import { + Button, + Dialog, + Drawer, + Input, + EmptyState, + FormField, + ConfirmDialog, + Textarea, + Select, +} from "@/components/ds"; +import { CrmTablePage } from "@/modules/crm/design-system"; +import { CrmBreadcrumbs } from "@/modules/crm/components/CrmBreadcrumbs"; +import { CrmPageLoader, CrmPageError } from "@/modules/crm/pages/shared"; +import { useTenantId } from "@/hooks/useTenantId"; +import { exportToCsv } from "@/modules/crm/pages/shared"; + +export type CrmFieldConfig = { + name: string; + label: string; + type?: "text" | "email" | "tel" | "number" | "textarea" | "select"; + options?: { value: string; label: string }[]; + required?: boolean; + editOnly?: boolean; + createOnly?: boolean; +}; + +export type CrmListCrudConfig = { + title: string; + description?: string; + breadcrumb?: string; + resourceKey: string; + createSchema: z.ZodTypeAny; + editSchema: z.ZodTypeAny; + createDefaults: DefaultValues; + fields: CrmFieldConfig[]; + columns: { + key: string; + header: string; + render?: (row: T) => React.ReactNode; + }[]; + list: (tenantId: string) => Promise; + create: (tenantId: string, data: FieldValues) => Promise; + update?: (tenantId: string, id: string, data: FieldValues) => Promise; + remove?: (tenantId: string, id: string) => Promise; + restore?: (tenantId: string, id: string) => Promise; + exportColumns?: string[]; + filterDeleted?: boolean; +}; + +function renderField( + field: CrmFieldConfig, + register: ReturnType["register"], + mode: "create" | "edit" +) { + if (mode === "create" && field.editOnly) return null; + if (mode === "edit" && field.createOnly) return null; + + if (field.type === "textarea") { + return ( + +