@pylonsync/create-pylon 0.3.343 → 0.3.345
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +35 -0
- package/package.json +1 -1
- package/templates/agency/components/ui/badge.tsx +37 -0
- package/templates/agency/components/ui/dialog.tsx +110 -0
- package/templates/agency/components/ui/input.tsx +30 -0
- package/templates/agency/components/ui/label.tsx +21 -0
- package/templates/agency/components/ui/select.tsx +39 -0
- package/templates/agency/components/ui/table.tsx +124 -0
- package/templates/agency/components/ui/textarea.tsx +24 -0
- package/templates/agency/package.json +1 -0
- package/templates/ai-chat/components/ui/badge.tsx +37 -0
- package/templates/ai-chat/components/ui/button.tsx +56 -0
- package/templates/ai-chat/components/ui/card.tsx +90 -0
- package/templates/ai-chat/components/ui/dialog.tsx +110 -0
- package/templates/ai-chat/components/ui/input.tsx +30 -0
- package/templates/ai-chat/components/ui/label.tsx +21 -0
- package/templates/ai-chat/components/ui/select.tsx +39 -0
- package/templates/ai-chat/components/ui/table.tsx +124 -0
- package/templates/ai-chat/components/ui/textarea.tsx +24 -0
- package/templates/ai-chat/package.json +1 -0
- package/templates/ai-studio/components/ui/badge.tsx +37 -0
- package/templates/ai-studio/components/ui/button.tsx +56 -0
- package/templates/ai-studio/components/ui/card.tsx +90 -0
- package/templates/ai-studio/components/ui/dialog.tsx +110 -0
- package/templates/ai-studio/components/ui/input.tsx +30 -0
- package/templates/ai-studio/components/ui/label.tsx +21 -0
- package/templates/ai-studio/components/ui/select.tsx +39 -0
- package/templates/ai-studio/components/ui/table.tsx +124 -0
- package/templates/ai-studio/components/ui/textarea.tsx +24 -0
- package/templates/ai-studio/package.json +1 -0
- package/templates/barebones/components/ui/badge.tsx +37 -0
- package/templates/barebones/components/ui/dialog.tsx +110 -0
- package/templates/barebones/components/ui/input.tsx +30 -0
- package/templates/barebones/components/ui/label.tsx +21 -0
- package/templates/barebones/components/ui/select.tsx +39 -0
- package/templates/barebones/components/ui/table.tsx +124 -0
- package/templates/barebones/components/ui/textarea.tsx +24 -0
- package/templates/barebones/package.json +1 -0
- package/templates/chat/components/ui/badge.tsx +37 -0
- package/templates/chat/components/ui/dialog.tsx +110 -0
- package/templates/chat/components/ui/input.tsx +30 -0
- package/templates/chat/components/ui/label.tsx +21 -0
- package/templates/chat/components/ui/select.tsx +39 -0
- package/templates/chat/components/ui/table.tsx +124 -0
- package/templates/chat/components/ui/textarea.tsx +24 -0
- package/templates/chat/package.json +1 -0
- package/templates/consumer/components/ui/badge.tsx +37 -0
- package/templates/consumer/components/ui/dialog.tsx +110 -0
- package/templates/consumer/components/ui/input.tsx +30 -0
- package/templates/consumer/components/ui/label.tsx +21 -0
- package/templates/consumer/components/ui/select.tsx +39 -0
- package/templates/consumer/components/ui/table.tsx +124 -0
- package/templates/consumer/components/ui/textarea.tsx +24 -0
- package/templates/consumer/package.json +1 -0
- package/templates/creator/components/ui/badge.tsx +37 -0
- package/templates/creator/components/ui/dialog.tsx +110 -0
- package/templates/creator/components/ui/input.tsx +30 -0
- package/templates/creator/components/ui/label.tsx +21 -0
- package/templates/creator/components/ui/select.tsx +39 -0
- package/templates/creator/components/ui/table.tsx +124 -0
- package/templates/creator/components/ui/textarea.tsx +24 -0
- package/templates/creator/package.json +1 -0
- package/templates/crm/AGENTS.md +116 -0
- package/templates/crm/CLAUDE.md +7 -0
- package/templates/crm/README.md +96 -0
- package/templates/crm/app/companies/companies-view.tsx +129 -0
- package/templates/crm/app/companies/page.tsx +17 -0
- package/templates/crm/app/contacts/contacts-view.tsx +139 -0
- package/templates/crm/app/contacts/page.tsx +17 -0
- package/templates/crm/app/deals/[id]/deal-view.tsx +189 -0
- package/templates/crm/app/deals/[id]/page.tsx +23 -0
- package/templates/crm/app/error.tsx +43 -0
- package/templates/crm/app/globals.css +201 -0
- package/templates/crm/app/layout.tsx +31 -0
- package/templates/crm/app/login/auth-form.tsx +122 -0
- package/templates/crm/app/login/page.tsx +38 -0
- package/templates/crm/app/not-found.tsx +29 -0
- package/templates/crm/app/page.tsx +38 -0
- package/templates/crm/app/pipeline-view.tsx +99 -0
- package/templates/crm/app/robots.ts +12 -0
- package/templates/crm/app/sitemap.ts +27 -0
- package/templates/crm/app/workspace.tsx +231 -0
- package/templates/crm/app.ts +171 -0
- package/templates/crm/bunfig +4 -0
- package/templates/crm/components/activity-timeline.tsx +133 -0
- package/templates/crm/components/avatar.tsx +40 -0
- package/templates/crm/components/board-skeleton.tsx +44 -0
- package/templates/crm/components/command-palette.tsx +162 -0
- package/templates/crm/components/data-table.tsx +90 -0
- package/templates/crm/components/deal-card.tsx +91 -0
- package/templates/crm/components/deal-dialog.tsx +169 -0
- package/templates/crm/components/empty-state.tsx +43 -0
- package/templates/crm/components/kbd.tsx +23 -0
- package/templates/crm/components/metrics-bar.tsx +46 -0
- package/templates/crm/components/page-header.tsx +36 -0
- package/templates/crm/components/pipeline-board.tsx +105 -0
- package/templates/crm/components/record-dialog.tsx +126 -0
- package/templates/crm/components/sidebar.tsx +112 -0
- package/templates/crm/components/stage-badge.tsx +33 -0
- package/templates/crm/components/ui/badge.tsx +37 -0
- package/templates/crm/components/ui/button.tsx +56 -0
- package/templates/crm/components/ui/card.tsx +90 -0
- package/templates/crm/components/ui/dialog.tsx +110 -0
- package/templates/crm/components/ui/input.tsx +30 -0
- package/templates/crm/components/ui/label.tsx +21 -0
- package/templates/crm/components/ui/select.tsx +39 -0
- package/templates/crm/components/ui/table.tsx +124 -0
- package/templates/crm/components/ui/textarea.tsx +24 -0
- package/templates/crm/components.json +20 -0
- package/templates/crm/functions/createDeal.ts +59 -0
- package/templates/crm/functions/logActivity.ts +49 -0
- package/templates/crm/functions/moveDeal.ts +53 -0
- package/templates/crm/functions/seedWorkspace.ts +63 -0
- package/templates/crm/gitignore +10 -0
- package/templates/crm/lib/pipeline.ts +221 -0
- package/templates/crm/lib/search.ts +78 -0
- package/templates/crm/lib/seed.ts +130 -0
- package/templates/crm/lib/utils.ts +10 -0
- package/templates/crm/package.json +39 -0
- package/templates/crm/tests/components.test.tsx +245 -0
- package/templates/crm/tests/pipeline.test.ts +219 -0
- package/templates/crm/tests/search.test.ts +83 -0
- package/templates/crm/tests/setup.ts +5 -0
- package/templates/crm/tsconfig.json +33 -0
- package/templates/default/components/ui/badge.tsx +37 -0
- package/templates/default/components/ui/dialog.tsx +110 -0
- package/templates/default/components/ui/input.tsx +30 -0
- package/templates/default/components/ui/label.tsx +21 -0
- package/templates/default/components/ui/select.tsx +39 -0
- package/templates/default/components/ui/table.tsx +124 -0
- package/templates/default/components/ui/textarea.tsx +24 -0
- package/templates/default/package.json +1 -0
- package/templates/directory/components/ui/badge.tsx +37 -0
- package/templates/directory/components/ui/dialog.tsx +110 -0
- package/templates/directory/components/ui/input.tsx +30 -0
- package/templates/directory/components/ui/label.tsx +21 -0
- package/templates/directory/components/ui/select.tsx +39 -0
- package/templates/directory/components/ui/table.tsx +124 -0
- package/templates/directory/components/ui/textarea.tsx +24 -0
- package/templates/directory/package.json +1 -0
- package/templates/helpdesk/AGENTS.md +116 -0
- package/templates/helpdesk/CLAUDE.md +7 -0
- package/templates/helpdesk/README.md +90 -0
- package/templates/helpdesk/app/customers/customers-view.tsx +140 -0
- package/templates/helpdesk/app/customers/page.tsx +17 -0
- package/templates/helpdesk/app/error.tsx +43 -0
- package/templates/helpdesk/app/globals.css +201 -0
- package/templates/helpdesk/app/inbox-view.tsx +147 -0
- package/templates/helpdesk/app/layout.tsx +31 -0
- package/templates/helpdesk/app/login/auth-form.tsx +122 -0
- package/templates/helpdesk/app/login/page.tsx +38 -0
- package/templates/helpdesk/app/not-found.tsx +29 -0
- package/templates/helpdesk/app/page.tsx +38 -0
- package/templates/helpdesk/app/robots.ts +12 -0
- package/templates/helpdesk/app/sitemap.ts +27 -0
- package/templates/helpdesk/app/tickets/[id]/page.tsx +23 -0
- package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +164 -0
- package/templates/helpdesk/app/workspace.tsx +210 -0
- package/templates/helpdesk/app.ts +142 -0
- package/templates/helpdesk/bunfig +4 -0
- package/templates/helpdesk/components/avatar.tsx +40 -0
- package/templates/helpdesk/components/command-palette.tsx +162 -0
- package/templates/helpdesk/components/data-table.tsx +90 -0
- package/templates/helpdesk/components/empty-state.tsx +43 -0
- package/templates/helpdesk/components/kbd.tsx +23 -0
- package/templates/helpdesk/components/new-ticket-dialog.tsx +147 -0
- package/templates/helpdesk/components/page-header.tsx +36 -0
- package/templates/helpdesk/components/priority-badge.tsx +61 -0
- package/templates/helpdesk/components/record-dialog.tsx +126 -0
- package/templates/helpdesk/components/sidebar.tsx +111 -0
- package/templates/helpdesk/components/sla-indicator.tsx +47 -0
- package/templates/helpdesk/components/thread.tsx +143 -0
- package/templates/helpdesk/components/ticket-list.tsx +78 -0
- package/templates/helpdesk/components/ui/badge.tsx +37 -0
- package/templates/helpdesk/components/ui/button.tsx +56 -0
- package/templates/helpdesk/components/ui/card.tsx +90 -0
- package/templates/helpdesk/components/ui/dialog.tsx +110 -0
- package/templates/helpdesk/components/ui/input.tsx +30 -0
- package/templates/helpdesk/components/ui/label.tsx +21 -0
- package/templates/helpdesk/components/ui/select.tsx +39 -0
- package/templates/helpdesk/components/ui/table.tsx +124 -0
- package/templates/helpdesk/components/ui/textarea.tsx +24 -0
- package/templates/helpdesk/components.json +20 -0
- package/templates/helpdesk/functions/createTicket.ts +62 -0
- package/templates/helpdesk/functions/replyToTicket.ts +55 -0
- package/templates/helpdesk/functions/seedWorkspace.ts +52 -0
- package/templates/helpdesk/functions/setTicketState.ts +58 -0
- package/templates/helpdesk/gitignore +10 -0
- package/templates/helpdesk/lib/format.ts +53 -0
- package/templates/helpdesk/lib/search.ts +78 -0
- package/templates/helpdesk/lib/seed.ts +185 -0
- package/templates/helpdesk/lib/tickets.ts +184 -0
- package/templates/helpdesk/lib/utils.ts +10 -0
- package/templates/helpdesk/package.json +39 -0
- package/templates/helpdesk/tests/components.test.tsx +202 -0
- package/templates/helpdesk/tests/search.test.ts +83 -0
- package/templates/helpdesk/tests/setup.ts +5 -0
- package/templates/helpdesk/tests/tickets.test.ts +215 -0
- package/templates/helpdesk/tsconfig.json +33 -0
- package/templates/inventory/AGENTS.md +116 -0
- package/templates/inventory/CLAUDE.md +7 -0
- package/templates/inventory/README.md +103 -0
- package/templates/inventory/app/error.tsx +43 -0
- package/templates/inventory/app/globals.css +201 -0
- package/templates/inventory/app/layout.tsx +31 -0
- package/templates/inventory/app/login/auth-form.tsx +122 -0
- package/templates/inventory/app/login/page.tsx +38 -0
- package/templates/inventory/app/movements/movements-view.tsx +103 -0
- package/templates/inventory/app/movements/page.tsx +17 -0
- package/templates/inventory/app/not-found.tsx +29 -0
- package/templates/inventory/app/page.tsx +34 -0
- package/templates/inventory/app/products/[id]/page.tsx +23 -0
- package/templates/inventory/app/products/[id]/product-view.tsx +178 -0
- package/templates/inventory/app/products-view.tsx +213 -0
- package/templates/inventory/app/robots.ts +12 -0
- package/templates/inventory/app/sitemap.ts +27 -0
- package/templates/inventory/app/workspace.tsx +172 -0
- package/templates/inventory/app.ts +154 -0
- package/templates/inventory/bunfig +4 -0
- package/templates/inventory/components/avatar.tsx +40 -0
- package/templates/inventory/components/command-palette.tsx +162 -0
- package/templates/inventory/components/data-table.tsx +90 -0
- package/templates/inventory/components/empty-state.tsx +43 -0
- package/templates/inventory/components/kbd.tsx +23 -0
- package/templates/inventory/components/movement-dialog.tsx +182 -0
- package/templates/inventory/components/page-header.tsx +36 -0
- package/templates/inventory/components/record-dialog.tsx +126 -0
- package/templates/inventory/components/sidebar.tsx +111 -0
- package/templates/inventory/components/stock-level.tsx +47 -0
- package/templates/inventory/components/summary-bar.tsx +56 -0
- package/templates/inventory/components/ui/badge.tsx +37 -0
- package/templates/inventory/components/ui/button.tsx +56 -0
- package/templates/inventory/components/ui/card.tsx +90 -0
- package/templates/inventory/components/ui/dialog.tsx +110 -0
- package/templates/inventory/components/ui/input.tsx +30 -0
- package/templates/inventory/components/ui/label.tsx +21 -0
- package/templates/inventory/components/ui/select.tsx +39 -0
- package/templates/inventory/components/ui/table.tsx +124 -0
- package/templates/inventory/components/ui/textarea.tsx +24 -0
- package/templates/inventory/components.json +20 -0
- package/templates/inventory/functions/recordMovement.ts +72 -0
- package/templates/inventory/functions/seedWorkspace.ts +40 -0
- package/templates/inventory/gitignore +10 -0
- package/templates/inventory/lib/format.ts +53 -0
- package/templates/inventory/lib/search.ts +78 -0
- package/templates/inventory/lib/seed.ts +102 -0
- package/templates/inventory/lib/stock.ts +207 -0
- package/templates/inventory/lib/utils.ts +10 -0
- package/templates/inventory/package.json +39 -0
- package/templates/inventory/tests/components.test.tsx +155 -0
- package/templates/inventory/tests/search.test.ts +83 -0
- package/templates/inventory/tests/setup.ts +5 -0
- package/templates/inventory/tests/stock.test.ts +255 -0
- package/templates/inventory/tsconfig.json +33 -0
- package/templates/invoices/AGENTS.md +116 -0
- package/templates/invoices/CLAUDE.md +7 -0
- package/templates/invoices/README.md +99 -0
- package/templates/invoices/app/clients/clients-view.tsx +130 -0
- package/templates/invoices/app/clients/page.tsx +17 -0
- package/templates/invoices/app/error.tsx +43 -0
- package/templates/invoices/app/globals.css +201 -0
- package/templates/invoices/app/invoices/[id]/invoice-view.tsx +253 -0
- package/templates/invoices/app/invoices/[id]/page.tsx +23 -0
- package/templates/invoices/app/invoices-view.tsx +183 -0
- package/templates/invoices/app/layout.tsx +31 -0
- package/templates/invoices/app/login/auth-form.tsx +122 -0
- package/templates/invoices/app/login/page.tsx +38 -0
- package/templates/invoices/app/not-found.tsx +29 -0
- package/templates/invoices/app/page.tsx +36 -0
- package/templates/invoices/app/robots.ts +12 -0
- package/templates/invoices/app/sitemap.ts +27 -0
- package/templates/invoices/app/workspace.tsx +174 -0
- package/templates/invoices/app.ts +169 -0
- package/templates/invoices/bunfig +4 -0
- package/templates/invoices/components/avatar.tsx +40 -0
- package/templates/invoices/components/command-palette.tsx +162 -0
- package/templates/invoices/components/data-table.tsx +90 -0
- package/templates/invoices/components/empty-state.tsx +43 -0
- package/templates/invoices/components/kbd.tsx +23 -0
- package/templates/invoices/components/line-items.tsx +172 -0
- package/templates/invoices/components/page-header.tsx +36 -0
- package/templates/invoices/components/payment-dialog.tsx +132 -0
- package/templates/invoices/components/record-dialog.tsx +126 -0
- package/templates/invoices/components/sidebar.tsx +111 -0
- package/templates/invoices/components/status-badge.tsx +35 -0
- package/templates/invoices/components/summary-bar.tsx +65 -0
- package/templates/invoices/components/totals-panel.tsx +70 -0
- package/templates/invoices/components/ui/badge.tsx +37 -0
- package/templates/invoices/components/ui/button.tsx +56 -0
- package/templates/invoices/components/ui/card.tsx +90 -0
- package/templates/invoices/components/ui/dialog.tsx +110 -0
- package/templates/invoices/components/ui/input.tsx +30 -0
- package/templates/invoices/components/ui/label.tsx +21 -0
- package/templates/invoices/components/ui/select.tsx +39 -0
- package/templates/invoices/components/ui/table.tsx +124 -0
- package/templates/invoices/components/ui/textarea.tsx +24 -0
- package/templates/invoices/components.json +20 -0
- package/templates/invoices/functions/createInvoice.ts +49 -0
- package/templates/invoices/functions/recordPayment.ts +72 -0
- package/templates/invoices/functions/seedWorkspace.ts +57 -0
- package/templates/invoices/functions/setInvoiceStatus.ts +35 -0
- package/templates/invoices/gitignore +10 -0
- package/templates/invoices/lib/billing.ts +259 -0
- package/templates/invoices/lib/format.ts +53 -0
- package/templates/invoices/lib/search.ts +78 -0
- package/templates/invoices/lib/seed.ts +190 -0
- package/templates/invoices/lib/utils.ts +10 -0
- package/templates/invoices/package.json +39 -0
- package/templates/invoices/tests/billing.test.ts +283 -0
- package/templates/invoices/tests/components.test.tsx +172 -0
- package/templates/invoices/tests/search.test.ts +83 -0
- package/templates/invoices/tests/setup.ts +5 -0
- package/templates/invoices/tsconfig.json +33 -0
- package/templates/local-service/components/ui/badge.tsx +37 -0
- package/templates/local-service/components/ui/dialog.tsx +110 -0
- package/templates/local-service/components/ui/input.tsx +30 -0
- package/templates/local-service/components/ui/label.tsx +21 -0
- package/templates/local-service/components/ui/select.tsx +39 -0
- package/templates/local-service/components/ui/table.tsx +124 -0
- package/templates/local-service/components/ui/textarea.tsx +24 -0
- package/templates/local-service/package.json +1 -0
- package/templates/marketplace/components/ui/badge.tsx +37 -0
- package/templates/marketplace/components/ui/button.tsx +56 -0
- package/templates/marketplace/components/ui/card.tsx +90 -0
- package/templates/marketplace/components/ui/dialog.tsx +110 -0
- package/templates/marketplace/components/ui/input.tsx +30 -0
- package/templates/marketplace/components/ui/label.tsx +21 -0
- package/templates/marketplace/components/ui/select.tsx +39 -0
- package/templates/marketplace/components/ui/table.tsx +124 -0
- package/templates/marketplace/components/ui/textarea.tsx +24 -0
- package/templates/marketplace/lib/utils.ts +10 -0
- package/templates/marketplace/package.json +1 -0
- package/templates/projects/AGENTS.md +116 -0
- package/templates/projects/CLAUDE.md +7 -0
- package/templates/projects/README.md +96 -0
- package/templates/projects/app/clients/clients-view.tsx +126 -0
- package/templates/projects/app/clients/page.tsx +17 -0
- package/templates/projects/app/error.tsx +43 -0
- package/templates/projects/app/globals.css +201 -0
- package/templates/projects/app/layout.tsx +31 -0
- package/templates/projects/app/login/auth-form.tsx +122 -0
- package/templates/projects/app/login/page.tsx +38 -0
- package/templates/projects/app/not-found.tsx +29 -0
- package/templates/projects/app/page.tsx +30 -0
- package/templates/projects/app/projects/[id]/page.tsx +23 -0
- package/templates/projects/app/projects/[id]/project-view.tsx +179 -0
- package/templates/projects/app/projects-view.tsx +181 -0
- package/templates/projects/app/robots.ts +12 -0
- package/templates/projects/app/sitemap.ts +27 -0
- package/templates/projects/app/workspace.tsx +190 -0
- package/templates/projects/app.ts +186 -0
- package/templates/projects/bunfig +4 -0
- package/templates/projects/components/avatar.tsx +40 -0
- package/templates/projects/components/board-skeleton.tsx +37 -0
- package/templates/projects/components/budget-bar.tsx +61 -0
- package/templates/projects/components/command-palette.tsx +162 -0
- package/templates/projects/components/data-table.tsx +90 -0
- package/templates/projects/components/empty-state.tsx +43 -0
- package/templates/projects/components/kbd.tsx +23 -0
- package/templates/projects/components/page-header.tsx +36 -0
- package/templates/projects/components/record-dialog.tsx +126 -0
- package/templates/projects/components/sidebar.tsx +111 -0
- package/templates/projects/components/task-board.tsx +109 -0
- package/templates/projects/components/task-card.tsx +77 -0
- package/templates/projects/components/time-dialog.tsx +114 -0
- package/templates/projects/components/ui/badge.tsx +37 -0
- package/templates/projects/components/ui/button.tsx +56 -0
- package/templates/projects/components/ui/card.tsx +90 -0
- package/templates/projects/components/ui/dialog.tsx +110 -0
- package/templates/projects/components/ui/input.tsx +30 -0
- package/templates/projects/components/ui/label.tsx +21 -0
- package/templates/projects/components/ui/select.tsx +39 -0
- package/templates/projects/components/ui/table.tsx +124 -0
- package/templates/projects/components/ui/textarea.tsx +24 -0
- package/templates/projects/components.json +20 -0
- package/templates/projects/functions/logTime.ts +49 -0
- package/templates/projects/functions/moveTask.ts +40 -0
- package/templates/projects/functions/seedWorkspace.ts +59 -0
- package/templates/projects/gitignore +10 -0
- package/templates/projects/lib/format.ts +53 -0
- package/templates/projects/lib/search.ts +78 -0
- package/templates/projects/lib/seed.ts +140 -0
- package/templates/projects/lib/utils.ts +10 -0
- package/templates/projects/lib/work.ts +233 -0
- package/templates/projects/package.json +39 -0
- package/templates/projects/tests/components.test.tsx +200 -0
- package/templates/projects/tests/search.test.ts +83 -0
- package/templates/projects/tests/setup.ts +5 -0
- package/templates/projects/tests/work.test.ts +238 -0
- package/templates/projects/tsconfig.json +33 -0
- package/templates/restaurant/components/ui/badge.tsx +37 -0
- package/templates/restaurant/components/ui/dialog.tsx +110 -0
- package/templates/restaurant/components/ui/input.tsx +30 -0
- package/templates/restaurant/components/ui/label.tsx +21 -0
- package/templates/restaurant/components/ui/select.tsx +39 -0
- package/templates/restaurant/components/ui/table.tsx +124 -0
- package/templates/restaurant/components/ui/textarea.tsx +24 -0
- package/templates/restaurant/package.json +1 -0
- package/templates/shop/components/ui/badge.tsx +37 -0
- package/templates/shop/components/ui/dialog.tsx +110 -0
- package/templates/shop/components/ui/input.tsx +30 -0
- package/templates/shop/components/ui/label.tsx +21 -0
- package/templates/shop/components/ui/select.tsx +39 -0
- package/templates/shop/components/ui/table.tsx +124 -0
- package/templates/shop/components/ui/textarea.tsx +24 -0
- package/templates/shop/package.json +1 -0
- package/templates/todo/components/ui/badge.tsx +37 -0
- package/templates/todo/components/ui/dialog.tsx +110 -0
- package/templates/todo/components/ui/input.tsx +30 -0
- package/templates/todo/components/ui/label.tsx +21 -0
- package/templates/todo/components/ui/select.tsx +39 -0
- package/templates/todo/components/ui/table.tsx +124 -0
- package/templates/todo/components/ui/textarea.tsx +24 -0
- package/templates/todo/package.json +1 -0
- package/templates/waitlist/components/ui/badge.tsx +37 -0
- package/templates/waitlist/components/ui/dialog.tsx +110 -0
- package/templates/waitlist/components/ui/input.tsx +30 -0
- package/templates/waitlist/components/ui/label.tsx +21 -0
- package/templates/waitlist/components/ui/select.tsx +39 -0
- package/templates/waitlist/components/ui/table.tsx +124 -0
- package/templates/waitlist/components/ui/textarea.tsx +24 -0
- package/templates/waitlist/package.json +1 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { Link, callFn, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { ArrowLeft, ArrowLeftRight } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { PageHeader } from "@/components/page-header";
|
|
8
|
+
import { EmptyState } from "@/components/empty-state";
|
|
9
|
+
import { StockLevel } from "@/components/stock-level";
|
|
10
|
+
import { MovementDialog } from "@/components/movement-dialog";
|
|
11
|
+
import { relativeTime } from "@/lib/format";
|
|
12
|
+
import { money, reasonById, signedQuantity } from "@/lib/stock";
|
|
13
|
+
import { Workspace } from "../../workspace";
|
|
14
|
+
|
|
15
|
+
export function ProductView({
|
|
16
|
+
email,
|
|
17
|
+
productId,
|
|
18
|
+
}: {
|
|
19
|
+
email: string;
|
|
20
|
+
productId: string;
|
|
21
|
+
}) {
|
|
22
|
+
const [moveOpen, setMoveOpen] = useState(false);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Workspace email={email} pathname="/">
|
|
26
|
+
{(data) => {
|
|
27
|
+
const product = data.products.find((p) => p.id === productId);
|
|
28
|
+
|
|
29
|
+
if (!product) {
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<PageHeader title="Product" />
|
|
33
|
+
<EmptyState
|
|
34
|
+
title={data.loading ? "Loading…" : "Product not found"}
|
|
35
|
+
description={
|
|
36
|
+
data.loading ? undefined : "It may have been deleted, or the link is wrong."
|
|
37
|
+
}
|
|
38
|
+
action={
|
|
39
|
+
data.loading ? undefined : (
|
|
40
|
+
<Button size="sm" variant="secondary" asChild>
|
|
41
|
+
<Link href="/">Back to products</Link>
|
|
42
|
+
</Button>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
/>
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const quantity = data.levels.get(product.id) ?? 0;
|
|
51
|
+
// Newest first, and running backwards from the current level so each
|
|
52
|
+
// row shows what the shelf held right after it — the question you
|
|
53
|
+
// actually ask when auditing a count.
|
|
54
|
+
const history = data.movements
|
|
55
|
+
.filter((movement) => movement.productId === product.id)
|
|
56
|
+
.sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
|
|
57
|
+
let running = quantity;
|
|
58
|
+
const withBalance = history.map((movement) => {
|
|
59
|
+
const after = running;
|
|
60
|
+
running -= Math.trunc(Number(movement.delta) || 0);
|
|
61
|
+
return { movement, after };
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<PageHeader title={product.name}>
|
|
67
|
+
<Button size="sm" onClick={() => setMoveOpen(true)}>
|
|
68
|
+
<ArrowLeftRight />
|
|
69
|
+
Record movement
|
|
70
|
+
</Button>
|
|
71
|
+
</PageHeader>
|
|
72
|
+
|
|
73
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
74
|
+
<div className="mx-auto flex max-w-3xl flex-col gap-6 px-6 py-6">
|
|
75
|
+
<Link
|
|
76
|
+
href="/"
|
|
77
|
+
className="inline-flex w-fit items-center gap-1.5 text-[12px] text-muted-foreground transition-colors hover:text-foreground"
|
|
78
|
+
>
|
|
79
|
+
<ArrowLeft className="size-3.5" />
|
|
80
|
+
Products
|
|
81
|
+
</Link>
|
|
82
|
+
|
|
83
|
+
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 rounded-lg border border-border bg-card p-4 sm:grid-cols-4">
|
|
84
|
+
<Field label="SKU">
|
|
85
|
+
<span className="tabular">{product.sku}</span>
|
|
86
|
+
</Field>
|
|
87
|
+
<Field label="On hand">
|
|
88
|
+
<StockLevel
|
|
89
|
+
quantity={quantity}
|
|
90
|
+
reorderPoint={product.reorderPoint}
|
|
91
|
+
/>
|
|
92
|
+
</Field>
|
|
93
|
+
<Field label="Unit cost">{money(product.unitCostCents)}</Field>
|
|
94
|
+
<Field label="Stock value">
|
|
95
|
+
{money(Math.max(0, quantity) * (Number(product.unitCostCents) || 0))}
|
|
96
|
+
</Field>
|
|
97
|
+
</dl>
|
|
98
|
+
|
|
99
|
+
<section>
|
|
100
|
+
<h2 className="mb-2 text-[13px] font-semibold">History</h2>
|
|
101
|
+
<p className="mb-3 text-[12px] text-muted-foreground">
|
|
102
|
+
On hand is the sum of these movements. Nothing here is ever
|
|
103
|
+
edited — a correction is another movement.
|
|
104
|
+
</p>
|
|
105
|
+
{withBalance.length === 0 ? (
|
|
106
|
+
<p className="py-6 text-center text-[12px] text-muted-foreground">
|
|
107
|
+
No movements yet.
|
|
108
|
+
</p>
|
|
109
|
+
) : (
|
|
110
|
+
<ol className="space-y-1.5">
|
|
111
|
+
{withBalance.map(({ movement, after }) => (
|
|
112
|
+
<li
|
|
113
|
+
key={movement.id}
|
|
114
|
+
className="flex items-center gap-3 rounded-md border border-border bg-card px-3 py-2 text-[12px]"
|
|
115
|
+
>
|
|
116
|
+
<span
|
|
117
|
+
className={
|
|
118
|
+
"tabular w-12 shrink-0 font-medium " +
|
|
119
|
+
(movement.delta > 0 ? "text-stage-won" : "text-destructive")
|
|
120
|
+
}
|
|
121
|
+
>
|
|
122
|
+
{signedQuantity(movement.delta)}
|
|
123
|
+
</span>
|
|
124
|
+
<span className="shrink-0">
|
|
125
|
+
{reasonById(movement.reason)?.label ?? movement.reason}
|
|
126
|
+
</span>
|
|
127
|
+
{movement.note ? (
|
|
128
|
+
<span className="truncate text-muted-foreground">
|
|
129
|
+
{movement.note}
|
|
130
|
+
</span>
|
|
131
|
+
) : null}
|
|
132
|
+
<span className="tabular ml-auto shrink-0 text-muted-foreground">
|
|
133
|
+
→ {after}
|
|
134
|
+
</span>
|
|
135
|
+
<time
|
|
136
|
+
className="w-20 shrink-0 text-right text-muted-foreground"
|
|
137
|
+
dateTime={movement.createdAt ?? undefined}
|
|
138
|
+
>
|
|
139
|
+
{relativeTime(movement.createdAt)}
|
|
140
|
+
</time>
|
|
141
|
+
</li>
|
|
142
|
+
))}
|
|
143
|
+
</ol>
|
|
144
|
+
)}
|
|
145
|
+
</section>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<MovementDialog
|
|
150
|
+
open={moveOpen}
|
|
151
|
+
products={data.products.filter((p) => !p.archived)}
|
|
152
|
+
currentLevel={(id) => data.levels.get(id) ?? 0}
|
|
153
|
+
defaultProductId={product.id}
|
|
154
|
+
onOpenChange={setMoveOpen}
|
|
155
|
+
onRecord={(pid, delta, reason, note) =>
|
|
156
|
+
callFn("recordMovement", {
|
|
157
|
+
productId: pid,
|
|
158
|
+
delta,
|
|
159
|
+
reason,
|
|
160
|
+
...(note ? { note } : {}),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
/>
|
|
164
|
+
</>
|
|
165
|
+
);
|
|
166
|
+
}}
|
|
167
|
+
</Workspace>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
|
172
|
+
return (
|
|
173
|
+
<div className="min-w-0">
|
|
174
|
+
<dt className="text-[11px] text-muted-foreground">{label}</dt>
|
|
175
|
+
<dd className="mt-1 truncate text-[13px]">{children}</dd>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { callFn, db, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { Boxes, Plus, ArrowLeftRight } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Kbd } from "@/components/kbd";
|
|
8
|
+
import { PageHeader } from "@/components/page-header";
|
|
9
|
+
import { DataTable, type ColumnDef } from "@/components/data-table";
|
|
10
|
+
import { EmptyState } from "@/components/empty-state";
|
|
11
|
+
import { RecordDialog } from "@/components/record-dialog";
|
|
12
|
+
import { SummaryBar } from "@/components/summary-bar";
|
|
13
|
+
import { StockLevel } from "@/components/stock-level";
|
|
14
|
+
import { MovementDialog } from "@/components/movement-dialog";
|
|
15
|
+
import { money, parseAmount, parseCount, stockState } from "@/lib/stock";
|
|
16
|
+
import { Workspace, type ProductRow } from "./workspace";
|
|
17
|
+
|
|
18
|
+
export function ProductsView({
|
|
19
|
+
email,
|
|
20
|
+
openNew,
|
|
21
|
+
initialFilter,
|
|
22
|
+
}: {
|
|
23
|
+
email: string;
|
|
24
|
+
openNew?: boolean;
|
|
25
|
+
initialFilter?: string;
|
|
26
|
+
}) {
|
|
27
|
+
const router = useRouter();
|
|
28
|
+
const [newOpen, setNewOpen] = useState(Boolean(openNew));
|
|
29
|
+
const [moveOpen, setMoveOpen] = useState(false);
|
|
30
|
+
const [reorderOnly, setReorderOnly] = useState(initialFilter === "reorder");
|
|
31
|
+
|
|
32
|
+
// "m" records a movement — the thing you do twenty times a day. Ignored
|
|
33
|
+
// while typing.
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
function onKey(event: KeyboardEvent) {
|
|
36
|
+
const target = event.target as HTMLElement | null;
|
|
37
|
+
const typing =
|
|
38
|
+
!!target &&
|
|
39
|
+
(target.tagName === "INPUT" ||
|
|
40
|
+
target.tagName === "TEXTAREA" ||
|
|
41
|
+
target.isContentEditable);
|
|
42
|
+
if (typing || event.metaKey || event.ctrlKey) return;
|
|
43
|
+
if (event.key === "m") {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
setMoveOpen(true);
|
|
46
|
+
} else if (event.key === "c") {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
setNewOpen(true);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
window.addEventListener("keydown", onKey);
|
|
52
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Workspace email={email} pathname="/">
|
|
57
|
+
{(data) => {
|
|
58
|
+
const level = (id: string) => data.levels.get(id) ?? 0;
|
|
59
|
+
|
|
60
|
+
const columns: ColumnDef<ProductRow>[] = [
|
|
61
|
+
{
|
|
62
|
+
key: "sku",
|
|
63
|
+
header: "SKU",
|
|
64
|
+
cell: (row) => <span className="tabular text-muted-foreground">{row.sku}</span>,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: "name",
|
|
68
|
+
header: "Product",
|
|
69
|
+
cell: (row) => <span className="truncate font-medium">{row.name}</span>,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "category",
|
|
73
|
+
header: "Category",
|
|
74
|
+
cell: (row) => (
|
|
75
|
+
<span className="text-muted-foreground">{row.category ?? "—"}</span>
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "onhand",
|
|
80
|
+
header: "On hand",
|
|
81
|
+
numeric: true,
|
|
82
|
+
cell: (row) => (
|
|
83
|
+
<StockLevel quantity={level(row.id)} reorderPoint={row.reorderPoint} />
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "reorder",
|
|
88
|
+
header: "Reorder at",
|
|
89
|
+
numeric: true,
|
|
90
|
+
cell: (row) => (
|
|
91
|
+
<span className="text-muted-foreground">{row.reorderPoint || "—"}</span>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "value",
|
|
96
|
+
header: "Value",
|
|
97
|
+
numeric: true,
|
|
98
|
+
cell: (row) =>
|
|
99
|
+
money(Math.max(0, level(row.id)) * (Number(row.unitCostCents) || 0)),
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const rows = data.products
|
|
104
|
+
.filter((product) => !product.archived)
|
|
105
|
+
.filter((product) =>
|
|
106
|
+
reorderOnly
|
|
107
|
+
? stockState(level(product.id), product.reorderPoint) !== "ok"
|
|
108
|
+
: true,
|
|
109
|
+
)
|
|
110
|
+
.sort((a, b) => a.sku.localeCompare(b.sku));
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<>
|
|
114
|
+
<PageHeader title="Products" count={rows.length}>
|
|
115
|
+
<Button
|
|
116
|
+
size="sm"
|
|
117
|
+
variant={reorderOnly ? "default" : "secondary"}
|
|
118
|
+
onClick={() => setReorderOnly((on) => !on)}
|
|
119
|
+
>
|
|
120
|
+
Needs reorder
|
|
121
|
+
</Button>
|
|
122
|
+
<Button size="sm" variant="secondary" onClick={() => setMoveOpen(true)}>
|
|
123
|
+
<ArrowLeftRight />
|
|
124
|
+
Movement
|
|
125
|
+
<Kbd className="ml-1">m</Kbd>
|
|
126
|
+
</Button>
|
|
127
|
+
<Button size="sm" onClick={() => setNewOpen(true)}>
|
|
128
|
+
<Plus />
|
|
129
|
+
New product
|
|
130
|
+
</Button>
|
|
131
|
+
</PageHeader>
|
|
132
|
+
|
|
133
|
+
<SummaryBar products={data.products} movements={data.movements} />
|
|
134
|
+
|
|
135
|
+
<DataTable
|
|
136
|
+
rows={rows}
|
|
137
|
+
columns={columns}
|
|
138
|
+
onRowClick={(row) => router.push(`/products/${row.id}`)}
|
|
139
|
+
empty={
|
|
140
|
+
<EmptyState
|
|
141
|
+
icon={<Boxes />}
|
|
142
|
+
title={
|
|
143
|
+
data.loading
|
|
144
|
+
? "Loading…"
|
|
145
|
+
: reorderOnly
|
|
146
|
+
? "Nothing needs reordering"
|
|
147
|
+
: "No products yet"
|
|
148
|
+
}
|
|
149
|
+
description={
|
|
150
|
+
reorderOnly
|
|
151
|
+
? "Every line is above its reorder point."
|
|
152
|
+
: "Add a product, then record what you receive. On-hand is the sum of those movements — there is no quantity to keep in sync."
|
|
153
|
+
}
|
|
154
|
+
action={
|
|
155
|
+
reorderOnly ? undefined : (
|
|
156
|
+
<Button size="sm" onClick={() => setNewOpen(true)}>
|
|
157
|
+
<Plus />
|
|
158
|
+
New product
|
|
159
|
+
</Button>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
/>
|
|
163
|
+
}
|
|
164
|
+
/>
|
|
165
|
+
|
|
166
|
+
<RecordDialog
|
|
167
|
+
open={newOpen}
|
|
168
|
+
title="New product"
|
|
169
|
+
submitLabel="Create product"
|
|
170
|
+
onOpenChange={setNewOpen}
|
|
171
|
+
fields={[
|
|
172
|
+
{ name: "sku", label: "SKU", required: true, placeholder: "CFE-001" },
|
|
173
|
+
{ name: "name", label: "Name", required: true, placeholder: "House Blend, 1kg" },
|
|
174
|
+
{ name: "category", label: "Category", placeholder: "Coffee" },
|
|
175
|
+
{ name: "unitCost", label: "Unit cost", placeholder: "9.40" },
|
|
176
|
+
{ name: "unitPrice", label: "Unit price", placeholder: "18.00" },
|
|
177
|
+
{ name: "reorderPoint", label: "Reorder at", placeholder: "12" },
|
|
178
|
+
]}
|
|
179
|
+
onCreate={async (values) => {
|
|
180
|
+
// Money and counts are parsed here, at the edge — everything
|
|
181
|
+
// below this line is integers.
|
|
182
|
+
await db.insert("Product", {
|
|
183
|
+
sku: values.sku,
|
|
184
|
+
name: values.name,
|
|
185
|
+
category: values.category ?? null,
|
|
186
|
+
unitCostCents: parseAmount(values.unitCost ?? "") ?? 0,
|
|
187
|
+
unitPriceCents: parseAmount(values.unitPrice ?? "") ?? 0,
|
|
188
|
+
reorderPoint: parseCount(values.reorderPoint ?? "") ?? 0,
|
|
189
|
+
archived: false,
|
|
190
|
+
});
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
|
|
194
|
+
<MovementDialog
|
|
195
|
+
open={moveOpen}
|
|
196
|
+
products={data.products.filter((p) => !p.archived)}
|
|
197
|
+
currentLevel={level}
|
|
198
|
+
onOpenChange={setMoveOpen}
|
|
199
|
+
onRecord={(productId, delta, reason, note) =>
|
|
200
|
+
callFn("recordMovement", {
|
|
201
|
+
productId,
|
|
202
|
+
delta,
|
|
203
|
+
reason,
|
|
204
|
+
...(note ? { note } : {}),
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
/>
|
|
208
|
+
</>
|
|
209
|
+
);
|
|
210
|
+
}}
|
|
211
|
+
</Workspace>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Robots } from "@pylonsync/react";
|
|
2
|
+
|
|
3
|
+
// app/robots.ts → served at /robots.txt. The default export may also be async.
|
|
4
|
+
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
5
|
+
|
|
6
|
+
export default function robots(): Robots {
|
|
7
|
+
return {
|
|
8
|
+
// Keep the authenticated app and the API out of the index.
|
|
9
|
+
rules: { userAgent: "*", allow: "/", disallow: ["/dashboard", "/api/"] },
|
|
10
|
+
sitemap: `${SITE}/sitemap.xml`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Sitemap } from "@pylonsync/react";
|
|
2
|
+
|
|
3
|
+
// app/sitemap.ts → served at /sitemap.xml. The default export can be async, so
|
|
4
|
+
// it can enumerate dynamic pages from your database. Point SITE_URL at your
|
|
5
|
+
// domain in production.
|
|
6
|
+
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
7
|
+
|
|
8
|
+
export default async function sitemap(): Promise<Sitemap> {
|
|
9
|
+
// Only public pages belong here — /dashboard is private (and noindex), so
|
|
10
|
+
// it's intentionally left out.
|
|
11
|
+
const staticRoutes: Sitemap = [
|
|
12
|
+
{ url: `${SITE}/`, changeFrequency: "weekly", priority: 1 },
|
|
13
|
+
{ url: `${SITE}/login`, changeFrequency: "yearly", priority: 0.3 },
|
|
14
|
+
{ url: `${SITE}/signup`, changeFrequency: "yearly", priority: 0.5 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// The export is async, so you can enumerate dynamic pages from a DB read:
|
|
18
|
+
//
|
|
19
|
+
// const posts = await fetchPublishedPosts();
|
|
20
|
+
// const postRoutes: Sitemap = posts.map((p) => ({
|
|
21
|
+
// url: `${SITE}/blog/${p.slug}`,
|
|
22
|
+
// lastModified: p.updatedAt,
|
|
23
|
+
// }));
|
|
24
|
+
// return [...staticRoutes, ...postRoutes];
|
|
25
|
+
|
|
26
|
+
return staticRoutes;
|
|
27
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { callFn, db, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { useAuth } from "@pylonsync/client";
|
|
6
|
+
import { Sidebar } from "@/components/sidebar";
|
|
7
|
+
import { CommandPalette } from "@/components/command-palette";
|
|
8
|
+
import type { SearchItem } from "@/lib/search";
|
|
9
|
+
import { onHandByProduct, summarize, type Movement, type Product } from "@/lib/stock";
|
|
10
|
+
|
|
11
|
+
export type ProductRow = Product;
|
|
12
|
+
export type MovementRow = Movement;
|
|
13
|
+
export interface UserRow {
|
|
14
|
+
id: string;
|
|
15
|
+
email: string;
|
|
16
|
+
displayName?: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorkspaceData {
|
|
20
|
+
products: ProductRow[];
|
|
21
|
+
movements: MovementRow[];
|
|
22
|
+
/** On-hand for every product, computed once per render rather than per row. */
|
|
23
|
+
levels: Map<string, number>;
|
|
24
|
+
productName: (id: string | null | undefined) => string | null;
|
|
25
|
+
actorName: (id: string | null | undefined) => string | null;
|
|
26
|
+
loading: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The app shell, and the ONLY place that touches `db`.
|
|
31
|
+
*
|
|
32
|
+
* Every view below receives plain data through the render prop and reports
|
|
33
|
+
* changes through callbacks, which is what lets the components in `components/`
|
|
34
|
+
* render from fixtures in a test with nothing mocked.
|
|
35
|
+
*
|
|
36
|
+
* The queries are live: a movement recorded at the back door updates the level
|
|
37
|
+
* on the shop floor\'s screen immediately, so nobody sells what was just
|
|
38
|
+
* damaged.
|
|
39
|
+
*/
|
|
40
|
+
export function Workspace({
|
|
41
|
+
email,
|
|
42
|
+
pathname,
|
|
43
|
+
children,
|
|
44
|
+
}: {
|
|
45
|
+
email: string;
|
|
46
|
+
pathname: string;
|
|
47
|
+
children: (data: WorkspaceData) => React.ReactNode;
|
|
48
|
+
}) {
|
|
49
|
+
const router = useRouter();
|
|
50
|
+
const { signOut } = useAuth();
|
|
51
|
+
const { data: products, loading } = db.useQuery<ProductRow>("Product");
|
|
52
|
+
const { data: movements } = db.useQuery<MovementRow>("Movement");
|
|
53
|
+
const { data: users } = db.useQuery<UserRow>("User");
|
|
54
|
+
|
|
55
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
56
|
+
const seeded = useRef(false);
|
|
57
|
+
|
|
58
|
+
const productList = products ?? [];
|
|
59
|
+
const movementList = movements ?? [];
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (loading || seeded.current) return;
|
|
63
|
+
if (productList.length > 0) {
|
|
64
|
+
seeded.current = true;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
seeded.current = true;
|
|
68
|
+
callFn("seedWorkspace", {}).catch(() => {
|
|
69
|
+
seeded.current = false;
|
|
70
|
+
});
|
|
71
|
+
}, [loading, productList.length]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
function onKey(event: KeyboardEvent) {
|
|
75
|
+
const target = event.target as HTMLElement | null;
|
|
76
|
+
const typing =
|
|
77
|
+
!!target &&
|
|
78
|
+
(target.tagName === "INPUT" ||
|
|
79
|
+
target.tagName === "TEXTAREA" ||
|
|
80
|
+
target.isContentEditable);
|
|
81
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
setPaletteOpen((open) => !open);
|
|
84
|
+
} else if (event.key === "/" && !typing) {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
setPaletteOpen(true);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
window.addEventListener("keydown", onKey);
|
|
90
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
// One pass over the ledger for the whole render — calling onHand() per row
|
|
94
|
+
// would be O(products x movements).
|
|
95
|
+
const levels = useMemo(() => onHandByProduct(movementList), [movementList]);
|
|
96
|
+
const productById = useMemo(
|
|
97
|
+
() => new Map(productList.map((p) => [p.id, p] as const)),
|
|
98
|
+
[productList],
|
|
99
|
+
);
|
|
100
|
+
const userById = useMemo(
|
|
101
|
+
() => new Map((users ?? []).map((u) => [u.id, u] as const)),
|
|
102
|
+
[users],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const searchIndex = useMemo<SearchItem[]>(
|
|
106
|
+
() =>
|
|
107
|
+
productList.map((product) => ({
|
|
108
|
+
id: `product:${product.id}`,
|
|
109
|
+
type: "company" as const,
|
|
110
|
+
title: product.name,
|
|
111
|
+
subtitle: product.sku,
|
|
112
|
+
href: `/products/${product.id}`,
|
|
113
|
+
keywords: product.category ?? undefined,
|
|
114
|
+
})),
|
|
115
|
+
[productList],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const summary = summarize(productList, movementList);
|
|
119
|
+
|
|
120
|
+
const data: WorkspaceData = {
|
|
121
|
+
products: productList,
|
|
122
|
+
movements: movementList,
|
|
123
|
+
levels,
|
|
124
|
+
productName: (id) => productById.get(id ?? "")?.name ?? null,
|
|
125
|
+
actorName: (id) => {
|
|
126
|
+
const user = userById.get(id ?? "");
|
|
127
|
+
return user?.displayName || user?.email || null;
|
|
128
|
+
},
|
|
129
|
+
loading,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex h-screen overflow-hidden">
|
|
134
|
+
<Sidebar
|
|
135
|
+
workspace="Inventory"
|
|
136
|
+
email={email}
|
|
137
|
+
pathname={pathname}
|
|
138
|
+
counts={{ "/": summary.skuCount }}
|
|
139
|
+
onOpenCommand={() => setPaletteOpen(true)}
|
|
140
|
+
onSignOut={async () => {
|
|
141
|
+
await signOut();
|
|
142
|
+
window.location.assign("/login");
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
<main className="flex min-w-0 flex-1 flex-col">{children(data)}</main>
|
|
146
|
+
|
|
147
|
+
<CommandPalette
|
|
148
|
+
open={paletteOpen}
|
|
149
|
+
items={searchIndex}
|
|
150
|
+
actions={[
|
|
151
|
+
{
|
|
152
|
+
id: "new-product",
|
|
153
|
+
label: "New product",
|
|
154
|
+
run: () => router.push("/?new=product"),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "reorder",
|
|
158
|
+
label: `Needs reorder (${summary.lowCount + summary.outCount})`,
|
|
159
|
+
run: () => router.push("/?filter=reorder"),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "go-movements",
|
|
163
|
+
label: "Go to Movements",
|
|
164
|
+
run: () => router.push("/movements"),
|
|
165
|
+
},
|
|
166
|
+
]}
|
|
167
|
+
onClose={() => setPaletteOpen(false)}
|
|
168
|
+
onSelect={(item) => router.push(item.href)}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|