@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,185 @@
|
|
|
1
|
+
// Demo data for a brand-new helpdesk.
|
|
2
|
+
//
|
|
3
|
+
// An empty inbox shows you nothing about triage, SLA, or the thread view. The
|
|
4
|
+
// first sign-in seeds a realistic queue — including one ticket already past its
|
|
5
|
+
// first-response window, so the breach state is visible rather than theoretical.
|
|
6
|
+
//
|
|
7
|
+
// Pure data + pure shaping; `functions/seedWorkspace.ts` stays a thin wrapper.
|
|
8
|
+
|
|
9
|
+
export interface SeedCustomer {
|
|
10
|
+
key: string;
|
|
11
|
+
name: string;
|
|
12
|
+
email: string;
|
|
13
|
+
company: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SeedTicket {
|
|
17
|
+
key: string;
|
|
18
|
+
customer: string;
|
|
19
|
+
subject: string;
|
|
20
|
+
status: string;
|
|
21
|
+
priority: string;
|
|
22
|
+
/** Hours since it arrived. */
|
|
23
|
+
age: number;
|
|
24
|
+
/** Hours since the first agent reply; null means nobody has answered. */
|
|
25
|
+
respondedAgo: number | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SeedMessage {
|
|
29
|
+
ticket: string;
|
|
30
|
+
body: string;
|
|
31
|
+
fromCustomer: boolean;
|
|
32
|
+
internal?: boolean;
|
|
33
|
+
/** Hours ago. */
|
|
34
|
+
age: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const SEED_CUSTOMERS: SeedCustomer[] = [
|
|
38
|
+
{ key: "dana", name: "Dana Whitfield", email: "dana@northwind.co", company: "Northwind Logistics" },
|
|
39
|
+
{ key: "tom", name: "Tom Alvarez", email: "tom@riverbed.coffee", company: "Riverbed Coffee" },
|
|
40
|
+
{ key: "priya", name: "Priya Raman", email: "priya@hallmarkdental.com", company: "Hallmark Dental" },
|
|
41
|
+
{ key: "ben", name: "Ben Osei", email: "ben@quarry.design", company: "Quarry Design Co" },
|
|
42
|
+
{ key: "lena", name: "Lena Fischer", email: "lena@beaconproperty.com", company: "Beacon Property" },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const SEED_TICKETS: SeedTicket[] = [
|
|
46
|
+
// Urgent, unanswered, 3h old against a 1h target — a live breach on first load.
|
|
47
|
+
{
|
|
48
|
+
key: "export",
|
|
49
|
+
customer: "dana",
|
|
50
|
+
subject: "Export is timing out on large date ranges",
|
|
51
|
+
status: "open",
|
|
52
|
+
priority: "urgent",
|
|
53
|
+
age: 3,
|
|
54
|
+
respondedAgo: null,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "invite",
|
|
58
|
+
customer: "tom",
|
|
59
|
+
subject: "Can't invite a second manager to the account",
|
|
60
|
+
status: "open",
|
|
61
|
+
priority: "high",
|
|
62
|
+
age: 6,
|
|
63
|
+
respondedAgo: 5,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: "invoice",
|
|
67
|
+
customer: "priya",
|
|
68
|
+
subject: "Invoice shows last month's plan",
|
|
69
|
+
status: "pending",
|
|
70
|
+
priority: "normal",
|
|
71
|
+
age: 26,
|
|
72
|
+
respondedAgo: 24,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "sso",
|
|
76
|
+
customer: "ben",
|
|
77
|
+
subject: "SSO with Google Workspace — is it supported?",
|
|
78
|
+
status: "open",
|
|
79
|
+
priority: "normal",
|
|
80
|
+
age: 14,
|
|
81
|
+
respondedAgo: null,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: "mobile",
|
|
85
|
+
customer: "lena",
|
|
86
|
+
subject: "Mobile layout cuts off the listing photos",
|
|
87
|
+
status: "solved",
|
|
88
|
+
priority: "low",
|
|
89
|
+
age: 120,
|
|
90
|
+
respondedAgo: 110,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: "webhook",
|
|
94
|
+
customer: "dana",
|
|
95
|
+
subject: "Webhook retries are firing twice",
|
|
96
|
+
status: "closed",
|
|
97
|
+
priority: "high",
|
|
98
|
+
age: 300,
|
|
99
|
+
respondedAgo: 290,
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
export const SEED_MESSAGES: SeedMessage[] = [
|
|
104
|
+
{
|
|
105
|
+
ticket: "export",
|
|
106
|
+
body: "Pulling a 90-day export just spins and eventually errors. 30 days is fine. This is blocking our month-end reporting.",
|
|
107
|
+
fromCustomer: true,
|
|
108
|
+
age: 3,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
ticket: "invite",
|
|
112
|
+
body: "I added marcus@northwind.co but he never got the email and doesn't show in the members list.",
|
|
113
|
+
fromCustomer: true,
|
|
114
|
+
age: 6,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
ticket: "invite",
|
|
118
|
+
body: "Thanks Tom — I can see the invite was created but the delivery bounced. Re-sending now and checking the domain's SPF record.",
|
|
119
|
+
fromCustomer: false,
|
|
120
|
+
age: 5,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
ticket: "invite",
|
|
124
|
+
body: "Their MX is misconfigured — flagging for the infra team rather than sending again blind.",
|
|
125
|
+
fromCustomer: false,
|
|
126
|
+
internal: true,
|
|
127
|
+
age: 5,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
ticket: "invoice",
|
|
131
|
+
body: "We upgraded on the 3rd but the invoice still lists the old plan. Can you re-issue?",
|
|
132
|
+
fromCustomer: true,
|
|
133
|
+
age: 26,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
ticket: "invoice",
|
|
137
|
+
body: "Re-issued and emailed — the proration lands on next month's invoice.",
|
|
138
|
+
fromCustomer: false,
|
|
139
|
+
age: 24,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
ticket: "sso",
|
|
143
|
+
body: "We're standardising on Google Workspace this quarter. Does your plan include SAML, or only OAuth sign-in?",
|
|
144
|
+
fromCustomer: true,
|
|
145
|
+
age: 14,
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
export interface ShapedSeed {
|
|
150
|
+
customers: Array<{ key: string; row: Record<string, unknown> }>;
|
|
151
|
+
tickets: Array<{ key: string; customer: string; row: Record<string, unknown> }>;
|
|
152
|
+
messages: Array<{ ticket: string; row: Record<string, unknown> }>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function shapeSeed(now: number = Date.now()): ShapedSeed {
|
|
156
|
+
const hoursAgo = (hours: number) => new Date(now - hours * 3_600_000).toISOString();
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
customers: SEED_CUSTOMERS.map((c) => ({
|
|
160
|
+
key: c.key,
|
|
161
|
+
row: { name: c.name, email: c.email, company: c.company, createdAt: hoursAgo(720) },
|
|
162
|
+
})),
|
|
163
|
+
tickets: SEED_TICKETS.map((t) => ({
|
|
164
|
+
key: t.key,
|
|
165
|
+
customer: t.customer,
|
|
166
|
+
row: {
|
|
167
|
+
subject: t.subject,
|
|
168
|
+
status: t.status,
|
|
169
|
+
priority: t.priority,
|
|
170
|
+
firstRespondedAt: t.respondedAgo === null ? null : hoursAgo(t.respondedAgo),
|
|
171
|
+
createdAt: hoursAgo(t.age),
|
|
172
|
+
updatedAt: hoursAgo(t.respondedAgo ?? t.age),
|
|
173
|
+
},
|
|
174
|
+
})),
|
|
175
|
+
messages: SEED_MESSAGES.map((m) => ({
|
|
176
|
+
ticket: m.ticket,
|
|
177
|
+
row: {
|
|
178
|
+
body: m.body,
|
|
179
|
+
fromCustomer: m.fromCustomer,
|
|
180
|
+
internal: m.internal ?? false,
|
|
181
|
+
createdAt: hoursAgo(m.age),
|
|
182
|
+
},
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// The support model: statuses, priority, SLA, and the queue ordering.
|
|
2
|
+
//
|
|
3
|
+
// Pure — no React, no `db`. The rules that decide what an agent sees first are
|
|
4
|
+
// the most important logic in a helpdesk, so they live where they can be tested
|
|
5
|
+
// exhaustively without a server.
|
|
6
|
+
|
|
7
|
+
export interface Status {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
/** Solved and closed leave the queue. */
|
|
11
|
+
terminal?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const STATUSES: Status[] = [
|
|
15
|
+
{ id: "open", label: "Open" },
|
|
16
|
+
{ id: "pending", label: "Pending" },
|
|
17
|
+
{ id: "solved", label: "Solved", terminal: true },
|
|
18
|
+
{ id: "closed", label: "Closed", terminal: true },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export interface Priority {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
/** Lower sorts first. */
|
|
25
|
+
rank: number;
|
|
26
|
+
/** Hours allowed for a first reply. */
|
|
27
|
+
firstResponseHours: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const PRIORITIES: Priority[] = [
|
|
31
|
+
{ id: "urgent", label: "Urgent", rank: 0, firstResponseHours: 1 },
|
|
32
|
+
{ id: "high", label: "High", rank: 1, firstResponseHours: 4 },
|
|
33
|
+
{ id: "normal", label: "Normal", rank: 2, firstResponseHours: 24 },
|
|
34
|
+
{ id: "low", label: "Low", rank: 3, firstResponseHours: 72 },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export interface Ticket {
|
|
38
|
+
id: string;
|
|
39
|
+
subject: string;
|
|
40
|
+
status: string;
|
|
41
|
+
priority: string;
|
|
42
|
+
customerId?: string | null;
|
|
43
|
+
assigneeId?: string | null;
|
|
44
|
+
createdAt?: string | null;
|
|
45
|
+
updatedAt?: string | null;
|
|
46
|
+
/** When an agent first replied; null while the customer is still waiting. */
|
|
47
|
+
firstRespondedAt?: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function statusById(id: string): Status | undefined {
|
|
51
|
+
return STATUSES.find((s) => s.id === id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function priorityById(id: string): Priority | undefined {
|
|
55
|
+
return PRIORITIES.find((p) => p.id === id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isValidStatus(id: string): boolean {
|
|
59
|
+
return STATUSES.some((s) => s.id === id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isValidPriority(id: string): boolean {
|
|
63
|
+
return PRIORITIES.some((p) => p.id === id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isOpen(ticket: { status: string }): boolean {
|
|
67
|
+
return !statusById(ticket.status)?.terminal;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type SlaState = "met" | "due" | "breached" | "none";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Where a ticket stands against its first-response target.
|
|
74
|
+
*
|
|
75
|
+
* met — an agent has already replied
|
|
76
|
+
* breached — no reply and the window has passed
|
|
77
|
+
* due — no reply, inside the window
|
|
78
|
+
* none — terminal, or unparseable dates; nothing to chase
|
|
79
|
+
*
|
|
80
|
+
* A solved ticket that was answered late is still `met`: the SLA question is
|
|
81
|
+
* "did we reply", and re-flagging closed work just adds noise to the queue.
|
|
82
|
+
*/
|
|
83
|
+
export function slaState(
|
|
84
|
+
ticket: Ticket,
|
|
85
|
+
now: number = Date.now(),
|
|
86
|
+
): SlaState {
|
|
87
|
+
if (!isOpen(ticket)) return "none";
|
|
88
|
+
if (ticket.firstRespondedAt) return "met";
|
|
89
|
+
const created = Date.parse(ticket.createdAt ?? "");
|
|
90
|
+
if (!Number.isFinite(created)) return "none";
|
|
91
|
+
const hours = priorityById(ticket.priority)?.firstResponseHours;
|
|
92
|
+
if (hours === undefined) return "none";
|
|
93
|
+
return now > created + hours * 3_600_000 ? "breached" : "due";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Minutes left before the first-response target, negative once breached. */
|
|
97
|
+
export function minutesToBreach(
|
|
98
|
+
ticket: Ticket,
|
|
99
|
+
now: number = Date.now(),
|
|
100
|
+
): number | null {
|
|
101
|
+
if (!isOpen(ticket) || ticket.firstRespondedAt) return null;
|
|
102
|
+
const created = Date.parse(ticket.createdAt ?? "");
|
|
103
|
+
const hours = priorityById(ticket.priority)?.firstResponseHours;
|
|
104
|
+
if (!Number.isFinite(created) || hours === undefined) return null;
|
|
105
|
+
return Math.round((created + hours * 3_600_000 - now) / 60_000);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Queue order: breached first, then by priority, then oldest first.
|
|
110
|
+
*
|
|
111
|
+
* Oldest-first within a priority is deliberate — sorting newest-first is how a
|
|
112
|
+
* ticket sits at the bottom of a busy queue for a week.
|
|
113
|
+
*/
|
|
114
|
+
export function queueOrder(tickets: Ticket[], now: number = Date.now()): Ticket[] {
|
|
115
|
+
return [...tickets].sort((a, b) => {
|
|
116
|
+
const aBreached = slaState(a, now) === "breached" ? 0 : 1;
|
|
117
|
+
const bBreached = slaState(b, now) === "breached" ? 0 : 1;
|
|
118
|
+
if (aBreached !== bBreached) return aBreached - bBreached;
|
|
119
|
+
|
|
120
|
+
const aRank = priorityById(a.priority)?.rank ?? 99;
|
|
121
|
+
const bRank = priorityById(b.priority)?.rank ?? 99;
|
|
122
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
123
|
+
|
|
124
|
+
return (a.createdAt ?? "").localeCompare(b.createdAt ?? "");
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface QueueCounts {
|
|
129
|
+
open: number;
|
|
130
|
+
pending: number;
|
|
131
|
+
solved: number;
|
|
132
|
+
closed: number;
|
|
133
|
+
unassigned: number;
|
|
134
|
+
breached: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function counts(tickets: Ticket[], now: number = Date.now()): QueueCounts {
|
|
138
|
+
const result: QueueCounts = {
|
|
139
|
+
open: 0,
|
|
140
|
+
pending: 0,
|
|
141
|
+
solved: 0,
|
|
142
|
+
closed: 0,
|
|
143
|
+
unassigned: 0,
|
|
144
|
+
breached: 0,
|
|
145
|
+
};
|
|
146
|
+
for (const ticket of tickets) {
|
|
147
|
+
if (ticket.status in result) {
|
|
148
|
+
result[ticket.status as keyof QueueCounts] += 1;
|
|
149
|
+
}
|
|
150
|
+
if (isOpen(ticket) && !ticket.assigneeId) result.unassigned += 1;
|
|
151
|
+
if (slaState(ticket, now) === "breached") result.breached += 1;
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface Filter {
|
|
157
|
+
status?: string;
|
|
158
|
+
priority?: string;
|
|
159
|
+
assigneeId?: string;
|
|
160
|
+
/** "unassigned" narrows to tickets with no agent. */
|
|
161
|
+
unassigned?: boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function applyFilter(tickets: Ticket[], filter: Filter): Ticket[] {
|
|
165
|
+
return tickets.filter((ticket) => {
|
|
166
|
+
if (filter.status && ticket.status !== filter.status) return false;
|
|
167
|
+
if (filter.priority && ticket.priority !== filter.priority) return false;
|
|
168
|
+
if (filter.assigneeId && ticket.assigneeId !== filter.assigneeId) return false;
|
|
169
|
+
if (filter.unassigned && ticket.assigneeId) return false;
|
|
170
|
+
return true;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** "#1042" — short, stable, and greppable in an email thread. */
|
|
175
|
+
export function ticketNumber(id: string, createdAt?: string | null): string {
|
|
176
|
+
// Derived from the id so it needs no counter and can't collide across
|
|
177
|
+
// concurrent inserts; the created date is only a tiebreaker for display.
|
|
178
|
+
let hash = 0;
|
|
179
|
+
const seed = `${id}${createdAt ?? ""}`;
|
|
180
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
181
|
+
hash = (hash * 31 + seed.charCodeAt(i)) | 0;
|
|
182
|
+
}
|
|
183
|
+
return `#${(Math.abs(hash) % 9000) + 1000}`;
|
|
184
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
// `cn` — the shadcn class merger. clsx resolves conditional/array class
|
|
5
|
+
// inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
|
|
6
|
+
// the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
|
|
7
|
+
// component routes its className through this.
|
|
8
|
+
export function cn(...inputs: ClassValue[]) {
|
|
9
|
+
return twMerge(clsx(inputs));
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME_KEBAB__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"deploy": "pylon deploy",
|
|
9
|
+
"check": "tsc --noEmit",
|
|
10
|
+
"test": "pylon test"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@pylonsync/react": "^__PYLON_VERSION__",
|
|
14
|
+
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
15
|
+
"@pylonsync/functions": "^__PYLON_VERSION__",
|
|
16
|
+
"@pylonsync/client": "^__PYLON_VERSION__",
|
|
17
|
+
"react": "^19.0.0",
|
|
18
|
+
"react-dom": "^19.0.0",
|
|
19
|
+
"tailwindcss": "^4.3.0",
|
|
20
|
+
"@tailwindcss/cli": "^4.3.0",
|
|
21
|
+
"tw-animate-css": "^1.2.0",
|
|
22
|
+
"class-variance-authority": "^0.7.1",
|
|
23
|
+
"clsx": "^2.1.1",
|
|
24
|
+
"tailwind-merge": "^2.5.0",
|
|
25
|
+
"lucide-react": "^0.460.0",
|
|
26
|
+
"@radix-ui/react-dialog": "^1.1.4",
|
|
27
|
+
"@radix-ui/react-slot": "^1.1.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@happy-dom/global-registrator": "^20.10.0",
|
|
31
|
+
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
32
|
+
"@testing-library/dom": "^10.4.0",
|
|
33
|
+
"@testing-library/react": "^16.3.0",
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
35
|
+
"@types/react": "^19.0.0",
|
|
36
|
+
"@types/react-dom": "^19.0.0",
|
|
37
|
+
"typescript": "^5.6.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
// This project uses the classic JSX transform, so .tsx tests import React.
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
|
|
5
|
+
import { TicketList } from "../components/ticket-list";
|
|
6
|
+
import { Thread } from "../components/thread";
|
|
7
|
+
import { SlaIndicator } from "../components/sla-indicator";
|
|
8
|
+
import type { Ticket } from "../lib/tickets";
|
|
9
|
+
|
|
10
|
+
afterEach(cleanup);
|
|
11
|
+
|
|
12
|
+
// Tier 2: components. No mocking needed — they take data as props and report
|
|
13
|
+
// changes through callbacks, because the container owns `db`.
|
|
14
|
+
|
|
15
|
+
const NOW = Date.parse("2026-07-27T12:00:00Z");
|
|
16
|
+
const hoursAgo = (h: number) => new Date(NOW - h * 3_600_000).toISOString();
|
|
17
|
+
const noName = () => null;
|
|
18
|
+
|
|
19
|
+
const tickets: Ticket[] = [
|
|
20
|
+
{
|
|
21
|
+
id: "breached",
|
|
22
|
+
subject: "Export is timing out",
|
|
23
|
+
status: "open",
|
|
24
|
+
priority: "urgent",
|
|
25
|
+
createdAt: hoursAgo(3),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "calm",
|
|
29
|
+
subject: "Mobile layout is off",
|
|
30
|
+
status: "open",
|
|
31
|
+
priority: "low",
|
|
32
|
+
createdAt: hoursAgo(1),
|
|
33
|
+
firstRespondedAt: hoursAgo(1),
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
describe("TicketList", () => {
|
|
38
|
+
test("puts the breached ticket at the top", () => {
|
|
39
|
+
// The queue's whole job is to answer "what next" — newest-first would bury
|
|
40
|
+
// the overdue one.
|
|
41
|
+
render(
|
|
42
|
+
<TicketList
|
|
43
|
+
tickets={tickets}
|
|
44
|
+
customerName={noName}
|
|
45
|
+
assigneeName={noName}
|
|
46
|
+
now={NOW}
|
|
47
|
+
onSelect={() => {}}
|
|
48
|
+
/>,
|
|
49
|
+
);
|
|
50
|
+
const rows = screen.getAllByRole("button");
|
|
51
|
+
expect(within(rows[0]).getByText("Export is timing out")).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("marks a ticket with no agent as unassigned", () => {
|
|
55
|
+
render(
|
|
56
|
+
<TicketList
|
|
57
|
+
tickets={tickets}
|
|
58
|
+
customerName={noName}
|
|
59
|
+
assigneeName={noName}
|
|
60
|
+
now={NOW}
|
|
61
|
+
onSelect={() => {}}
|
|
62
|
+
/>,
|
|
63
|
+
);
|
|
64
|
+
expect(screen.getAllByText("Unassigned").length).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("selecting a row reports its id", () => {
|
|
68
|
+
const picked: string[] = [];
|
|
69
|
+
render(
|
|
70
|
+
<TicketList
|
|
71
|
+
tickets={tickets}
|
|
72
|
+
customerName={noName}
|
|
73
|
+
assigneeName={noName}
|
|
74
|
+
now={NOW}
|
|
75
|
+
onSelect={(id) => picked.push(id)}
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
fireEvent.click(screen.getByText("Mobile layout is off"));
|
|
79
|
+
expect(picked).toEqual(["calm"]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("SlaIndicator", () => {
|
|
84
|
+
test("shows how far past the window a breach is", () => {
|
|
85
|
+
const { container } = render(
|
|
86
|
+
<SlaIndicator ticket={tickets[0]} now={NOW} />,
|
|
87
|
+
);
|
|
88
|
+
expect(container.textContent).toContain("over");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("renders nothing once answered", () => {
|
|
92
|
+
// A badge on every row is a badge that means nothing.
|
|
93
|
+
const { container } = render(<SlaIndicator ticket={tickets[1]} now={NOW} />);
|
|
94
|
+
expect(container.firstChild).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("renders nothing for a closed ticket", () => {
|
|
98
|
+
const { container } = render(
|
|
99
|
+
<SlaIndicator
|
|
100
|
+
ticket={{ ...tickets[0], status: "closed" }}
|
|
101
|
+
now={NOW}
|
|
102
|
+
/>,
|
|
103
|
+
);
|
|
104
|
+
expect(container.firstChild).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("Thread", () => {
|
|
109
|
+
const messages = [
|
|
110
|
+
{ id: "m1", body: "It broke", fromCustomer: true, createdAt: hoursAgo(3) },
|
|
111
|
+
{ id: "m2", body: "Looking now", fromCustomer: false, createdAt: hoursAgo(2) },
|
|
112
|
+
{ id: "m3", body: "Their MX is wrong", fromCustomer: false, internal: true, createdAt: hoursAgo(1) },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
test("renders oldest first", () => {
|
|
116
|
+
render(
|
|
117
|
+
<Thread
|
|
118
|
+
messages={messages}
|
|
119
|
+
authorName={() => "Agent"}
|
|
120
|
+
customerName="Dana"
|
|
121
|
+
now={NOW}
|
|
122
|
+
onSend={() => {}}
|
|
123
|
+
/>,
|
|
124
|
+
);
|
|
125
|
+
const articles = screen.getAllByRole("article");
|
|
126
|
+
expect(within(articles[0]).getByText("It broke")).toBeDefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("marks an internal note so it can't be mistaken for a reply", () => {
|
|
130
|
+
render(
|
|
131
|
+
<Thread
|
|
132
|
+
messages={messages}
|
|
133
|
+
authorName={() => "Agent"}
|
|
134
|
+
customerName="Dana"
|
|
135
|
+
now={NOW}
|
|
136
|
+
onSend={() => {}}
|
|
137
|
+
/>,
|
|
138
|
+
);
|
|
139
|
+
// Scoped to the message: the composer's toggle carries the same label, so a
|
|
140
|
+
// bare getByText would match twice and throw.
|
|
141
|
+
const notes = screen
|
|
142
|
+
.getAllByRole("article")
|
|
143
|
+
.filter((article) => within(article).queryByText("Internal note"));
|
|
144
|
+
expect(notes).toHaveLength(1);
|
|
145
|
+
expect(within(notes[0]).getByText("Their MX is wrong")).toBeDefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("sends a public reply by default", () => {
|
|
149
|
+
const sent: Array<[string, boolean]> = [];
|
|
150
|
+
render(
|
|
151
|
+
<Thread
|
|
152
|
+
messages={[]}
|
|
153
|
+
authorName={() => "Agent"}
|
|
154
|
+
customerName="Dana"
|
|
155
|
+
now={NOW}
|
|
156
|
+
onSend={(body, internal) => sent.push([body, internal])}
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
fireEvent.change(screen.getByLabelText("Reply"), {
|
|
160
|
+
target: { value: "On it" },
|
|
161
|
+
});
|
|
162
|
+
fireEvent.click(screen.getByRole("button", { name: /send reply/i }));
|
|
163
|
+
expect(sent).toEqual([["On it", false]]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("the internal toggle switches the composer and the flag", () => {
|
|
167
|
+
// Sending an internal note to the customer is the expensive mistake here,
|
|
168
|
+
// so the mode has to be visible AND actually carried through.
|
|
169
|
+
const sent: Array<[string, boolean]> = [];
|
|
170
|
+
render(
|
|
171
|
+
<Thread
|
|
172
|
+
messages={[]}
|
|
173
|
+
authorName={() => "Agent"}
|
|
174
|
+
customerName="Dana"
|
|
175
|
+
now={NOW}
|
|
176
|
+
onSend={(body, internal) => sent.push([body, internal])}
|
|
177
|
+
/>,
|
|
178
|
+
);
|
|
179
|
+
fireEvent.click(screen.getByLabelText("Internal note"));
|
|
180
|
+
fireEvent.change(screen.getByLabelText("Internal note", { selector: "textarea" }), {
|
|
181
|
+
target: { value: "MX is broken" },
|
|
182
|
+
});
|
|
183
|
+
fireEvent.click(screen.getByRole("button", { name: /add note/i }));
|
|
184
|
+
expect(sent).toEqual([["MX is broken", true]]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("an empty reply is not sent", () => {
|
|
188
|
+
const sent: unknown[] = [];
|
|
189
|
+
render(
|
|
190
|
+
<Thread
|
|
191
|
+
messages={[]}
|
|
192
|
+
authorName={() => "Agent"}
|
|
193
|
+
customerName="Dana"
|
|
194
|
+
now={NOW}
|
|
195
|
+
onSend={() => sent.push(1)}
|
|
196
|
+
/>,
|
|
197
|
+
);
|
|
198
|
+
fireEvent.change(screen.getByLabelText("Reply"), { target: { value: " " } });
|
|
199
|
+
fireEvent.click(screen.getByRole("button", { name: /send reply/i }));
|
|
200
|
+
expect(sent).toEqual([]);
|
|
201
|
+
});
|
|
202
|
+
});
|