diff --git a/apps/super-admin/.env.example b/apps/super-admin/.env.example index 5f276d4..65c755f 100644 --- a/apps/super-admin/.env.example +++ b/apps/super-admin/.env.example @@ -3,4 +3,7 @@ VITE_API_BASE_URL=http://localhost:3000/api/v1 VITE_ADMIN_DOMAIN=meshkee.app # Public domain used in platform invoice links (https://{domain}/invoices/{id}). VITE_INVOICE_PUBLIC_DOMAIN=meshkee.com +# Optional full origin for local show-page design (overrides domain). Leave unset in DEV — +# links use the current Vite origin (e.g. https://meshkee.app:5174/invoices/{id}). +# VITE_INVOICE_PUBLIC_BASE_URL=https://meshkee.app:5174 # 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 0fa4152..c295ee6 100644 --- a/apps/super-admin/src/App.tsx +++ b/apps/super-admin/src/App.tsx @@ -11,6 +11,9 @@ import { BusinessInvoicesPage } from './pages/BusinessInvoicesPage' import { UsersPage } from './pages/UsersPage' import { WebsitesPage } from './pages/WebsitesPage' import { SettingsPage } from './pages/SettingsPage' +import { InvoiceTemplateEditorPage } from './pages/InvoiceTemplateEditorPage' +import { IssueInvoicePage } from './pages/IssueInvoicePage' +import { PublicInvoicePage } from './pages/PublicInvoicePage' import { ProfilePage } from './pages/ProfilePage' import { LoginPage } from './pages/LoginPage' @@ -21,6 +24,8 @@ function App() { + } /> + }> } /> @@ -30,9 +35,15 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> + } /> + } + /> } /> diff --git a/apps/super-admin/src/components/InvoiceDraftFields.tsx b/apps/super-admin/src/components/InvoiceDraftFields.tsx new file mode 100644 index 0000000..3b66610 --- /dev/null +++ b/apps/super-admin/src/components/InvoiceDraftFields.tsx @@ -0,0 +1,314 @@ +import { Pencil, Plus, X } from 'lucide-react' +import type { InvoiceItemTemplate } from '../types/invoice' +import { formatIrtInput, formatIrtPrice } from '../utils/irtPrice' +import type { DraftAccount, DraftKeyPoint, DraftLineItem } from '../utils/invoiceDraft' +import { + draftItemFromItemTemplate, + emptyDraftAccount, + emptyDraftItem, + emptyDraftKeyPoint, +} from '../utils/invoiceDraft' +import tableStyles from '../pages/BusinessesPage.module.css' +import styles from '../pages/BusinessInvoicesPage.module.css' + +type Props = { + itemTemplates: InvoiceItemTemplate[] + items: DraftLineItem[] + keyPoints: DraftKeyPoint[] + accounts: DraftAccount[] + selectedItemTemplateId: string + onSelectedItemTemplateId: (id: string) => void + onItemsChange: (items: DraftLineItem[]) => void + onKeyPointsChange: (points: DraftKeyPoint[]) => void + onAccountsChange: (accounts: DraftAccount[]) => void + showItemTemplatePicker?: boolean +} + +export function InvoiceDraftFields({ + itemTemplates, + items, + keyPoints, + accounts, + selectedItemTemplateId, + onSelectedItemTemplateId, + onItemsChange, + onKeyPointsChange, + onAccountsChange, + showItemTemplatePicker = true, +}: Props) { + function updateItem(key: string, patch: Partial) { + onItemsChange(items.map((item) => (item.key === key ? { ...item, ...patch } : item))) + } + + function removeItem(key: string) { + if (items.length <= 1) return + onItemsChange(items.filter((item) => item.key !== key)) + } + + function addFromItemTemplate() { + const template = itemTemplates.find((t) => t.id === selectedItemTemplateId) + if (!template) return + const onlyEmpty = + items.length === 1 && + !items[0].title.trim() && + !items[0].price.trim() && + !items[0].description.trim() + onItemsChange(onlyEmpty ? [draftItemFromItemTemplate(template)] : [...items, draftItemFromItemTemplate(template)]) + onSelectedItemTemplateId('') + } + + return ( + <> +
+ {items.map((item, index) => ( +
+
+ Item {index + 1} + +
+
+
+ + updateItem(item.key, { title: e.target.value })} + placeholder="Service title" + /> +
+
+ + updateItem(item.key, { duration: e.target.value })} + placeholder="e.g. 3 months" + /> +
+
+ + updateItem(item.key, { worktime: e.target.value })} + placeholder="e.g. 40 hours" + /> +
+
+ + updateItem(item.key, { price: formatIrtInput(e.target.value) })} + placeholder="0" + /> +
+
+ + + updateItem(item.key, { discountedPrice: formatIrtInput(e.target.value) }) + } + placeholder="Optional" + /> +
+
+ +