[{"data":1,"prerenderedAt":2615},["ShallowReactive",2],{"navigation":3,"\u002Fexamples\u002Fsales-invoices":189,"\u002Fexamples\u002Fsales-invoices-surround":2610},[4,23,40,51,74,116,157,177],{"title":5,"path":6,"stem":7,"children":8,"icon":22},"Get Started","\u002Fget-started","1.get-started\u002F1.index",[9,12,17],{"title":10,"path":6,"stem":7,"icon":11},"Introduction","i-lucide-house",{"title":13,"path":14,"stem":15,"icon":16},"Prerequisites","\u002Fget-started\u002Fprerequisites","1.get-started\u002F2.prerequisites","i-lucide-list-checks",{"title":18,"path":19,"stem":20,"icon":21},"Installation","\u002Fget-started\u002Finstallation","1.get-started\u002F3.installation","i-lucide-settings","i-lucide-rocket",{"title":24,"icon":25,"path":26,"stem":27,"children":28,"page":39},"Develop","i-lucide-code","\u002Fdevelop","2.develop",[29,34],{"title":30,"path":31,"stem":32,"icon":33},"Version Control","\u002Fdevelop\u002Fversion-control","2.develop\u002F1.version-control","i-lucide-git-branch",{"title":35,"path":36,"stem":37,"icon":38},"Claude Code","\u002Fdevelop\u002Fclaude-code","2.develop\u002F2.claude-code","i-lucide-sparkles",false,{"title":41,"icon":42,"path":43,"stem":44,"children":45,"page":39},"Launch","i-lucide-globe","\u002Flaunch","3.launch",[46],{"title":47,"path":48,"stem":49,"icon":50},"Cloudflare","\u002Flaunch\u002Fcloudflare","3.launch\u002F1.cloudflare","i-lucide-cloud-upload",{"title":52,"path":53,"stem":54,"children":55,"icon":73},"Plugins","\u002Fplugins","4.plugins\u002F1.index",[56,58,63,68],{"title":52,"path":53,"stem":54,"icon":57},"i-lucide-list",{"title":59,"path":60,"stem":61,"icon":62},"Public API","\u002Fplugins\u002Fapi-keys","4.plugins\u002F2.api-keys","i-lucide-key",{"title":64,"path":65,"stem":66,"icon":67},"Cron Jobs","\u002Fplugins\u002Fcron-jobs","4.plugins\u002F4.cron-jobs","i-lucide-clock",{"title":69,"path":70,"stem":71,"icon":72},"Rate Limiting","\u002Fplugins\u002Frate-limiting","4.plugins\u002F5.rate-limiting","i-lucide-gauge","i-lucide-puzzle",{"title":75,"path":76,"stem":77,"children":78,"icon":115},"Examples","\u002Fexamples","5.examples\u002F1.index",[79,80,85,90,95,100,105,110],{"title":75,"path":76,"stem":77,"icon":57},{"title":81,"path":82,"stem":83,"icon":84},"Job Management","\u002Fexamples\u002Fjob-management","5.examples\u002F2.job-management","i-lucide-briefcase",{"title":86,"path":87,"stem":88,"icon":89},"Kanban \u002F To-Do List","\u002Fexamples\u002Fkanban-todo","5.examples\u002F3.kanban-todo","i-lucide-kanban",{"title":91,"path":92,"stem":93,"icon":94},"Inventory Management","\u002Fexamples\u002Finventory-management","5.examples\u002F4.inventory-management","i-lucide-package",{"title":96,"path":97,"stem":98,"icon":99},"Mini CRM","\u002Fexamples\u002Fmini-crm","5.examples\u002F5.mini-crm","i-lucide-users",{"title":101,"path":102,"stem":103,"icon":104},"Sales Orders & Invoices","\u002Fexamples\u002Fsales-invoices","5.examples\u002F6.sales-invoices","i-lucide-receipt",{"title":106,"path":107,"stem":108,"icon":109},"Calendar & Booking","\u002Fexamples\u002Fcalendar-booking","5.examples\u002F7.calendar-booking","i-lucide-calendar",{"title":111,"path":112,"stem":113,"icon":114},"Support Tickets","\u002Fexamples\u002Fsupport-tickets","5.examples\u002F8.support-tickets","i-lucide-life-buoy","i-lucide-book-open",{"title":117,"icon":118,"path":119,"stem":120,"children":121,"page":39},"Reference","i-lucide-file-text","\u002Freference","6.reference",[122,127,132,137,142,147,152],{"title":123,"path":124,"stem":125,"icon":126},"Architecture","\u002Freference\u002Farchitecture","6.reference\u002F1.architecture","i-lucide-layers",{"title":128,"path":129,"stem":130,"icon":131},"Permissions","\u002Freference\u002Fpermissions","6.reference\u002F2.permissions","i-lucide-shield",{"title":133,"path":134,"stem":135,"icon":136},"Invitations","\u002Freference\u002Finvitations","6.reference\u002F3.invitations","i-lucide-mail",{"title":138,"path":139,"stem":140,"icon":141},"Webhooks","\u002Freference\u002Fwebhooks","6.reference\u002F4.webhooks","i-lucide-webhook",{"title":143,"path":144,"stem":145,"icon":146},"AI Chat","\u002Freference\u002Fai-chat","6.reference\u002F5.ai-chat","i-lucide-message-square",{"title":148,"path":149,"stem":150,"icon":151},"Activity Log","\u002Freference\u002Factivity-log","6.reference\u002F6.activity-log","i-lucide-scroll",{"title":153,"path":154,"stem":155,"icon":156},"Manual Setup","\u002Freference\u002Fmanual-setup","6.reference\u002F7.manual-setup","i-lucide-wrench",{"title":158,"icon":159,"path":160,"stem":161,"children":162,"page":39},"Legal","i-lucide-scale","\u002Flegal","7.legal",[163,168,172],{"title":164,"path":165,"stem":166,"icon":167},"License","\u002Flegal\u002Flicense","7.legal\u002F1.license","i-lucide-file-check",{"title":169,"path":170,"stem":171,"icon":118},"Terms and Conditions","\u002Flegal\u002Fterms","7.legal\u002F2.terms",{"title":173,"path":174,"stem":175,"icon":176},"Privacy Policy","\u002Flegal\u002Fprivacy","7.legal\u002F3.privacy","i-lucide-lock",{"title":178,"path":179,"stem":180,"children":181,"icon":183},"Upgrades","\u002Fupgrades","8.upgrades\u002F1.index",[182,184],{"title":178,"path":179,"stem":180,"icon":183},"i-lucide-arrow-up-circle",{"title":185,"path":186,"stem":187,"icon":188},"\u002Fapp\u002F* gated subtree routing","\u002Fupgrades\u002Fapp-subtree-routing","8.upgrades\u002F2.app-subtree-routing","i-lucide-route",{"id":190,"title":101,"body":191,"description":2603,"extension":2604,"links":2605,"meta":2606,"navigation":2607,"path":102,"seo":2608,"stem":103,"__hash__":2609},"docs\u002F5.examples\u002F6.sales-invoices.md",{"type":192,"value":193,"toc":2599},"minimark",[194,210,213,216,219,253,2524,2528,2531,2564,2571,2575,2595],[195,196,197],"note",{},[198,199,200,204,205,209],"p",{},[201,202,203],"strong",{},"Prerequisite:"," Complete the ",[206,207,5],"a",{"href":208},"\u002Fget-started\u002F"," guide first.",[198,211,212],{},"To build this feature, copy the first prompt, paste it into Claude Code (or Codex, or any AI coding tool), let it finish, then run the next prompt.",[198,214,215],{},"A sales order and invoicing system is what any product or service business uses to manage the money side. Think freelancers, agencies, retailers, wholesalers, or any business that creates quotes, takes orders, sends invoices, and tracks payments.",[198,217,218],{},"By the end of this example you will have:",[220,221,222,229,235,241,247],"ul",{},[223,224,225,228],"li",{},[201,226,227],{},"Customers"," — full CRUD for the people and businesses you sell to",[223,230,231,234],{},[201,232,233],{},"Orders"," — sales orders with line items, totals, and status tracking",[223,236,237,240],{},[201,238,239],{},"Invoices"," — generated from orders with payment tracking and due dates",[223,242,243,246],{},[201,244,245],{},"Sales dashboard"," — real stats showing revenue, outstanding invoices, and order pipeline",[223,248,249,252],{},[201,250,251],{},"Realtime"," — all changes sync instantly across connected browsers",[254,255,257,262,1009,1021,1024,1290,1293,1712,1716,2107,2111,2116,2341,2344],"steps",{"level":256},"2",[258,259,261],"h2",{"id":260},"database-schema-and-permissions","Database Schema and Permissions",[263,264,269],"pre",{"className":265,"code":266,"language":267,"meta":268,"style":268},"language-txt shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","We are building a sales order and invoicing app on top of this template.\nCreate the database schema and add the permissions we need.\n\nDatabase (via Supabase MCP):\n\nCreate five tables:\n\n1. `customers` table — id (uuid, default gen_random_uuid(), primary key),\n   team_id (uuid, references teams(id) on delete cascade, not null),\n   name (text, not null), email (text, nullable), phone (text, nullable),\n   company (text, nullable), address (text, nullable),\n   billing_address (text, nullable),\n   tax_id (text, nullable),\n   notes (text, nullable),\n   created_by (uuid, references profiles(id), not null),\n   created_at (timestamptz, default now()),\n   updated_at (timestamptz, default now()).\n\n2. `orders` table — id (uuid, default gen_random_uuid(), primary key),\n   team_id (uuid, references teams(id) on delete cascade, not null),\n   order_number (serial, not null),\n   customer_id (uuid, references customers(id) on delete cascade, not null),\n   status (text, check status in ('draft', 'confirmed', 'fulfilled',\n   'cancelled'), default 'draft', not null),\n   subtotal (numeric(12,2), default 0, not null),\n   tax_rate (numeric(5,2), default 0, not null),\n   tax_amount (numeric(12,2), default 0, not null),\n   total (numeric(12,2), default 0, not null),\n   notes (text, nullable),\n   order_date (date, default current_date, not null),\n   fulfilled_date (date, nullable),\n   created_by (uuid, references profiles(id), not null),\n   created_at (timestamptz, default now()),\n   updated_at (timestamptz, default now()).\n   Add a unique constraint on (team_id, order_number).\n\n3. `order_items` table — id (uuid, default gen_random_uuid(), primary key),\n   order_id (uuid, references orders(id) on delete cascade, not null),\n   description (text, not null),\n   quantity (numeric(10,2), default 1, not null),\n   unit_price (numeric(12,2), not null),\n   amount (numeric(12,2), not null),\n   sort_order (integer, default 0, not null),\n   created_at (timestamptz, default now()).\n\n4. `invoices` table — id (uuid, default gen_random_uuid(), primary key),\n   team_id (uuid, references teams(id) on delete cascade, not null),\n   invoice_number (serial, not null),\n   order_id (uuid, references orders(id) on delete set null, nullable),\n   customer_id (uuid, references customers(id) on delete cascade, not null),\n   status (text, check status in ('draft', 'sent', 'paid', 'overdue',\n   'cancelled', 'refunded'), default 'draft', not null),\n   subtotal (numeric(12,2), default 0, not null),\n   tax_rate (numeric(5,2), default 0, not null),\n   tax_amount (numeric(12,2), default 0, not null),\n   total (numeric(12,2), default 0, not null),\n   amount_paid (numeric(12,2), default 0, not null),\n   issue_date (date, default current_date, not null),\n   due_date (date, not null),\n   paid_date (date, nullable),\n   notes (text, nullable),\n   created_by (uuid, references profiles(id), not null),\n   created_at (timestamptz, default now()),\n   updated_at (timestamptz, default now()).\n   Add a unique constraint on (team_id, invoice_number).\n\n5. `payments` table — id (uuid, default gen_random_uuid(), primary key),\n   invoice_id (uuid, references invoices(id) on delete cascade, not null),\n   amount (numeric(12,2), not null),\n   method (text, check method in ('cash', 'bank_transfer', 'credit_card',\n   'check', 'other'), default 'bank_transfer', not null),\n   reference (text, nullable),\n   payment_date (date, default current_date, not null),\n   notes (text, nullable),\n   created_by (uuid, references profiles(id), not null),\n   created_at (timestamptz, default now()).\n\nEnable RLS on all five tables. Create policies that use the existing\n`is_team_member()` function to scope access. For `customers`, `orders`,\nand `invoices`, the policy should check that the row's `team_id` matches\na team the user belongs to. For `order_items`, join through the `orders`\ntable. For `payments`, join through the `invoices` table.\n\nOnly create a SELECT policy for team members — follow the\n`announcements_team_member_read` pattern in\n`supabase\u002Fmigrations\u002F00001_initial_schema.sql`. Writes go through\nservice-role server routes, so no insert\u002Fupdate\u002Fdelete RLS policies are\nneeded. Permission checks live in server routes via `authUser(event, \"key\")`.\n\nAfter creating the tables, add these permissions to `shared\u002Fpermissions.ts`:\n\n```\n\"customers.view\": [\"owner\", \"admin\", \"member\"],\n\"customers.create\": [\"owner\", \"admin\", \"member\"],\n\"customers.update\": [\"owner\", \"admin\"],\n\"customers.delete\": [\"owner\", \"admin\"],\n\"orders.view\": [\"owner\", \"admin\", \"member\"],\n\"orders.create\": [\"owner\", \"admin\", \"member\"],\n\"orders.update\": [\"owner\", \"admin\"],\n\"orders.delete\": [\"owner\", \"admin\"],\n\"invoices.view\": [\"owner\", \"admin\", \"member\"],\n\"invoices.create\": [\"owner\", \"admin\"],\n\"invoices.update\": [\"owner\", \"admin\"],\n\"invoices.delete\": [\"owner\", \"admin\"],\n\"payments.view\": [\"owner\", \"admin\", \"member\"],\n\"payments.create\": [\"owner\", \"admin\"],\n```\n\nAlso wire the new tables into the baked-in AI chat and activity log per\nCLAUDE.md conventions:\n\n- Add `COMMENT ON COLUMN` on every non-obvious column — one short technical\n  sentence. Mention format or business rules the DB does not enforce.\n- `select enable_activity_log('\u003Ctable>');` for each mutation-bearing table.\n- Grant chat read access on each team-scoped table:\n  ```\n  grant select on \u003Ctable> to chat_reader;\n  create policy \"\u003Ctable>_select_chat\" on \u003Ctable>\n    for select to chat_reader using (team_id = current_chat_team());\n  ```\n  Skip tables without a `team_id` column (scope them through a parent).\n- Register each table in `tablePermissions` in `shared\u002Fpermissions.ts`\n  using the permission keys above.\n- Add filter entries in `app\u002Fcomponents\u002Factivity\u002FList.vue` (`tableItems`)\n  for each new table.\n\nRegenerate the TypeScript types via Supabase MCP and save them to\n`shared\u002Ftypes\u002Fdatabase.types.ts`.\n","txt","",[270,271,272,280,286,293,299,304,310,315,321,327,333,339,345,351,357,363,369,375,380,386,391,397,403,409,415,421,427,433,439,444,450,456,461,466,471,477,482,488,494,500,506,512,518,524,530,535,541,546,552,558,563,569,575,580,585,590,595,601,607,613,619,624,629,634,639,645,650,656,662,667,673,679,685,691,696,701,706,711,717,723,729,735,741,746,752,758,764,770,776,781,787,792,798,804,810,816,822,828,834,840,846,852,858,864,870,876,882,887,892,898,904,909,915,921,927,933,939,945,951,957,962,968,974,980,986,992,997,1003],"code",{"__ignoreMap":268},[273,274,277],"span",{"class":275,"line":276},"line",1,[273,278,279],{},"We are building a sales order and invoicing app on top of this template.\n",[273,281,283],{"class":275,"line":282},2,[273,284,285],{},"Create the database schema and add the permissions we need.\n",[273,287,289],{"class":275,"line":288},3,[273,290,292],{"emptyLinePlaceholder":291},true,"\n",[273,294,296],{"class":275,"line":295},4,[273,297,298],{},"Database (via Supabase MCP):\n",[273,300,302],{"class":275,"line":301},5,[273,303,292],{"emptyLinePlaceholder":291},[273,305,307],{"class":275,"line":306},6,[273,308,309],{},"Create five tables:\n",[273,311,313],{"class":275,"line":312},7,[273,314,292],{"emptyLinePlaceholder":291},[273,316,318],{"class":275,"line":317},8,[273,319,320],{},"1. `customers` table — id (uuid, default gen_random_uuid(), primary key),\n",[273,322,324],{"class":275,"line":323},9,[273,325,326],{},"   team_id (uuid, references teams(id) on delete cascade, not null),\n",[273,328,330],{"class":275,"line":329},10,[273,331,332],{},"   name (text, not null), email (text, nullable), phone (text, nullable),\n",[273,334,336],{"class":275,"line":335},11,[273,337,338],{},"   company (text, nullable), address (text, nullable),\n",[273,340,342],{"class":275,"line":341},12,[273,343,344],{},"   billing_address (text, nullable),\n",[273,346,348],{"class":275,"line":347},13,[273,349,350],{},"   tax_id (text, nullable),\n",[273,352,354],{"class":275,"line":353},14,[273,355,356],{},"   notes (text, nullable),\n",[273,358,360],{"class":275,"line":359},15,[273,361,362],{},"   created_by (uuid, references profiles(id), not null),\n",[273,364,366],{"class":275,"line":365},16,[273,367,368],{},"   created_at (timestamptz, default now()),\n",[273,370,372],{"class":275,"line":371},17,[273,373,374],{},"   updated_at (timestamptz, default now()).\n",[273,376,378],{"class":275,"line":377},18,[273,379,292],{"emptyLinePlaceholder":291},[273,381,383],{"class":275,"line":382},19,[273,384,385],{},"2. `orders` table — id (uuid, default gen_random_uuid(), primary key),\n",[273,387,389],{"class":275,"line":388},20,[273,390,326],{},[273,392,394],{"class":275,"line":393},21,[273,395,396],{},"   order_number (serial, not null),\n",[273,398,400],{"class":275,"line":399},22,[273,401,402],{},"   customer_id (uuid, references customers(id) on delete cascade, not null),\n",[273,404,406],{"class":275,"line":405},23,[273,407,408],{},"   status (text, check status in ('draft', 'confirmed', 'fulfilled',\n",[273,410,412],{"class":275,"line":411},24,[273,413,414],{},"   'cancelled'), default 'draft', not null),\n",[273,416,418],{"class":275,"line":417},25,[273,419,420],{},"   subtotal (numeric(12,2), default 0, not null),\n",[273,422,424],{"class":275,"line":423},26,[273,425,426],{},"   tax_rate (numeric(5,2), default 0, not null),\n",[273,428,430],{"class":275,"line":429},27,[273,431,432],{},"   tax_amount (numeric(12,2), default 0, not null),\n",[273,434,436],{"class":275,"line":435},28,[273,437,438],{},"   total (numeric(12,2), default 0, not null),\n",[273,440,442],{"class":275,"line":441},29,[273,443,356],{},[273,445,447],{"class":275,"line":446},30,[273,448,449],{},"   order_date (date, default current_date, not null),\n",[273,451,453],{"class":275,"line":452},31,[273,454,455],{},"   fulfilled_date (date, nullable),\n",[273,457,459],{"class":275,"line":458},32,[273,460,362],{},[273,462,464],{"class":275,"line":463},33,[273,465,368],{},[273,467,469],{"class":275,"line":468},34,[273,470,374],{},[273,472,474],{"class":275,"line":473},35,[273,475,476],{},"   Add a unique constraint on (team_id, order_number).\n",[273,478,480],{"class":275,"line":479},36,[273,481,292],{"emptyLinePlaceholder":291},[273,483,485],{"class":275,"line":484},37,[273,486,487],{},"3. `order_items` table — id (uuid, default gen_random_uuid(), primary key),\n",[273,489,491],{"class":275,"line":490},38,[273,492,493],{},"   order_id (uuid, references orders(id) on delete cascade, not null),\n",[273,495,497],{"class":275,"line":496},39,[273,498,499],{},"   description (text, not null),\n",[273,501,503],{"class":275,"line":502},40,[273,504,505],{},"   quantity (numeric(10,2), default 1, not null),\n",[273,507,509],{"class":275,"line":508},41,[273,510,511],{},"   unit_price (numeric(12,2), not null),\n",[273,513,515],{"class":275,"line":514},42,[273,516,517],{},"   amount (numeric(12,2), not null),\n",[273,519,521],{"class":275,"line":520},43,[273,522,523],{},"   sort_order (integer, default 0, not null),\n",[273,525,527],{"class":275,"line":526},44,[273,528,529],{},"   created_at (timestamptz, default now()).\n",[273,531,533],{"class":275,"line":532},45,[273,534,292],{"emptyLinePlaceholder":291},[273,536,538],{"class":275,"line":537},46,[273,539,540],{},"4. `invoices` table — id (uuid, default gen_random_uuid(), primary key),\n",[273,542,544],{"class":275,"line":543},47,[273,545,326],{},[273,547,549],{"class":275,"line":548},48,[273,550,551],{},"   invoice_number (serial, not null),\n",[273,553,555],{"class":275,"line":554},49,[273,556,557],{},"   order_id (uuid, references orders(id) on delete set null, nullable),\n",[273,559,561],{"class":275,"line":560},50,[273,562,402],{},[273,564,566],{"class":275,"line":565},51,[273,567,568],{},"   status (text, check status in ('draft', 'sent', 'paid', 'overdue',\n",[273,570,572],{"class":275,"line":571},52,[273,573,574],{},"   'cancelled', 'refunded'), default 'draft', not null),\n",[273,576,578],{"class":275,"line":577},53,[273,579,420],{},[273,581,583],{"class":275,"line":582},54,[273,584,426],{},[273,586,588],{"class":275,"line":587},55,[273,589,432],{},[273,591,593],{"class":275,"line":592},56,[273,594,438],{},[273,596,598],{"class":275,"line":597},57,[273,599,600],{},"   amount_paid (numeric(12,2), default 0, not null),\n",[273,602,604],{"class":275,"line":603},58,[273,605,606],{},"   issue_date (date, default current_date, not null),\n",[273,608,610],{"class":275,"line":609},59,[273,611,612],{},"   due_date (date, not null),\n",[273,614,616],{"class":275,"line":615},60,[273,617,618],{},"   paid_date (date, nullable),\n",[273,620,622],{"class":275,"line":621},61,[273,623,356],{},[273,625,627],{"class":275,"line":626},62,[273,628,362],{},[273,630,632],{"class":275,"line":631},63,[273,633,368],{},[273,635,637],{"class":275,"line":636},64,[273,638,374],{},[273,640,642],{"class":275,"line":641},65,[273,643,644],{},"   Add a unique constraint on (team_id, invoice_number).\n",[273,646,648],{"class":275,"line":647},66,[273,649,292],{"emptyLinePlaceholder":291},[273,651,653],{"class":275,"line":652},67,[273,654,655],{},"5. `payments` table — id (uuid, default gen_random_uuid(), primary key),\n",[273,657,659],{"class":275,"line":658},68,[273,660,661],{},"   invoice_id (uuid, references invoices(id) on delete cascade, not null),\n",[273,663,665],{"class":275,"line":664},69,[273,666,517],{},[273,668,670],{"class":275,"line":669},70,[273,671,672],{},"   method (text, check method in ('cash', 'bank_transfer', 'credit_card',\n",[273,674,676],{"class":275,"line":675},71,[273,677,678],{},"   'check', 'other'), default 'bank_transfer', not null),\n",[273,680,682],{"class":275,"line":681},72,[273,683,684],{},"   reference (text, nullable),\n",[273,686,688],{"class":275,"line":687},73,[273,689,690],{},"   payment_date (date, default current_date, not null),\n",[273,692,694],{"class":275,"line":693},74,[273,695,356],{},[273,697,699],{"class":275,"line":698},75,[273,700,362],{},[273,702,704],{"class":275,"line":703},76,[273,705,529],{},[273,707,709],{"class":275,"line":708},77,[273,710,292],{"emptyLinePlaceholder":291},[273,712,714],{"class":275,"line":713},78,[273,715,716],{},"Enable RLS on all five tables. Create policies that use the existing\n",[273,718,720],{"class":275,"line":719},79,[273,721,722],{},"`is_team_member()` function to scope access. For `customers`, `orders`,\n",[273,724,726],{"class":275,"line":725},80,[273,727,728],{},"and `invoices`, the policy should check that the row's `team_id` matches\n",[273,730,732],{"class":275,"line":731},81,[273,733,734],{},"a team the user belongs to. For `order_items`, join through the `orders`\n",[273,736,738],{"class":275,"line":737},82,[273,739,740],{},"table. For `payments`, join through the `invoices` table.\n",[273,742,744],{"class":275,"line":743},83,[273,745,292],{"emptyLinePlaceholder":291},[273,747,749],{"class":275,"line":748},84,[273,750,751],{},"Only create a SELECT policy for team members — follow the\n",[273,753,755],{"class":275,"line":754},85,[273,756,757],{},"`announcements_team_member_read` pattern in\n",[273,759,761],{"class":275,"line":760},86,[273,762,763],{},"`supabase\u002Fmigrations\u002F00001_initial_schema.sql`. Writes go through\n",[273,765,767],{"class":275,"line":766},87,[273,768,769],{},"service-role server routes, so no insert\u002Fupdate\u002Fdelete RLS policies are\n",[273,771,773],{"class":275,"line":772},88,[273,774,775],{},"needed. Permission checks live in server routes via `authUser(event, \"key\")`.\n",[273,777,779],{"class":275,"line":778},89,[273,780,292],{"emptyLinePlaceholder":291},[273,782,784],{"class":275,"line":783},90,[273,785,786],{},"After creating the tables, add these permissions to `shared\u002Fpermissions.ts`:\n",[273,788,790],{"class":275,"line":789},91,[273,791,292],{"emptyLinePlaceholder":291},[273,793,795],{"class":275,"line":794},92,[273,796,797],{},"```\n",[273,799,801],{"class":275,"line":800},93,[273,802,803],{},"\"customers.view\": [\"owner\", \"admin\", \"member\"],\n",[273,805,807],{"class":275,"line":806},94,[273,808,809],{},"\"customers.create\": [\"owner\", \"admin\", \"member\"],\n",[273,811,813],{"class":275,"line":812},95,[273,814,815],{},"\"customers.update\": [\"owner\", \"admin\"],\n",[273,817,819],{"class":275,"line":818},96,[273,820,821],{},"\"customers.delete\": [\"owner\", \"admin\"],\n",[273,823,825],{"class":275,"line":824},97,[273,826,827],{},"\"orders.view\": [\"owner\", \"admin\", \"member\"],\n",[273,829,831],{"class":275,"line":830},98,[273,832,833],{},"\"orders.create\": [\"owner\", \"admin\", \"member\"],\n",[273,835,837],{"class":275,"line":836},99,[273,838,839],{},"\"orders.update\": [\"owner\", \"admin\"],\n",[273,841,843],{"class":275,"line":842},100,[273,844,845],{},"\"orders.delete\": [\"owner\", \"admin\"],\n",[273,847,849],{"class":275,"line":848},101,[273,850,851],{},"\"invoices.view\": [\"owner\", \"admin\", \"member\"],\n",[273,853,855],{"class":275,"line":854},102,[273,856,857],{},"\"invoices.create\": [\"owner\", \"admin\"],\n",[273,859,861],{"class":275,"line":860},103,[273,862,863],{},"\"invoices.update\": [\"owner\", \"admin\"],\n",[273,865,867],{"class":275,"line":866},104,[273,868,869],{},"\"invoices.delete\": [\"owner\", \"admin\"],\n",[273,871,873],{"class":275,"line":872},105,[273,874,875],{},"\"payments.view\": [\"owner\", \"admin\", \"member\"],\n",[273,877,879],{"class":275,"line":878},106,[273,880,881],{},"\"payments.create\": [\"owner\", \"admin\"],\n",[273,883,885],{"class":275,"line":884},107,[273,886,797],{},[273,888,890],{"class":275,"line":889},108,[273,891,292],{"emptyLinePlaceholder":291},[273,893,895],{"class":275,"line":894},109,[273,896,897],{},"Also wire the new tables into the baked-in AI chat and activity log per\n",[273,899,901],{"class":275,"line":900},110,[273,902,903],{},"CLAUDE.md conventions:\n",[273,905,907],{"class":275,"line":906},111,[273,908,292],{"emptyLinePlaceholder":291},[273,910,912],{"class":275,"line":911},112,[273,913,914],{},"- Add `COMMENT ON COLUMN` on every non-obvious column — one short technical\n",[273,916,918],{"class":275,"line":917},113,[273,919,920],{},"  sentence. Mention format or business rules the DB does not enforce.\n",[273,922,924],{"class":275,"line":923},114,[273,925,926],{},"- `select enable_activity_log('\u003Ctable>');` for each mutation-bearing table.\n",[273,928,930],{"class":275,"line":929},115,[273,931,932],{},"- Grant chat read access on each team-scoped table:\n",[273,934,936],{"class":275,"line":935},116,[273,937,938],{},"  ```\n",[273,940,942],{"class":275,"line":941},117,[273,943,944],{},"  grant select on \u003Ctable> to chat_reader;\n",[273,946,948],{"class":275,"line":947},118,[273,949,950],{},"  create policy \"\u003Ctable>_select_chat\" on \u003Ctable>\n",[273,952,954],{"class":275,"line":953},119,[273,955,956],{},"    for select to chat_reader using (team_id = current_chat_team());\n",[273,958,960],{"class":275,"line":959},120,[273,961,938],{},[273,963,965],{"class":275,"line":964},121,[273,966,967],{},"  Skip tables without a `team_id` column (scope them through a parent).\n",[273,969,971],{"class":275,"line":970},122,[273,972,973],{},"- Register each table in `tablePermissions` in `shared\u002Fpermissions.ts`\n",[273,975,977],{"class":275,"line":976},123,[273,978,979],{},"  using the permission keys above.\n",[273,981,983],{"class":275,"line":982},124,[273,984,985],{},"- Add filter entries in `app\u002Fcomponents\u002Factivity\u002FList.vue` (`tableItems`)\n",[273,987,989],{"class":275,"line":988},125,[273,990,991],{},"  for each new table.\n",[273,993,995],{"class":275,"line":994},126,[273,996,292],{"emptyLinePlaceholder":291},[273,998,1000],{"class":275,"line":999},127,[273,1001,1002],{},"Regenerate the TypeScript types via Supabase MCP and save them to\n",[273,1004,1006],{"class":275,"line":1005},128,[273,1007,1008],{},"`shared\u002Ftypes\u002Fdatabase.types.ts`.\n",[1010,1011,1012],"tip",{},[198,1013,1014,1017,1018,1020],{},[201,1015,1016],{},"Adding the public API later?"," If you plan to add the ",[206,1019,59],{"href":60}," plugin later, its page will guide you through adding the required permissions. You don't need to add them now.",[258,1022,227],{"id":1023},"customers",[263,1025,1027],{"className":265,"code":1026,"language":267,"meta":268,"style":268},"Build the customer module — server routes and a full UI page.\n\nServer routes (all use `authUser` with the appropriate permission):\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fcustomers` — uses\n  `authUser(event, \"customers.view\")`. Returns all customers for the team,\n  ordered by name asc. Include a count of orders and total revenue\n  (sum of paid invoice totals) per customer by joining.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fcustomers` — uses\n  `authUser(event, \"customers.create\")`. Reads { name, email, phone,\n  company, address, billing_address, tax_id, notes } from the body.\n  Validates that name is required. Sets team_id from the auth context\n  and created_by from the authenticated user (user.sub).\n\n- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fcustomers\u002F[customerId]` — uses\n  `authUser(event, \"customers.update\")`. Updates whichever fields are\n  provided in the body. Validates that the customer belongs to the team.\n\n- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Fcustomers\u002F[customerId]` — uses\n  `authUser(event, \"customers.delete\")`. Validates that the customer\n  belongs to the team before deleting.\n\nUI:\n\nCreate `app\u002Fpages\u002Fapp\u002Fcustomers\u002Findex.vue` — a customer list page as a\ntop-level route (under `\u002Fapp`, sibling of other features), wrapped in a\n`UDashboardPanel`:\n\n- `UDashboardNavbar` in the header: title \"Customers\",\n  `UDashboardSidebarCollapse` on the left. On the right, a \"New Customer\"\n  button wrapped in `CanAccess permission=\"customers.create\"`.\n\n- The body shows a table of customers with columns: name, company, email,\n  phone, order count, and total revenue (formatted as currency). Use\n  `USkeleton` for loading state on initial load. Show an empty state with\n  an icon and message when there are no customers.\n\n- \"New Customer\" button opens a `UModal` with a form: name (required),\n  email, phone, company, address, billing address, tax ID, notes. Use Zod\n  for validation. Show loading on submit.\n\n- Clicking a customer row opens a `USlideover` for editing. Show all fields\n  in a form. Below the form, show an \"Orders\" section: a read-only list of\n  orders for this customer (order number, date, status, total). Include a\n  delete button wrapped in `CanAccess permission=\"customers.delete\"` with\n  a confirmation modal.\n\n- After creating, editing, or deleting a customer, refresh the list.\n\nSidebar navigation:\n\nAdd a \"Customers\" link to the top navigation group in\n`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Dashboard\" and\n\"Settings\". Use the icon `i-solar-users-group-two-rounded-bold-duotone`.\n",[270,1028,1029,1034,1038,1043,1047,1052,1057,1062,1067,1071,1076,1081,1086,1091,1096,1100,1105,1110,1115,1119,1124,1129,1134,1138,1143,1147,1152,1157,1162,1166,1171,1176,1181,1185,1190,1195,1200,1205,1209,1214,1219,1224,1228,1233,1238,1243,1248,1253,1257,1262,1266,1271,1275,1280,1285],{"__ignoreMap":268},[273,1030,1031],{"class":275,"line":276},[273,1032,1033],{},"Build the customer module — server routes and a full UI page.\n",[273,1035,1036],{"class":275,"line":282},[273,1037,292],{"emptyLinePlaceholder":291},[273,1039,1040],{"class":275,"line":288},[273,1041,1042],{},"Server routes (all use `authUser` with the appropriate permission):\n",[273,1044,1045],{"class":275,"line":295},[273,1046,292],{"emptyLinePlaceholder":291},[273,1048,1049],{"class":275,"line":301},[273,1050,1051],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fcustomers` — uses\n",[273,1053,1054],{"class":275,"line":306},[273,1055,1056],{},"  `authUser(event, \"customers.view\")`. Returns all customers for the team,\n",[273,1058,1059],{"class":275,"line":312},[273,1060,1061],{},"  ordered by name asc. Include a count of orders and total revenue\n",[273,1063,1064],{"class":275,"line":317},[273,1065,1066],{},"  (sum of paid invoice totals) per customer by joining.\n",[273,1068,1069],{"class":275,"line":323},[273,1070,292],{"emptyLinePlaceholder":291},[273,1072,1073],{"class":275,"line":329},[273,1074,1075],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fcustomers` — uses\n",[273,1077,1078],{"class":275,"line":335},[273,1079,1080],{},"  `authUser(event, \"customers.create\")`. Reads { name, email, phone,\n",[273,1082,1083],{"class":275,"line":341},[273,1084,1085],{},"  company, address, billing_address, tax_id, notes } from the body.\n",[273,1087,1088],{"class":275,"line":347},[273,1089,1090],{},"  Validates that name is required. Sets team_id from the auth context\n",[273,1092,1093],{"class":275,"line":353},[273,1094,1095],{},"  and created_by from the authenticated user (user.sub).\n",[273,1097,1098],{"class":275,"line":359},[273,1099,292],{"emptyLinePlaceholder":291},[273,1101,1102],{"class":275,"line":365},[273,1103,1104],{},"- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fcustomers\u002F[customerId]` — uses\n",[273,1106,1107],{"class":275,"line":371},[273,1108,1109],{},"  `authUser(event, \"customers.update\")`. Updates whichever fields are\n",[273,1111,1112],{"class":275,"line":377},[273,1113,1114],{},"  provided in the body. Validates that the customer belongs to the team.\n",[273,1116,1117],{"class":275,"line":382},[273,1118,292],{"emptyLinePlaceholder":291},[273,1120,1121],{"class":275,"line":388},[273,1122,1123],{},"- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Fcustomers\u002F[customerId]` — uses\n",[273,1125,1126],{"class":275,"line":393},[273,1127,1128],{},"  `authUser(event, \"customers.delete\")`. Validates that the customer\n",[273,1130,1131],{"class":275,"line":399},[273,1132,1133],{},"  belongs to the team before deleting.\n",[273,1135,1136],{"class":275,"line":405},[273,1137,292],{"emptyLinePlaceholder":291},[273,1139,1140],{"class":275,"line":411},[273,1141,1142],{},"UI:\n",[273,1144,1145],{"class":275,"line":417},[273,1146,292],{"emptyLinePlaceholder":291},[273,1148,1149],{"class":275,"line":423},[273,1150,1151],{},"Create `app\u002Fpages\u002Fapp\u002Fcustomers\u002Findex.vue` — a customer list page as a\n",[273,1153,1154],{"class":275,"line":429},[273,1155,1156],{},"top-level route (under `\u002Fapp`, sibling of other features), wrapped in a\n",[273,1158,1159],{"class":275,"line":435},[273,1160,1161],{},"`UDashboardPanel`:\n",[273,1163,1164],{"class":275,"line":441},[273,1165,292],{"emptyLinePlaceholder":291},[273,1167,1168],{"class":275,"line":446},[273,1169,1170],{},"- `UDashboardNavbar` in the header: title \"Customers\",\n",[273,1172,1173],{"class":275,"line":452},[273,1174,1175],{},"  `UDashboardSidebarCollapse` on the left. On the right, a \"New Customer\"\n",[273,1177,1178],{"class":275,"line":458},[273,1179,1180],{},"  button wrapped in `CanAccess permission=\"customers.create\"`.\n",[273,1182,1183],{"class":275,"line":463},[273,1184,292],{"emptyLinePlaceholder":291},[273,1186,1187],{"class":275,"line":468},[273,1188,1189],{},"- The body shows a table of customers with columns: name, company, email,\n",[273,1191,1192],{"class":275,"line":473},[273,1193,1194],{},"  phone, order count, and total revenue (formatted as currency). Use\n",[273,1196,1197],{"class":275,"line":479},[273,1198,1199],{},"  `USkeleton` for loading state on initial load. Show an empty state with\n",[273,1201,1202],{"class":275,"line":484},[273,1203,1204],{},"  an icon and message when there are no customers.\n",[273,1206,1207],{"class":275,"line":490},[273,1208,292],{"emptyLinePlaceholder":291},[273,1210,1211],{"class":275,"line":496},[273,1212,1213],{},"- \"New Customer\" button opens a `UModal` with a form: name (required),\n",[273,1215,1216],{"class":275,"line":502},[273,1217,1218],{},"  email, phone, company, address, billing address, tax ID, notes. Use Zod\n",[273,1220,1221],{"class":275,"line":508},[273,1222,1223],{},"  for validation. Show loading on submit.\n",[273,1225,1226],{"class":275,"line":514},[273,1227,292],{"emptyLinePlaceholder":291},[273,1229,1230],{"class":275,"line":520},[273,1231,1232],{},"- Clicking a customer row opens a `USlideover` for editing. Show all fields\n",[273,1234,1235],{"class":275,"line":526},[273,1236,1237],{},"  in a form. Below the form, show an \"Orders\" section: a read-only list of\n",[273,1239,1240],{"class":275,"line":532},[273,1241,1242],{},"  orders for this customer (order number, date, status, total). Include a\n",[273,1244,1245],{"class":275,"line":537},[273,1246,1247],{},"  delete button wrapped in `CanAccess permission=\"customers.delete\"` with\n",[273,1249,1250],{"class":275,"line":543},[273,1251,1252],{},"  a confirmation modal.\n",[273,1254,1255],{"class":275,"line":548},[273,1256,292],{"emptyLinePlaceholder":291},[273,1258,1259],{"class":275,"line":554},[273,1260,1261],{},"- After creating, editing, or deleting a customer, refresh the list.\n",[273,1263,1264],{"class":275,"line":560},[273,1265,292],{"emptyLinePlaceholder":291},[273,1267,1268],{"class":275,"line":565},[273,1269,1270],{},"Sidebar navigation:\n",[273,1272,1273],{"class":275,"line":571},[273,1274,292],{"emptyLinePlaceholder":291},[273,1276,1277],{"class":275,"line":577},[273,1278,1279],{},"Add a \"Customers\" link to the top navigation group in\n",[273,1281,1282],{"class":275,"line":582},[273,1283,1284],{},"`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Dashboard\" and\n",[273,1286,1287],{"class":275,"line":587},[273,1288,1289],{},"\"Settings\". Use the icon `i-solar-users-group-two-rounded-bold-duotone`.\n",[258,1291,233],{"id":1292},"orders",[263,1294,1296],{"className":265,"code":1295,"language":267,"meta":268,"style":268},"Build the order management module — this is the core sales feature.\n\nServer routes (all use `authUser` with the appropriate permission):\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Forders` — uses `authUser(event, \"orders.view\")`.\n  Returns all orders for the team with customer name and item count joined\n  in. Order by order_date desc. Support optional query params: `?status=`\n  to filter by status, `?customer_id=` to filter by customer.\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Forders\u002F[orderId]` — uses\n  `authUser(event, \"orders.view\")`. Returns the order with customer info,\n  all line items, and linked invoice info (if any). Validates that the order\n  belongs to the team.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Forders` — uses\n  `authUser(event, \"orders.create\")`. Reads { customer_id, status, tax_rate,\n  notes, order_date, items } from the body where items is an array of\n  { description, quantity, unit_price, sort_order }. Validates that\n  customer_id is required and items must have at least one entry. For each\n  item, calculate amount = quantity * unit_price. Calculate subtotal as sum\n  of all item amounts, tax_amount = subtotal * (tax_rate \u002F 100), and\n  total = subtotal + tax_amount. Sets team_id from auth context and\n  created_by from the authenticated user. Insert order first, then items.\n\n- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Forders\u002F[orderId]` — uses\n  `authUser(event, \"orders.update\")`. Updates order fields and\u002For replaces\n  line items. If items are provided, delete existing items and insert the\n  new ones. Recalculate subtotal, tax_amount, and total. If status is\n  changed to \"fulfilled\", set fulfilled_date to today. Validates that the\n  order belongs to the team.\n\n- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Forders\u002F[orderId]` — uses\n  `authUser(event, \"orders.delete\")`. Validates that the order belongs to\n  the team. Return 400 if the order has a linked invoice.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Forders\u002F[orderId]\u002Finvoice` — uses\n  `authUser(event, \"invoices.create\")`. Generates an invoice from the\n  order. Reads { due_date, notes } from the body. Copies customer_id,\n  subtotal, tax_rate, tax_amount, and total from the order. Sets issue_date\n  to today. Returns 400 if an invoice already exists for this order.\n\nUI:\n\nCreate `app\u002Fpages\u002Fapp\u002Forders\u002Findex.vue` — the main orders page as a top-level\nroute, wrapped in a `UDashboardPanel`:\n\n- `UDashboardNavbar` in the header: title \"Orders\",\n  `UDashboardSidebarCollapse` on the left. On the right, a \"New Order\"\n  button wrapped in `CanAccess permission=\"orders.create\"`.\n\n- `UDashboardToolbar` below the navbar: on the left, show status counts\n  (e.g. \"2 Draft, 5 Confirmed, 3 Fulfilled\"). On the right, status\n  filter using `USelect`: All, Draft, Confirmed, Fulfilled, Cancelled.\n\n- Order list as a table with columns: order number (formatted as #001),\n  customer name, order date, item count, status badge (color-coded —\n  draft = \"neutral\", confirmed = \"info\", fulfilled = \"success\",\n  cancelled = \"error\"), total (formatted as currency).\n\n- Each row has an inline status dropdown using `UDropdownMenu`.\n\n- \"New Order\" button opens a `UModal` (or full-width `USlideover` for\n  more space) with a form: customer (required — select from customers\n  fetched on mount), order date (use `UPopover` with `UCalendar`),\n  tax rate (numeric input, default 0), notes. Below, a \"Line Items\"\n  section: a dynamic list where each row has description (required),\n  quantity (default 1), unit price (required), and a calculated amount.\n  \"Add Item\" button to add rows, \"Remove\" button on each row. Show the\n  running subtotal, tax amount, and total at the bottom. Use Zod for\n  validation. Show loading on submit.\n\n- Clicking an order row opens a `USlideover` for the order detail. Show\n  order metadata at the top (customer, status, dates). Below, show the\n  line items table (description, qty, unit price, amount) with\n  subtotal\u002Ftax\u002Ftotal summary. Show an \"Invoice\" section: if an invoice\n  exists, show its number, status, and a link; if not, show a \"Generate\n  Invoice\" button (wrapped in `CanAccess permission=\"invoices.create\"`)\n  that prompts for a due date and creates the invoice. Include a delete\n  button wrapped in `CanAccess permission=\"orders.delete\"` with confirmation.\n\n- After creating, editing, or deleting an order, refresh the order list.\n\nSidebar navigation:\n\nAdd an \"Orders\" link to the top navigation group in\n`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Customers\" and\n\"Settings\". Use the icon `i-solar-cart-large-2-bold-duotone`.\n",[270,1297,1298,1303,1307,1311,1315,1320,1325,1330,1335,1339,1344,1349,1354,1359,1363,1368,1373,1378,1383,1388,1393,1398,1403,1408,1412,1417,1422,1427,1432,1437,1442,1446,1451,1456,1461,1465,1470,1475,1480,1485,1490,1494,1498,1502,1507,1512,1516,1521,1526,1531,1535,1540,1545,1550,1554,1559,1564,1569,1574,1578,1583,1587,1592,1597,1602,1607,1612,1617,1622,1627,1632,1636,1641,1646,1651,1656,1661,1666,1671,1676,1680,1685,1689,1693,1697,1702,1707],{"__ignoreMap":268},[273,1299,1300],{"class":275,"line":276},[273,1301,1302],{},"Build the order management module — this is the core sales feature.\n",[273,1304,1305],{"class":275,"line":282},[273,1306,292],{"emptyLinePlaceholder":291},[273,1308,1309],{"class":275,"line":288},[273,1310,1042],{},[273,1312,1313],{"class":275,"line":295},[273,1314,292],{"emptyLinePlaceholder":291},[273,1316,1317],{"class":275,"line":301},[273,1318,1319],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Forders` — uses `authUser(event, \"orders.view\")`.\n",[273,1321,1322],{"class":275,"line":306},[273,1323,1324],{},"  Returns all orders for the team with customer name and item count joined\n",[273,1326,1327],{"class":275,"line":312},[273,1328,1329],{},"  in. Order by order_date desc. Support optional query params: `?status=`\n",[273,1331,1332],{"class":275,"line":317},[273,1333,1334],{},"  to filter by status, `?customer_id=` to filter by customer.\n",[273,1336,1337],{"class":275,"line":323},[273,1338,292],{"emptyLinePlaceholder":291},[273,1340,1341],{"class":275,"line":329},[273,1342,1343],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Forders\u002F[orderId]` — uses\n",[273,1345,1346],{"class":275,"line":335},[273,1347,1348],{},"  `authUser(event, \"orders.view\")`. Returns the order with customer info,\n",[273,1350,1351],{"class":275,"line":341},[273,1352,1353],{},"  all line items, and linked invoice info (if any). Validates that the order\n",[273,1355,1356],{"class":275,"line":347},[273,1357,1358],{},"  belongs to the team.\n",[273,1360,1361],{"class":275,"line":353},[273,1362,292],{"emptyLinePlaceholder":291},[273,1364,1365],{"class":275,"line":359},[273,1366,1367],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Forders` — uses\n",[273,1369,1370],{"class":275,"line":365},[273,1371,1372],{},"  `authUser(event, \"orders.create\")`. Reads { customer_id, status, tax_rate,\n",[273,1374,1375],{"class":275,"line":371},[273,1376,1377],{},"  notes, order_date, items } from the body where items is an array of\n",[273,1379,1380],{"class":275,"line":377},[273,1381,1382],{},"  { description, quantity, unit_price, sort_order }. Validates that\n",[273,1384,1385],{"class":275,"line":382},[273,1386,1387],{},"  customer_id is required and items must have at least one entry. For each\n",[273,1389,1390],{"class":275,"line":388},[273,1391,1392],{},"  item, calculate amount = quantity * unit_price. Calculate subtotal as sum\n",[273,1394,1395],{"class":275,"line":393},[273,1396,1397],{},"  of all item amounts, tax_amount = subtotal * (tax_rate \u002F 100), and\n",[273,1399,1400],{"class":275,"line":399},[273,1401,1402],{},"  total = subtotal + tax_amount. Sets team_id from auth context and\n",[273,1404,1405],{"class":275,"line":405},[273,1406,1407],{},"  created_by from the authenticated user. Insert order first, then items.\n",[273,1409,1410],{"class":275,"line":411},[273,1411,292],{"emptyLinePlaceholder":291},[273,1413,1414],{"class":275,"line":417},[273,1415,1416],{},"- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Forders\u002F[orderId]` — uses\n",[273,1418,1419],{"class":275,"line":423},[273,1420,1421],{},"  `authUser(event, \"orders.update\")`. Updates order fields and\u002For replaces\n",[273,1423,1424],{"class":275,"line":429},[273,1425,1426],{},"  line items. If items are provided, delete existing items and insert the\n",[273,1428,1429],{"class":275,"line":435},[273,1430,1431],{},"  new ones. Recalculate subtotal, tax_amount, and total. If status is\n",[273,1433,1434],{"class":275,"line":441},[273,1435,1436],{},"  changed to \"fulfilled\", set fulfilled_date to today. Validates that the\n",[273,1438,1439],{"class":275,"line":446},[273,1440,1441],{},"  order belongs to the team.\n",[273,1443,1444],{"class":275,"line":452},[273,1445,292],{"emptyLinePlaceholder":291},[273,1447,1448],{"class":275,"line":458},[273,1449,1450],{},"- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Forders\u002F[orderId]` — uses\n",[273,1452,1453],{"class":275,"line":463},[273,1454,1455],{},"  `authUser(event, \"orders.delete\")`. Validates that the order belongs to\n",[273,1457,1458],{"class":275,"line":468},[273,1459,1460],{},"  the team. Return 400 if the order has a linked invoice.\n",[273,1462,1463],{"class":275,"line":473},[273,1464,292],{"emptyLinePlaceholder":291},[273,1466,1467],{"class":275,"line":479},[273,1468,1469],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Forders\u002F[orderId]\u002Finvoice` — uses\n",[273,1471,1472],{"class":275,"line":484},[273,1473,1474],{},"  `authUser(event, \"invoices.create\")`. Generates an invoice from the\n",[273,1476,1477],{"class":275,"line":490},[273,1478,1479],{},"  order. Reads { due_date, notes } from the body. Copies customer_id,\n",[273,1481,1482],{"class":275,"line":496},[273,1483,1484],{},"  subtotal, tax_rate, tax_amount, and total from the order. Sets issue_date\n",[273,1486,1487],{"class":275,"line":502},[273,1488,1489],{},"  to today. Returns 400 if an invoice already exists for this order.\n",[273,1491,1492],{"class":275,"line":508},[273,1493,292],{"emptyLinePlaceholder":291},[273,1495,1496],{"class":275,"line":514},[273,1497,1142],{},[273,1499,1500],{"class":275,"line":520},[273,1501,292],{"emptyLinePlaceholder":291},[273,1503,1504],{"class":275,"line":526},[273,1505,1506],{},"Create `app\u002Fpages\u002Fapp\u002Forders\u002Findex.vue` — the main orders page as a top-level\n",[273,1508,1509],{"class":275,"line":532},[273,1510,1511],{},"route, wrapped in a `UDashboardPanel`:\n",[273,1513,1514],{"class":275,"line":537},[273,1515,292],{"emptyLinePlaceholder":291},[273,1517,1518],{"class":275,"line":543},[273,1519,1520],{},"- `UDashboardNavbar` in the header: title \"Orders\",\n",[273,1522,1523],{"class":275,"line":548},[273,1524,1525],{},"  `UDashboardSidebarCollapse` on the left. On the right, a \"New Order\"\n",[273,1527,1528],{"class":275,"line":554},[273,1529,1530],{},"  button wrapped in `CanAccess permission=\"orders.create\"`.\n",[273,1532,1533],{"class":275,"line":560},[273,1534,292],{"emptyLinePlaceholder":291},[273,1536,1537],{"class":275,"line":565},[273,1538,1539],{},"- `UDashboardToolbar` below the navbar: on the left, show status counts\n",[273,1541,1542],{"class":275,"line":571},[273,1543,1544],{},"  (e.g. \"2 Draft, 5 Confirmed, 3 Fulfilled\"). On the right, status\n",[273,1546,1547],{"class":275,"line":577},[273,1548,1549],{},"  filter using `USelect`: All, Draft, Confirmed, Fulfilled, Cancelled.\n",[273,1551,1552],{"class":275,"line":582},[273,1553,292],{"emptyLinePlaceholder":291},[273,1555,1556],{"class":275,"line":587},[273,1557,1558],{},"- Order list as a table with columns: order number (formatted as #001),\n",[273,1560,1561],{"class":275,"line":592},[273,1562,1563],{},"  customer name, order date, item count, status badge (color-coded —\n",[273,1565,1566],{"class":275,"line":597},[273,1567,1568],{},"  draft = \"neutral\", confirmed = \"info\", fulfilled = \"success\",\n",[273,1570,1571],{"class":275,"line":603},[273,1572,1573],{},"  cancelled = \"error\"), total (formatted as currency).\n",[273,1575,1576],{"class":275,"line":609},[273,1577,292],{"emptyLinePlaceholder":291},[273,1579,1580],{"class":275,"line":615},[273,1581,1582],{},"- Each row has an inline status dropdown using `UDropdownMenu`.\n",[273,1584,1585],{"class":275,"line":621},[273,1586,292],{"emptyLinePlaceholder":291},[273,1588,1589],{"class":275,"line":626},[273,1590,1591],{},"- \"New Order\" button opens a `UModal` (or full-width `USlideover` for\n",[273,1593,1594],{"class":275,"line":631},[273,1595,1596],{},"  more space) with a form: customer (required — select from customers\n",[273,1598,1599],{"class":275,"line":636},[273,1600,1601],{},"  fetched on mount), order date (use `UPopover` with `UCalendar`),\n",[273,1603,1604],{"class":275,"line":641},[273,1605,1606],{},"  tax rate (numeric input, default 0), notes. Below, a \"Line Items\"\n",[273,1608,1609],{"class":275,"line":647},[273,1610,1611],{},"  section: a dynamic list where each row has description (required),\n",[273,1613,1614],{"class":275,"line":652},[273,1615,1616],{},"  quantity (default 1), unit price (required), and a calculated amount.\n",[273,1618,1619],{"class":275,"line":658},[273,1620,1621],{},"  \"Add Item\" button to add rows, \"Remove\" button on each row. Show the\n",[273,1623,1624],{"class":275,"line":664},[273,1625,1626],{},"  running subtotal, tax amount, and total at the bottom. Use Zod for\n",[273,1628,1629],{"class":275,"line":669},[273,1630,1631],{},"  validation. Show loading on submit.\n",[273,1633,1634],{"class":275,"line":675},[273,1635,292],{"emptyLinePlaceholder":291},[273,1637,1638],{"class":275,"line":681},[273,1639,1640],{},"- Clicking an order row opens a `USlideover` for the order detail. Show\n",[273,1642,1643],{"class":275,"line":687},[273,1644,1645],{},"  order metadata at the top (customer, status, dates). Below, show the\n",[273,1647,1648],{"class":275,"line":693},[273,1649,1650],{},"  line items table (description, qty, unit price, amount) with\n",[273,1652,1653],{"class":275,"line":698},[273,1654,1655],{},"  subtotal\u002Ftax\u002Ftotal summary. Show an \"Invoice\" section: if an invoice\n",[273,1657,1658],{"class":275,"line":703},[273,1659,1660],{},"  exists, show its number, status, and a link; if not, show a \"Generate\n",[273,1662,1663],{"class":275,"line":708},[273,1664,1665],{},"  Invoice\" button (wrapped in `CanAccess permission=\"invoices.create\"`)\n",[273,1667,1668],{"class":275,"line":713},[273,1669,1670],{},"  that prompts for a due date and creates the invoice. Include a delete\n",[273,1672,1673],{"class":275,"line":719},[273,1674,1675],{},"  button wrapped in `CanAccess permission=\"orders.delete\"` with confirmation.\n",[273,1677,1678],{"class":275,"line":725},[273,1679,292],{"emptyLinePlaceholder":291},[273,1681,1682],{"class":275,"line":731},[273,1683,1684],{},"- After creating, editing, or deleting an order, refresh the order list.\n",[273,1686,1687],{"class":275,"line":737},[273,1688,292],{"emptyLinePlaceholder":291},[273,1690,1691],{"class":275,"line":743},[273,1692,1270],{},[273,1694,1695],{"class":275,"line":748},[273,1696,292],{"emptyLinePlaceholder":291},[273,1698,1699],{"class":275,"line":754},[273,1700,1701],{},"Add an \"Orders\" link to the top navigation group in\n",[273,1703,1704],{"class":275,"line":760},[273,1705,1706],{},"`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Customers\" and\n",[273,1708,1709],{"class":275,"line":766},[273,1710,1711],{},"\"Settings\". Use the icon `i-solar-cart-large-2-bold-duotone`.\n",[258,1713,1715],{"id":1714},"invoices-payments","Invoices & Payments",[263,1717,1719],{"className":265,"code":1718,"language":267,"meta":268,"style":268},"Build the invoice and payment modules — billing and payment tracking.\n\nServer routes (all use `authUser` with the appropriate permission):\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices` — uses\n  `authUser(event, \"invoices.view\")`. Returns all invoices for the team\n  with customer name and payment count joined in. Order by issue_date desc.\n  Support optional query params: `?status=` to filter by status,\n  `?customer_id=` to filter by customer.\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices\u002F[invoiceId]` — uses\n  `authUser(event, \"invoices.view\")`. Returns the invoice with customer\n  info, linked order info (if any), and all payments. Validates team.\n\n- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices\u002F[invoiceId]` — uses\n  `authUser(event, \"invoices.update\")`. Updates whichever fields are\n  provided. If status is changed to \"paid\", set paid_date to today.\n  Validates that the invoice belongs to the team.\n\n- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices\u002F[invoiceId]` — uses\n  `authUser(event, \"invoices.delete\")`. Validates team. Return 400 if\n  the invoice has any payments recorded.\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices\u002F[invoiceId]\u002Fpayments` — uses\n  `authUser(event, \"payments.view\")`. Returns all payments for the invoice\n  with the recorder's name joined in, ordered by payment_date desc.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices\u002F[invoiceId]\u002Fpayments` — uses\n  `authUser(event, \"payments.create\")`. Reads { amount, method, reference,\n  payment_date, notes } from the body. Validates that amount is required\n  and positive. After inserting, update the invoice's amount_paid (sum all\n  payments). If amount_paid >= total, automatically set status to \"paid\"\n  and paid_date to today. Sets created_by from the authenticated user.\n\nUI:\n\nCreate `app\u002Fpages\u002Fapp\u002Finvoices\u002Findex.vue` — the invoices page as a top-level\nroute, wrapped in a `UDashboardPanel`:\n\n- `UDashboardNavbar` in the header: title \"Invoices\",\n  `UDashboardSidebarCollapse` on the left. On the right, a \"New Invoice\"\n  button wrapped in `CanAccess permission=\"invoices.create\"` (for manual\n  invoices not linked to an order).\n\n- `UDashboardToolbar` below the navbar: on the left, show summary\n  (e.g. \"3 Outstanding, $12,450 Due\"). On the right, status filter using\n  `USelect`: All, Draft, Sent, Paid, Overdue, Cancelled, Refunded.\n\n- Invoice list as a table with columns: invoice number (formatted as\n  INV-001), customer name, issue date, due date, status badge (color-coded —\n  draft = \"neutral\", sent = \"info\", paid = \"success\", overdue = \"error\",\n  cancelled = \"neutral\", refunded = \"warning\"), total (formatted as\n  currency), amount paid, balance due (total - amount_paid).\n\n- Overdue detection: if status is \"sent\" and due_date \u003C today, show the\n  status badge as \"Overdue\" in red instead of \"Sent\".\n\n- Each row has an inline status dropdown using `UDropdownMenu`.\n\n- \"New Invoice\" button opens a `UModal` with a form: customer (required —\n  select from customers), issue date, due date (required), tax rate, notes.\n  Below, a \"Line Items\" section identical to the order form (description,\n  qty, unit price, amount). Calculate subtotal\u002Ftax\u002Ftotal. Use Zod. Show\n  loading on submit.\n\n- Clicking an invoice row opens a `USlideover` with full invoice detail.\n  Show invoice metadata (customer, dates, status). Show line items from\n  the linked order (if any) or the invoice itself. Show a \"Payments\"\n  section: a list of recorded payments (date, amount, method, reference)\n  with a \"Record Payment\" button (wrapped in\n  `CanAccess permission=\"payments.create\"`) that opens a small inline form\n  (amount, method select, reference, date, notes). Show balance remaining.\n  Include a delete button wrapped in\n  `CanAccess permission=\"invoices.delete\"` with confirmation.\n\n- After any change, refresh the invoice list.\n\nSidebar navigation:\n\nAdd an \"Invoices\" link to the top navigation group in\n`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Orders\" and\n\"Settings\". Use the icon `i-solar-bill-list-bold-duotone`.\n",[270,1720,1721,1726,1730,1734,1738,1743,1748,1753,1758,1763,1767,1772,1777,1782,1786,1791,1796,1801,1806,1810,1815,1820,1825,1829,1834,1839,1844,1848,1853,1858,1863,1868,1873,1878,1882,1886,1890,1895,1899,1903,1908,1913,1918,1923,1927,1932,1937,1942,1946,1951,1956,1961,1966,1971,1975,1980,1985,1989,1993,1997,2002,2007,2012,2017,2022,2026,2031,2036,2041,2046,2051,2056,2061,2066,2071,2075,2080,2084,2088,2092,2097,2102],{"__ignoreMap":268},[273,1722,1723],{"class":275,"line":276},[273,1724,1725],{},"Build the invoice and payment modules — billing and payment tracking.\n",[273,1727,1728],{"class":275,"line":282},[273,1729,292],{"emptyLinePlaceholder":291},[273,1731,1732],{"class":275,"line":288},[273,1733,1042],{},[273,1735,1736],{"class":275,"line":295},[273,1737,292],{"emptyLinePlaceholder":291},[273,1739,1740],{"class":275,"line":301},[273,1741,1742],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices` — uses\n",[273,1744,1745],{"class":275,"line":306},[273,1746,1747],{},"  `authUser(event, \"invoices.view\")`. Returns all invoices for the team\n",[273,1749,1750],{"class":275,"line":312},[273,1751,1752],{},"  with customer name and payment count joined in. Order by issue_date desc.\n",[273,1754,1755],{"class":275,"line":317},[273,1756,1757],{},"  Support optional query params: `?status=` to filter by status,\n",[273,1759,1760],{"class":275,"line":323},[273,1761,1762],{},"  `?customer_id=` to filter by customer.\n",[273,1764,1765],{"class":275,"line":329},[273,1766,292],{"emptyLinePlaceholder":291},[273,1768,1769],{"class":275,"line":335},[273,1770,1771],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices\u002F[invoiceId]` — uses\n",[273,1773,1774],{"class":275,"line":341},[273,1775,1776],{},"  `authUser(event, \"invoices.view\")`. Returns the invoice with customer\n",[273,1778,1779],{"class":275,"line":347},[273,1780,1781],{},"  info, linked order info (if any), and all payments. Validates team.\n",[273,1783,1784],{"class":275,"line":353},[273,1785,292],{"emptyLinePlaceholder":291},[273,1787,1788],{"class":275,"line":359},[273,1789,1790],{},"- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices\u002F[invoiceId]` — uses\n",[273,1792,1793],{"class":275,"line":365},[273,1794,1795],{},"  `authUser(event, \"invoices.update\")`. Updates whichever fields are\n",[273,1797,1798],{"class":275,"line":371},[273,1799,1800],{},"  provided. If status is changed to \"paid\", set paid_date to today.\n",[273,1802,1803],{"class":275,"line":377},[273,1804,1805],{},"  Validates that the invoice belongs to the team.\n",[273,1807,1808],{"class":275,"line":382},[273,1809,292],{"emptyLinePlaceholder":291},[273,1811,1812],{"class":275,"line":388},[273,1813,1814],{},"- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices\u002F[invoiceId]` — uses\n",[273,1816,1817],{"class":275,"line":393},[273,1818,1819],{},"  `authUser(event, \"invoices.delete\")`. Validates team. Return 400 if\n",[273,1821,1822],{"class":275,"line":399},[273,1823,1824],{},"  the invoice has any payments recorded.\n",[273,1826,1827],{"class":275,"line":405},[273,1828,292],{"emptyLinePlaceholder":291},[273,1830,1831],{"class":275,"line":411},[273,1832,1833],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices\u002F[invoiceId]\u002Fpayments` — uses\n",[273,1835,1836],{"class":275,"line":417},[273,1837,1838],{},"  `authUser(event, \"payments.view\")`. Returns all payments for the invoice\n",[273,1840,1841],{"class":275,"line":423},[273,1842,1843],{},"  with the recorder's name joined in, ordered by payment_date desc.\n",[273,1845,1846],{"class":275,"line":429},[273,1847,292],{"emptyLinePlaceholder":291},[273,1849,1850],{"class":275,"line":435},[273,1851,1852],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Finvoices\u002F[invoiceId]\u002Fpayments` — uses\n",[273,1854,1855],{"class":275,"line":441},[273,1856,1857],{},"  `authUser(event, \"payments.create\")`. Reads { amount, method, reference,\n",[273,1859,1860],{"class":275,"line":446},[273,1861,1862],{},"  payment_date, notes } from the body. Validates that amount is required\n",[273,1864,1865],{"class":275,"line":452},[273,1866,1867],{},"  and positive. After inserting, update the invoice's amount_paid (sum all\n",[273,1869,1870],{"class":275,"line":458},[273,1871,1872],{},"  payments). If amount_paid >= total, automatically set status to \"paid\"\n",[273,1874,1875],{"class":275,"line":463},[273,1876,1877],{},"  and paid_date to today. Sets created_by from the authenticated user.\n",[273,1879,1880],{"class":275,"line":468},[273,1881,292],{"emptyLinePlaceholder":291},[273,1883,1884],{"class":275,"line":473},[273,1885,1142],{},[273,1887,1888],{"class":275,"line":479},[273,1889,292],{"emptyLinePlaceholder":291},[273,1891,1892],{"class":275,"line":484},[273,1893,1894],{},"Create `app\u002Fpages\u002Fapp\u002Finvoices\u002Findex.vue` — the invoices page as a top-level\n",[273,1896,1897],{"class":275,"line":490},[273,1898,1511],{},[273,1900,1901],{"class":275,"line":496},[273,1902,292],{"emptyLinePlaceholder":291},[273,1904,1905],{"class":275,"line":502},[273,1906,1907],{},"- `UDashboardNavbar` in the header: title \"Invoices\",\n",[273,1909,1910],{"class":275,"line":508},[273,1911,1912],{},"  `UDashboardSidebarCollapse` on the left. On the right, a \"New Invoice\"\n",[273,1914,1915],{"class":275,"line":514},[273,1916,1917],{},"  button wrapped in `CanAccess permission=\"invoices.create\"` (for manual\n",[273,1919,1920],{"class":275,"line":520},[273,1921,1922],{},"  invoices not linked to an order).\n",[273,1924,1925],{"class":275,"line":526},[273,1926,292],{"emptyLinePlaceholder":291},[273,1928,1929],{"class":275,"line":532},[273,1930,1931],{},"- `UDashboardToolbar` below the navbar: on the left, show summary\n",[273,1933,1934],{"class":275,"line":537},[273,1935,1936],{},"  (e.g. \"3 Outstanding, $12,450 Due\"). On the right, status filter using\n",[273,1938,1939],{"class":275,"line":543},[273,1940,1941],{},"  `USelect`: All, Draft, Sent, Paid, Overdue, Cancelled, Refunded.\n",[273,1943,1944],{"class":275,"line":548},[273,1945,292],{"emptyLinePlaceholder":291},[273,1947,1948],{"class":275,"line":554},[273,1949,1950],{},"- Invoice list as a table with columns: invoice number (formatted as\n",[273,1952,1953],{"class":275,"line":560},[273,1954,1955],{},"  INV-001), customer name, issue date, due date, status badge (color-coded —\n",[273,1957,1958],{"class":275,"line":565},[273,1959,1960],{},"  draft = \"neutral\", sent = \"info\", paid = \"success\", overdue = \"error\",\n",[273,1962,1963],{"class":275,"line":571},[273,1964,1965],{},"  cancelled = \"neutral\", refunded = \"warning\"), total (formatted as\n",[273,1967,1968],{"class":275,"line":577},[273,1969,1970],{},"  currency), amount paid, balance due (total - amount_paid).\n",[273,1972,1973],{"class":275,"line":582},[273,1974,292],{"emptyLinePlaceholder":291},[273,1976,1977],{"class":275,"line":587},[273,1978,1979],{},"- Overdue detection: if status is \"sent\" and due_date \u003C today, show the\n",[273,1981,1982],{"class":275,"line":592},[273,1983,1984],{},"  status badge as \"Overdue\" in red instead of \"Sent\".\n",[273,1986,1987],{"class":275,"line":597},[273,1988,292],{"emptyLinePlaceholder":291},[273,1990,1991],{"class":275,"line":603},[273,1992,1582],{},[273,1994,1995],{"class":275,"line":609},[273,1996,292],{"emptyLinePlaceholder":291},[273,1998,1999],{"class":275,"line":615},[273,2000,2001],{},"- \"New Invoice\" button opens a `UModal` with a form: customer (required —\n",[273,2003,2004],{"class":275,"line":621},[273,2005,2006],{},"  select from customers), issue date, due date (required), tax rate, notes.\n",[273,2008,2009],{"class":275,"line":626},[273,2010,2011],{},"  Below, a \"Line Items\" section identical to the order form (description,\n",[273,2013,2014],{"class":275,"line":631},[273,2015,2016],{},"  qty, unit price, amount). Calculate subtotal\u002Ftax\u002Ftotal. Use Zod. Show\n",[273,2018,2019],{"class":275,"line":636},[273,2020,2021],{},"  loading on submit.\n",[273,2023,2024],{"class":275,"line":641},[273,2025,292],{"emptyLinePlaceholder":291},[273,2027,2028],{"class":275,"line":647},[273,2029,2030],{},"- Clicking an invoice row opens a `USlideover` with full invoice detail.\n",[273,2032,2033],{"class":275,"line":652},[273,2034,2035],{},"  Show invoice metadata (customer, dates, status). Show line items from\n",[273,2037,2038],{"class":275,"line":658},[273,2039,2040],{},"  the linked order (if any) or the invoice itself. Show a \"Payments\"\n",[273,2042,2043],{"class":275,"line":664},[273,2044,2045],{},"  section: a list of recorded payments (date, amount, method, reference)\n",[273,2047,2048],{"class":275,"line":669},[273,2049,2050],{},"  with a \"Record Payment\" button (wrapped in\n",[273,2052,2053],{"class":275,"line":675},[273,2054,2055],{},"  `CanAccess permission=\"payments.create\"`) that opens a small inline form\n",[273,2057,2058],{"class":275,"line":681},[273,2059,2060],{},"  (amount, method select, reference, date, notes). Show balance remaining.\n",[273,2062,2063],{"class":275,"line":687},[273,2064,2065],{},"  Include a delete button wrapped in\n",[273,2067,2068],{"class":275,"line":693},[273,2069,2070],{},"  `CanAccess permission=\"invoices.delete\"` with confirmation.\n",[273,2072,2073],{"class":275,"line":698},[273,2074,292],{"emptyLinePlaceholder":291},[273,2076,2077],{"class":275,"line":703},[273,2078,2079],{},"- After any change, refresh the invoice list.\n",[273,2081,2082],{"class":275,"line":708},[273,2083,292],{"emptyLinePlaceholder":291},[273,2085,2086],{"class":275,"line":713},[273,2087,1270],{},[273,2089,2090],{"class":275,"line":719},[273,2091,292],{"emptyLinePlaceholder":291},[273,2093,2094],{"class":275,"line":725},[273,2095,2096],{},"Add an \"Invoices\" link to the top navigation group in\n",[273,2098,2099],{"class":275,"line":731},[273,2100,2101],{},"`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Orders\" and\n",[273,2103,2104],{"class":275,"line":737},[273,2105,2106],{},"\"Settings\". Use the icon `i-solar-bill-list-bold-duotone`.\n",[258,2108,2110],{"id":2109},"dashboard-and-realtime","Dashboard and Realtime",[2112,2113,2115],"h4",{"id":2114},"dashboard","Dashboard",[263,2117,2119],{"className":265,"code":2118,"language":267,"meta":268,"style":268},"Replace the placeholder dashboard with real sales and billing stats.\n\nServer route:\n\nCreate `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fstats` — uses `authUser(event, \"team.view\")`.\nReturns a JSON object with:\n\n- `customer_count` — total number of customers for the team\n- `open_orders` — count of orders with status \"draft\" or \"confirmed\"\n- `revenue_this_month` — sum of totals for invoices with status \"paid\" where\n  paid_date is in the current calendar month. Return 0 if none.\n- `outstanding_amount` — sum of (total - amount_paid) for invoices with\n  status \"sent\" or \"overdue\". Return 0 if none.\n- `overdue_invoices` — count of invoices where status is \"sent\" and\n  due_date \u003C today\n- `recent_orders` — the last 5 orders with customer name, status, total,\n  and order_date\n- `recent_payments` — the last 5 payments with invoice number, customer\n  name, amount, method, and payment_date\n- `revenue_by_month` — array of { month, total } for the last 6 months\n  of paid invoices, ordered chronologically\n\nAll queries are scoped to the team's team_id.\n\nUI:\n\nUpdate `app\u002Fpages\u002Fapp\u002Findex.vue` to show:\n\n- A row of four stat cards at the top using a grid layout. Each card shows\n  an icon, a label, and the value. Cards: \"Total Customers\" (customer_count),\n  \"Open Orders\" (open_orders), \"Revenue This Month\" (revenue_this_month\n  formatted as currency), \"Outstanding\" (outstanding_amount formatted as\n  currency, with overdue_invoices count shown as a warning sub-label if > 0).\n  Use `USkeleton` placeholders while loading.\n\n- Below the stats, a two-column layout:\n  - Left column: \"Recent Orders\" — a list of the last 5 orders. Each item\n    shows order number, customer name, status badge, total, and date.\n    Clicking an order navigates to the orders page. Show an empty state if\n    no orders.\n  - Right column: \"Recent Payments\" — a list of the last 5 payments. Each\n    item shows invoice number, customer name, amount (formatted as\n    currency), payment method badge, and date. Show an empty state if none.\n\nRemove any placeholder\u002Fscaffolding content that was in the dashboard before.\nKeep the `UDashboardPanel` wrapper with navbar and sidebar collapse.\n",[270,2120,2121,2126,2130,2135,2139,2144,2149,2153,2158,2163,2168,2173,2178,2183,2188,2193,2198,2203,2208,2213,2218,2223,2227,2232,2236,2240,2244,2249,2253,2258,2263,2268,2273,2278,2283,2287,2292,2297,2302,2307,2312,2317,2322,2327,2331,2336],{"__ignoreMap":268},[273,2122,2123],{"class":275,"line":276},[273,2124,2125],{},"Replace the placeholder dashboard with real sales and billing stats.\n",[273,2127,2128],{"class":275,"line":282},[273,2129,292],{"emptyLinePlaceholder":291},[273,2131,2132],{"class":275,"line":288},[273,2133,2134],{},"Server route:\n",[273,2136,2137],{"class":275,"line":295},[273,2138,292],{"emptyLinePlaceholder":291},[273,2140,2141],{"class":275,"line":301},[273,2142,2143],{},"Create `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fstats` — uses `authUser(event, \"team.view\")`.\n",[273,2145,2146],{"class":275,"line":306},[273,2147,2148],{},"Returns a JSON object with:\n",[273,2150,2151],{"class":275,"line":312},[273,2152,292],{"emptyLinePlaceholder":291},[273,2154,2155],{"class":275,"line":317},[273,2156,2157],{},"- `customer_count` — total number of customers for the team\n",[273,2159,2160],{"class":275,"line":323},[273,2161,2162],{},"- `open_orders` — count of orders with status \"draft\" or \"confirmed\"\n",[273,2164,2165],{"class":275,"line":329},[273,2166,2167],{},"- `revenue_this_month` — sum of totals for invoices with status \"paid\" where\n",[273,2169,2170],{"class":275,"line":335},[273,2171,2172],{},"  paid_date is in the current calendar month. Return 0 if none.\n",[273,2174,2175],{"class":275,"line":341},[273,2176,2177],{},"- `outstanding_amount` — sum of (total - amount_paid) for invoices with\n",[273,2179,2180],{"class":275,"line":347},[273,2181,2182],{},"  status \"sent\" or \"overdue\". Return 0 if none.\n",[273,2184,2185],{"class":275,"line":353},[273,2186,2187],{},"- `overdue_invoices` — count of invoices where status is \"sent\" and\n",[273,2189,2190],{"class":275,"line":359},[273,2191,2192],{},"  due_date \u003C today\n",[273,2194,2195],{"class":275,"line":365},[273,2196,2197],{},"- `recent_orders` — the last 5 orders with customer name, status, total,\n",[273,2199,2200],{"class":275,"line":371},[273,2201,2202],{},"  and order_date\n",[273,2204,2205],{"class":275,"line":377},[273,2206,2207],{},"- `recent_payments` — the last 5 payments with invoice number, customer\n",[273,2209,2210],{"class":275,"line":382},[273,2211,2212],{},"  name, amount, method, and payment_date\n",[273,2214,2215],{"class":275,"line":388},[273,2216,2217],{},"- `revenue_by_month` — array of { month, total } for the last 6 months\n",[273,2219,2220],{"class":275,"line":393},[273,2221,2222],{},"  of paid invoices, ordered chronologically\n",[273,2224,2225],{"class":275,"line":399},[273,2226,292],{"emptyLinePlaceholder":291},[273,2228,2229],{"class":275,"line":405},[273,2230,2231],{},"All queries are scoped to the team's team_id.\n",[273,2233,2234],{"class":275,"line":411},[273,2235,292],{"emptyLinePlaceholder":291},[273,2237,2238],{"class":275,"line":417},[273,2239,1142],{},[273,2241,2242],{"class":275,"line":423},[273,2243,292],{"emptyLinePlaceholder":291},[273,2245,2246],{"class":275,"line":429},[273,2247,2248],{},"Update `app\u002Fpages\u002Fapp\u002Findex.vue` to show:\n",[273,2250,2251],{"class":275,"line":435},[273,2252,292],{"emptyLinePlaceholder":291},[273,2254,2255],{"class":275,"line":441},[273,2256,2257],{},"- A row of four stat cards at the top using a grid layout. Each card shows\n",[273,2259,2260],{"class":275,"line":446},[273,2261,2262],{},"  an icon, a label, and the value. Cards: \"Total Customers\" (customer_count),\n",[273,2264,2265],{"class":275,"line":452},[273,2266,2267],{},"  \"Open Orders\" (open_orders), \"Revenue This Month\" (revenue_this_month\n",[273,2269,2270],{"class":275,"line":458},[273,2271,2272],{},"  formatted as currency), \"Outstanding\" (outstanding_amount formatted as\n",[273,2274,2275],{"class":275,"line":463},[273,2276,2277],{},"  currency, with overdue_invoices count shown as a warning sub-label if > 0).\n",[273,2279,2280],{"class":275,"line":468},[273,2281,2282],{},"  Use `USkeleton` placeholders while loading.\n",[273,2284,2285],{"class":275,"line":473},[273,2286,292],{"emptyLinePlaceholder":291},[273,2288,2289],{"class":275,"line":479},[273,2290,2291],{},"- Below the stats, a two-column layout:\n",[273,2293,2294],{"class":275,"line":484},[273,2295,2296],{},"  - Left column: \"Recent Orders\" — a list of the last 5 orders. Each item\n",[273,2298,2299],{"class":275,"line":490},[273,2300,2301],{},"    shows order number, customer name, status badge, total, and date.\n",[273,2303,2304],{"class":275,"line":496},[273,2305,2306],{},"    Clicking an order navigates to the orders page. Show an empty state if\n",[273,2308,2309],{"class":275,"line":502},[273,2310,2311],{},"    no orders.\n",[273,2313,2314],{"class":275,"line":508},[273,2315,2316],{},"  - Right column: \"Recent Payments\" — a list of the last 5 payments. Each\n",[273,2318,2319],{"class":275,"line":514},[273,2320,2321],{},"    item shows invoice number, customer name, amount (formatted as\n",[273,2323,2324],{"class":275,"line":520},[273,2325,2326],{},"    currency), payment method badge, and date. Show an empty state if none.\n",[273,2328,2329],{"class":275,"line":526},[273,2330,292],{"emptyLinePlaceholder":291},[273,2332,2333],{"class":275,"line":532},[273,2334,2335],{},"Remove any placeholder\u002Fscaffolding content that was in the dashboard before.\n",[273,2337,2338],{"class":275,"line":537},[273,2339,2340],{},"Keep the `UDashboardPanel` wrapper with navbar and sidebar collapse.\n",[2112,2342,251],{"id":2343},"realtime",[263,2345,2347],{"className":265,"code":2346,"language":267,"meta":268,"style":268},"Add Supabase Realtime sync for the new tables so changes appear instantly\nacross browser sessions.\n\nDatabase migration (via Supabase MCP):\n\nEnable realtime publication and full replica identity for the new tables:\n\n```sql\nALTER PUBLICATION supabase_realtime ADD TABLE customers, orders, order_items, invoices, payments;\nALTER TABLE customers REPLICA IDENTITY FULL;\nALTER TABLE orders REPLICA IDENTITY FULL;\nALTER TABLE order_items REPLICA IDENTITY FULL;\nALTER TABLE invoices REPLICA IDENTITY FULL;\nALTER TABLE payments REPLICA IDENTITY FULL;\n```\n\nUpdate `app\u002Fcomposables\u002FuseRealtime.ts`:\n\n1. Add `\"customers\"`, `\"orders\"`, `\"order_items\"`, `\"invoices\"`, and\n   `\"payments\"` to the `RealtimeTable` union type.\n2. In the `setup()` function, add `.on(\"postgres_changes\", ...)` handlers\n   for each new table, filtered by `team_id=eq.${teamId}` where the table\n   has a team_id column. For `order_items`, subscribe unfiltered since it\n   lacks a direct team_id — RLS already scopes the events.\n\nIntegration — use `onTableDebounced` from `useRealtime()` inline in each\npage. Do NOT create separate `useRealtimeX` composable files:\n\n- In `app\u002Fpages\u002Fapp\u002Forders\u002Findex.vue`:\n  `const { onTableDebounced } = useRealtime()`\n  `onTableDebounced([\"orders\", \"order_items\"], () => refreshOrders())`\n- In `app\u002Fpages\u002Fapp\u002Finvoices\u002Findex.vue`:\n  `const { onTableDebounced } = useRealtime()`\n  `onTableDebounced([\"invoices\", \"payments\"], () => refreshInvoices())`\n- In the dashboard stats page:\n  `const { onTableDebounced } = useRealtime()`\n  `onTableDebounced([\"orders\", \"invoices\", \"payments\"], () => refreshStats())`\n",[270,2348,2349,2354,2359,2363,2368,2372,2377,2381,2386,2391,2396,2401,2406,2411,2416,2420,2424,2429,2433,2438,2443,2448,2453,2458,2463,2467,2472,2477,2481,2486,2491,2496,2501,2505,2510,2515,2519],{"__ignoreMap":268},[273,2350,2351],{"class":275,"line":276},[273,2352,2353],{},"Add Supabase Realtime sync for the new tables so changes appear instantly\n",[273,2355,2356],{"class":275,"line":282},[273,2357,2358],{},"across browser sessions.\n",[273,2360,2361],{"class":275,"line":288},[273,2362,292],{"emptyLinePlaceholder":291},[273,2364,2365],{"class":275,"line":295},[273,2366,2367],{},"Database migration (via Supabase MCP):\n",[273,2369,2370],{"class":275,"line":301},[273,2371,292],{"emptyLinePlaceholder":291},[273,2373,2374],{"class":275,"line":306},[273,2375,2376],{},"Enable realtime publication and full replica identity for the new tables:\n",[273,2378,2379],{"class":275,"line":312},[273,2380,292],{"emptyLinePlaceholder":291},[273,2382,2383],{"class":275,"line":317},[273,2384,2385],{},"```sql\n",[273,2387,2388],{"class":275,"line":323},[273,2389,2390],{},"ALTER PUBLICATION supabase_realtime ADD TABLE customers, orders, order_items, invoices, payments;\n",[273,2392,2393],{"class":275,"line":329},[273,2394,2395],{},"ALTER TABLE customers REPLICA IDENTITY FULL;\n",[273,2397,2398],{"class":275,"line":335},[273,2399,2400],{},"ALTER TABLE orders REPLICA IDENTITY FULL;\n",[273,2402,2403],{"class":275,"line":341},[273,2404,2405],{},"ALTER TABLE order_items REPLICA IDENTITY FULL;\n",[273,2407,2408],{"class":275,"line":347},[273,2409,2410],{},"ALTER TABLE invoices REPLICA IDENTITY FULL;\n",[273,2412,2413],{"class":275,"line":353},[273,2414,2415],{},"ALTER TABLE payments REPLICA IDENTITY FULL;\n",[273,2417,2418],{"class":275,"line":359},[273,2419,797],{},[273,2421,2422],{"class":275,"line":365},[273,2423,292],{"emptyLinePlaceholder":291},[273,2425,2426],{"class":275,"line":371},[273,2427,2428],{},"Update `app\u002Fcomposables\u002FuseRealtime.ts`:\n",[273,2430,2431],{"class":275,"line":377},[273,2432,292],{"emptyLinePlaceholder":291},[273,2434,2435],{"class":275,"line":382},[273,2436,2437],{},"1. Add `\"customers\"`, `\"orders\"`, `\"order_items\"`, `\"invoices\"`, and\n",[273,2439,2440],{"class":275,"line":388},[273,2441,2442],{},"   `\"payments\"` to the `RealtimeTable` union type.\n",[273,2444,2445],{"class":275,"line":393},[273,2446,2447],{},"2. In the `setup()` function, add `.on(\"postgres_changes\", ...)` handlers\n",[273,2449,2450],{"class":275,"line":399},[273,2451,2452],{},"   for each new table, filtered by `team_id=eq.${teamId}` where the table\n",[273,2454,2455],{"class":275,"line":405},[273,2456,2457],{},"   has a team_id column. For `order_items`, subscribe unfiltered since it\n",[273,2459,2460],{"class":275,"line":411},[273,2461,2462],{},"   lacks a direct team_id — RLS already scopes the events.\n",[273,2464,2465],{"class":275,"line":417},[273,2466,292],{"emptyLinePlaceholder":291},[273,2468,2469],{"class":275,"line":423},[273,2470,2471],{},"Integration — use `onTableDebounced` from `useRealtime()` inline in each\n",[273,2473,2474],{"class":275,"line":429},[273,2475,2476],{},"page. Do NOT create separate `useRealtimeX` composable files:\n",[273,2478,2479],{"class":275,"line":435},[273,2480,292],{"emptyLinePlaceholder":291},[273,2482,2483],{"class":275,"line":441},[273,2484,2485],{},"- In `app\u002Fpages\u002Fapp\u002Forders\u002Findex.vue`:\n",[273,2487,2488],{"class":275,"line":446},[273,2489,2490],{},"  `const { onTableDebounced } = useRealtime()`\n",[273,2492,2493],{"class":275,"line":452},[273,2494,2495],{},"  `onTableDebounced([\"orders\", \"order_items\"], () => refreshOrders())`\n",[273,2497,2498],{"class":275,"line":458},[273,2499,2500],{},"- In `app\u002Fpages\u002Fapp\u002Finvoices\u002Findex.vue`:\n",[273,2502,2503],{"class":275,"line":463},[273,2504,2490],{},[273,2506,2507],{"class":275,"line":468},[273,2508,2509],{},"  `onTableDebounced([\"invoices\", \"payments\"], () => refreshInvoices())`\n",[273,2511,2512],{"class":275,"line":473},[273,2513,2514],{},"- In the dashboard stats page:\n",[273,2516,2517],{"class":275,"line":479},[273,2518,2490],{},[273,2520,2521],{"class":275,"line":484},[273,2522,2523],{},"  `onTableDebounced([\"orders\", \"invoices\", \"payments\"], () => refreshStats())`\n",[258,2525,2527],{"id":2526},"what-you-built","What You Built",[198,2529,2530],{},"Starting from a template that handled auth, teams, roles, and permissions, you added:",[2532,2533,2534,2539,2544,2549,2555,2560],"ol",{},[223,2535,2536,2538],{},[201,2537,227],{}," — a customer database with contact info, billing details, and revenue tracking",[223,2540,2541,2543],{},[201,2542,233],{}," — sales orders with dynamic line items, tax calculation, and status workflow",[223,2545,2546,2548],{},[201,2547,239],{}," — invoice generation from orders with payment tracking and overdue detection",[223,2550,2551,2554],{},[201,2552,2553],{},"Payments"," — payment recording with automatic invoice status updates",[223,2556,2557,2559],{},[201,2558,2115],{}," — revenue metrics, outstanding amounts, and recent activity",[223,2561,2562,252],{},[201,2563,251],{},[198,2565,2566,2567,2570],{},"Every feature follows the same patterns: permission-gated server routes, team-scoped data, Nuxt UI components, and the conventions defined in ",[270,2568,2569],{},"CLAUDE.md",".",[258,2572,2574],{"id":2573},"whats-next","What's Next",[220,2576,2577,2584],{},[223,2578,2579,2583],{},[201,2580,2581],{},[206,2582,59],{"href":60}," — let accounting software or payment gateways push payment records",[223,2585,2586,2590,2591,2594],{},[201,2587,2588],{},[206,2589,143],{"href":144}," — the baked-in assistant is already wired to your new tables via ",[270,2592,2593],{},"tablePermissions",". Try it with \"What's our outstanding balance?\" or \"Which invoices are overdue?\"",[2596,2597,2598],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":268,"searchDepth":276,"depth":282,"links":2600},[2601,2602],{"id":2526,"depth":282,"text":2527},{"id":2573,"depth":282,"text":2574},"Build a sales system with orders, line items, invoice generation, and payment tracking","md",null,{},{"icon":104},{"title":101,"description":2603},"P2kCtIabocf46EBIcABwsfk3fn_m8K1kLqAeAz8xIOs",[2611,2613],{"title":96,"path":97,"stem":98,"description":2612,"icon":99,"children":-1},"Build a lightweight CRM with contacts, deals, pipeline stages, and activity tracking",{"title":106,"path":107,"stem":108,"description":2614,"icon":109,"children":-1},"Build a scheduling system with events, availability, recurring schedules, and booking management",1777092169686]