feat: restructure sales, add PDF quote generation and WebSocket support

This commit is contained in:
2026-03-06 23:25:37 -06:00
parent 4efca6cc53
commit 1907bb433b
73 changed files with 8115 additions and 170 deletions
+20 -12
View File
@@ -136,18 +136,25 @@ Admin-specific UI permissions that control visibility and data loading for admin
Permissions for accessing and managing sales opportunities. Opportunities are synced from ConnectWise and stored locally; sub-resources (products, notes, contacts) are fetched live from CW.
| Permission Node | Description | Used In | Dependencies |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
| `sales.opportunity.fetch` | Fetch a single opportunity and its CW sub-resources (products, notes, contacts) | [src/api/sales/[id]/fetch.ts](src/api/sales/[id]/fetch.ts), [src/api/sales/[id]/products.ts](src/api/sales/[id]/products.ts), [src/api/sales/[id]/notes.ts](src/api/sales/[id]/notes.ts), [src/api/sales/[id]/fetchNote.ts](src/api/sales/[id]/fetchNote.ts), [src/api/sales/[id]/contacts.ts](src/api/sales/[id]/contacts.ts) | |
| `sales.opportunity.fetch.many` | Fetch multiple opportunities (paginated/searchable), count, or opportunity types | [src/api/sales/fetchAll.ts](src/api/sales/fetchAll.ts), [src/api/sales/count.ts](src/api/sales/count.ts), [src/api/sales/fetchOpportunityTypes.ts](src/api/sales/fetchOpportunityTypes.ts) | |
| `sales.opportunity.refresh` | Refresh a single opportunity's local data from ConnectWise | [src/api/sales/[id]/refresh.ts](src/api/sales/[id]/refresh.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.note.create` | Create a new note on an opportunity | [src/api/sales/[id]/createNote.ts](src/api/sales/[id]/createNote.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.note.update` | Update an existing note on an opportunity | [src/api/sales/[id]/updateNote.ts](src/api/sales/[id]/updateNote.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.note.delete` | Delete a note from an opportunity | [src/api/sales/[id]/deleteNote.ts](src/api/sales/[id]/deleteNote.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.product.update` | Update products (forecast items) on an opportunity, including resequencing | [src/api/sales/[id]/resequenceProducts.ts](src/api/sales/[id]/resequenceProducts.ts), [src/api/sales/[id]/updateProduct.ts](src/api/sales/[id]/updateProduct.ts), [src/api/sales/[id]/cancelProduct.ts](src/api/sales/[id]/cancelProduct.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.product.add` | Add a new product (forecast item) to an opportunity. Individual fields gated by `sales.opportunity.product.field.<field>` permissions. | [src/api/sales/[id]/addProduct.ts](src/api/sales/[id]/addProduct.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.product.add.specialOrder` | Add one or more "SPECIAL ORDER" products via the dedicated special-order route. | [src/api/sales/[id]/addSpecialOrderProduct.ts](src/api/sales/[id]/addSpecialOrderProduct.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.product.add.labor` | Add labor products via the dedicated labor route with Field/Tech catalog selection and labor pricing inputs. | [src/api/sales/[id]/addLabor.ts](src/api/sales/[id]/addLabor.ts), [src/api/sales/[id]/laborOptions.ts](src/api/sales/[id]/laborOptions.ts) | `sales.opportunity.fetch` |
**WebSocket note:** The `/secure` socket event chain `opp:live_quote_preview` and `opp:live_quote_preview:<id>:data` is gated by `sales.opportunity.fetch`.
| Permission Node | Description | Used In | Dependencies |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
| `sales.opportunity.fetch` | Fetch a single opportunity and its CW sub-resources (products, notes, contacts) | [src/api/sales/opportunities/[id]/fetch.ts](src/api/sales/opportunities/[id]/fetch.ts), [src/api/sales/opportunities/[id]/products/fetchAll.ts](src/api/sales/opportunities/[id]/products/fetchAll.ts), [src/api/sales/opportunities/[id]/notes/fetchAll.ts](src/api/sales/opportunities/[id]/notes/fetchAll.ts), [src/api/sales/opportunities/[id]/notes/fetch.ts](src/api/sales/opportunities/[id]/notes/fetch.ts), [src/api/sales/opportunities/[id]/contacts/fetchAll.ts](src/api/sales/opportunities/[id]/contacts/fetchAll.ts), [src/api/sockets/events/liveQuotePreview.ts](src/api/sockets/events/liveQuotePreview.ts) | |
| `sales.opportunity.fetch.many` | Fetch multiple opportunities (paginated/searchable), count, or opportunity types | [src/api/sales/opportunities/fetchAll.ts](src/api/sales/opportunities/fetchAll.ts), [src/api/sales/opportunities/count.ts](src/api/sales/opportunities/count.ts), [src/api/sales/opportunities/fetchTypes.ts](src/api/sales/opportunities/fetchTypes.ts) | |
| `sales.opportunity.refresh` | Refresh a single opportunity's local data from ConnectWise | [src/api/sales/opportunities/[id]/refresh.ts](src/api/sales/opportunities/[id]/refresh.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.note.create` | Create a new note on an opportunity | [src/api/sales/opportunities/[id]/notes/create.ts](src/api/sales/opportunities/[id]/notes/create.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.note.update` | Update an existing note on an opportunity | [src/api/sales/opportunities/[id]/notes/update.ts](src/api/sales/opportunities/[id]/notes/update.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.note.delete` | Delete a note from an opportunity | [src/api/sales/opportunities/[id]/notes/delete.ts](src/api/sales/opportunities/[id]/notes/delete.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.product.update` | Update products (forecast items) on an opportunity, including resequencing | [src/api/sales/opportunities/[id]/products/resequence.ts](src/api/sales/opportunities/[id]/products/resequence.ts), [src/api/sales/opportunities/[id]/products/update.ts](src/api/sales/opportunities/[id]/products/update.ts), [src/api/sales/opportunities/[id]/products/cancel.ts](src/api/sales/opportunities/[id]/products/cancel.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.product.add` | Add a new product (forecast item) to an opportunity. Individual fields gated by `sales.opportunity.product.field.<field>` permissions. | [src/api/sales/opportunities/[id]/products/add.ts](src/api/sales/opportunities/[id]/products/add.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.product.add.specialOrder` | Add one or more "SPECIAL ORDER" products via the dedicated special-order route. | [src/api/sales/opportunities/[id]/products/addSpecialOrder.ts](src/api/sales/opportunities/[id]/products/addSpecialOrder.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.product.add.labor` | Add labor products via the dedicated labor route with Field/Tech catalog selection and labor pricing inputs. | [src/api/sales/opportunities/[id]/products/addLabor.ts](src/api/sales/opportunities/[id]/products/addLabor.ts), [src/api/sales/opportunities/[id]/products/laborOptions.ts](src/api/sales/opportunities/[id]/products/laborOptions.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.quote.fetch` | Fetch all committed quotes for an opportunity. | [src/api/sales/opportunities/[id]/quotes/fetchAll.ts](src/api/sales/opportunities/[id]/quotes/fetchAll.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.quote.commit` | Generate and store a finalized quote PDF for an opportunity with regeneration metadata and creator attribution. | [src/api/sales/opportunities/[id]/quotes/commit.ts](src/api/sales/opportunities/[id]/quotes/commit.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.quote.preview` | Generate a preview-stamped quote PDF for an opportunity without storing it. | [src/api/sales/opportunities/[id]/quotes/preview.ts](src/api/sales/opportunities/[id]/quotes/preview.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.quote.download` | Download a committed quote PDF. Each download is recorded with timestamp and user info. | [src/api/sales/opportunities/[id]/quotes/download.ts](src/api/sales/opportunities/[id]/quotes/download.ts) | `sales.opportunity.fetch` |
| `sales.opportunity.quote.fetch_downloads` | Fetch download/print history for all quotes on an opportunity. Admin-level permission. | [src/api/sales/opportunities/[id]/quotes/fetchDownloads.ts](src/api/sales/opportunities/[id]/quotes/fetchDownloads.ts) | `sales.opportunity.fetch` |
<details>
<summary><strong>Field-level permissions for <code>sales.opportunity.product.add</code></strong></summary>
@@ -349,6 +356,7 @@ All fetch and fetchAll routes gate response object keys using `processObjectValu
| `obj.opportunity.site` | View site |
| `obj.opportunity.customerPO` | View customer PO |
| `obj.opportunity.totalSalesTax` | View total sales tax |
| `obj.opportunity.probability` | View probability percentage |
| `obj.opportunity.location` | View location |
| `obj.opportunity.department` | View department |
| `obj.opportunity.expectedCloseDate` | View expected close date |