From 8f4af7a16c0e2d1c1f5038bee54bb0f941538e01 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Fri, 24 Jul 2026 20:59:34 +0330 Subject: [PATCH] Add super-admin invoices: settings templates and per-business issue flow. Platform invoice templates live under Settings; each business can list and issue invoices with optional name and a meshkee.com public link. Co-authored-by: Cursor --- apps/super-admin/.env.example | 2 + apps/super-admin/src/App.tsx | 4 + .../src/components/Modal.module.css | 4 + apps/super-admin/src/components/Modal.tsx | 8 +- .../src/components/PageContent.module.css | 2 +- apps/super-admin/src/components/Sidebar.tsx | 3 +- apps/super-admin/src/lib/config.ts | 6 + .../src/pages/BusinessInvoicesPage.module.css | 316 ++++++++ .../src/pages/BusinessInvoicesPage.tsx | 716 ++++++++++++++++++ .../src/pages/BusinessesPage.module.css | 10 +- apps/super-admin/src/pages/BusinessesPage.tsx | 10 + apps/super-admin/src/pages/HomePage.tsx | 9 +- .../src/pages/SettingsPage.module.css | 102 +++ apps/super-admin/src/pages/SettingsPage.tsx | 376 +++++++++ .../src/services/businessService.ts | 15 + .../src/services/invoiceService.ts | 100 +++ apps/super-admin/src/types/invoice.ts | 101 +++ apps/super-admin/src/utils/irtPrice.ts | 19 + apps/super-admin/src/vite-env.d.ts | 1 + docs/PROJECT_CONTEXT.md | 42 +- 20 files changed, 1839 insertions(+), 7 deletions(-) create mode 100644 apps/super-admin/src/pages/BusinessInvoicesPage.module.css create mode 100644 apps/super-admin/src/pages/BusinessInvoicesPage.tsx create mode 100644 apps/super-admin/src/pages/SettingsPage.module.css create mode 100644 apps/super-admin/src/pages/SettingsPage.tsx create mode 100644 apps/super-admin/src/services/invoiceService.ts create mode 100644 apps/super-admin/src/types/invoice.ts create mode 100644 apps/super-admin/src/utils/irtPrice.ts diff --git a/apps/super-admin/.env.example b/apps/super-admin/.env.example index e7b3a01..5f276d4 100644 --- a/apps/super-admin/.env.example +++ b/apps/super-admin/.env.example @@ -1,4 +1,6 @@ VITE_API_BASE_URL=http://localhost:3000/api/v1 # Hostname for this dashboard (meshkee.app in production). VITE_ADMIN_DOMAIN=meshkee.app +# Public domain used in platform invoice links (https://{domain}/invoices/{id}). +VITE_INVOICE_PUBLIC_DOMAIN=meshkee.com # Local HTTPS certs (gitignored): mkcert -cert-file .certs/meshkee.app.pem -key-file .certs/meshkee.app-key.pem meshkee.app diff --git a/apps/super-admin/src/App.tsx b/apps/super-admin/src/App.tsx index b7217d4..0fa4152 100644 --- a/apps/super-admin/src/App.tsx +++ b/apps/super-admin/src/App.tsx @@ -7,8 +7,10 @@ import { GuestRoute } from './components/GuestRoute' import { PageLayout } from './components/PageLayout' import { HomePage } from './pages/HomePage' import { BusinessesPage } from './pages/BusinessesPage' +import { BusinessInvoicesPage } from './pages/BusinessInvoicesPage' import { UsersPage } from './pages/UsersPage' import { WebsitesPage } from './pages/WebsitesPage' +import { SettingsPage } from './pages/SettingsPage' import { ProfilePage } from './pages/ProfilePage' import { LoginPage } from './pages/LoginPage' @@ -27,8 +29,10 @@ function App() { }> } /> } /> + } /> } /> } /> + } /> } /> diff --git a/apps/super-admin/src/components/Modal.module.css b/apps/super-admin/src/components/Modal.module.css index 01d91be..b2ef730 100644 --- a/apps/super-admin/src/components/Modal.module.css +++ b/apps/super-admin/src/components/Modal.module.css @@ -27,6 +27,10 @@ max-width: 640px; } +.modalXl { + max-width: min(1180px, calc(100vw - 40px)); +} + .header { position: relative; padding: 18px 18px 10px; diff --git a/apps/super-admin/src/components/Modal.tsx b/apps/super-admin/src/components/Modal.tsx index ebc6b96..b1ccbcb 100644 --- a/apps/super-admin/src/components/Modal.tsx +++ b/apps/super-admin/src/components/Modal.tsx @@ -8,9 +8,11 @@ interface ModalProps { children: React.ReactNode onClose: () => void wide?: boolean + /** Extra-wide dialog for dense forms (e.g. invoice editor). */ + xl?: boolean } -export function Modal({ open, title, children, onClose, wide }: ModalProps) { +export function Modal({ open, title, children, onClose, wide, xl }: ModalProps) { useEffect(() => { if (!open) return const onKey = (e: KeyboardEvent) => { @@ -22,9 +24,11 @@ export function Modal({ open, title, children, onClose, wide }: ModalProps) { if (!open) return null + const sizeClass = xl ? styles.modalXl : wide ? styles.modalWide : '' + return (
-
e.stopPropagation()}> +
e.stopPropagation()}>

{title}

+
+ + {error ?

{error}

: null} + +
+
+
Invoices
+
+ {loading ? 'Loading…' : `${total} invoice${total === 1 ? '' : 's'}`} +
+
+ + + + + + + + + + + + + {!loading && invoices.length === 0 ? ( + + + + ) : null} + {invoices.map((invoice) => ( + + + + + + + + + ))} + +
NameIssuedStatusTotalLinkActions
+ No invoices yet for this business. +
+
{invoice.name || `Invoice #${invoice.id}`}
+
+ {invoice.items?.length ?? 0} item{(invoice.items?.length ?? 0) === 1 ? '' : 's'} +
+
{formatDate(invoice.issuedAt)} + + {statusLabel(invoice.status)} + + {formatIrtPrice(invoice.total ?? 0)} +
+ + {invoicePublicUrl(invoice).replace(/^https?:\/\//, '')} + + +
+
+
+ + +
+
+
+ + {totalPages > 1 ? ( +
+
+ Page {page} / {totalPages} +
+
+ + +
+
+ ) : null} + + !submitting && setCreateOpen(false)} + xl + > +
+
+ + setInvoiceName(e.target.value)} + placeholder="e.g. Website redesign package" + /> +
+
+ + setNotes(e.target.value)} + placeholder="Optional notes for this invoice" + /> +
+
+ +
+
+ + +
+ +
+ + {templates.length === 0 ? ( +

+ No predefined items yet. Manage them in{' '} + Settings → Invoices, or add custom lines below. +

+ ) : null} + +
+ {draftItems.map((item, index) => ( +
+
+ Item {index + 1} + +
+
+
+ + updateDraftItem(item.key, { title: e.target.value })} + placeholder="Service title" + /> +
+
+ + updateDraftItem(item.key, { duration: e.target.value })} + placeholder="e.g. 3 months" + /> +
+
+ + updateDraftItem(item.key, { worktime: e.target.value })} + placeholder="e.g. 40 hours" + /> +
+
+ + + updateDraftItem(item.key, { price: formatIrtInput(e.target.value) }) + } + placeholder="0" + /> +
+
+ + + updateDraftItem(item.key, { + discountedPrice: formatIrtInput(e.target.value), + }) + } + placeholder="Optional" + /> +
+
+ +