[{"data":1,"prerenderedAt":2226},["ShallowReactive",2],{"navigation":3,"\u002Fexamples\u002Finventory-management":189,"\u002Fexamples\u002Finventory-management-surround":2221},[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":91,"body":191,"description":2214,"extension":2215,"links":2216,"meta":2217,"navigation":2218,"path":92,"seo":2219,"stem":93,"__hash__":2220},"docs\u002F5.examples\u002F4.inventory-management.md",{"type":192,"value":193,"toc":2210},"minimark",[194,210,213,216,219,253,2141,2145,2148,2175,2182,2186,2206],[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],{},"An inventory management system is what any business with physical goods needs to keep track of stock levels across locations. Think retail, e-commerce, manufacturing, food service, or distribution. Any business that stores products somewhere and needs to know how much they have, where it is, and when it moves.",[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],{},"Product catalog"," — full CRUD for products with SKUs, categories, and pricing",[223,230,231,234],{},[201,232,233],{},"Warehouses"," — locations where stock is stored",[223,236,237,240],{},[201,238,239],{},"Stock movements"," — inbound, outbound, and transfer records with full audit trail",[223,242,243,246],{},[201,244,245],{},"Inventory dashboard"," — real stats showing stock levels, low-stock alerts, and recent movements",[223,248,249,252],{},[201,250,251],{},"Realtime"," — all changes sync instantly across connected browsers",[254,255,257,262,802,814,818,1118,1121,1374,1378,1749,1753,1758,1968,1971],"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 an inventory management app on top of this template. Create\nthe database schema and add the permissions we need.\n\nDatabase (via Supabase MCP):\n\nCreate four tables:\n\n1. `products` 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), sku (text, not null),\n   category (text, nullable), description (text, nullable),\n   unit_price (numeric(10,2), nullable),\n   unit (text, default 'each', not null),\n   reorder_point (integer, default 0, not null),\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, sku).\n\n2. `warehouses` 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), address (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\n3. `stock_levels` table — id (uuid, default gen_random_uuid(), primary key),\n   team_id (uuid, references teams(id) on delete cascade, not null),\n   product_id (uuid, references products(id) on delete cascade, not null),\n   warehouse_id (uuid, references warehouses(id) on delete cascade, not null),\n   quantity (integer, default 0, not null),\n   updated_at (timestamptz, default now()).\n   Add a unique constraint on (product_id, warehouse_id).\n\n4. `stock_movements` table — id (uuid, default gen_random_uuid(), primary key),\n   team_id (uuid, references teams(id) on delete cascade, not null),\n   product_id (uuid, references products(id) on delete cascade, not null),\n   warehouse_id (uuid, references warehouses(id) on delete cascade, not null),\n   type (text, check type in ('inbound', 'outbound', 'transfer', 'adjustment'),\n   not null),\n   quantity (integer, not null),\n   reference (text, nullable),\n   notes (text, nullable),\n   created_by (uuid, references profiles(id), not null),\n   created_at (timestamptz, default now()).\n\nEnable RLS on all four tables. Create policies that use the existing\n`is_team_member()` function to scope access. For all tables, the policy\nshould check that the row's `team_id` matches a team the user belongs to.\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\"products.view\": [\"owner\", \"admin\", \"member\"],\n\"products.create\": [\"owner\", \"admin\", \"member\"],\n\"products.update\": [\"owner\", \"admin\"],\n\"products.delete\": [\"owner\", \"admin\"],\n\"warehouses.view\": [\"owner\", \"admin\", \"member\"],\n\"warehouses.create\": [\"owner\", \"admin\"],\n\"warehouses.update\": [\"owner\", \"admin\"],\n\"warehouses.delete\": [\"owner\", \"admin\"],\n\"inventory.view\": [\"owner\", \"admin\", \"member\"],\n\"inventory.manage\": [\"owner\", \"admin\", \"member\"],\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,381,386,392,397,403,409,414,419,424,429,435,440,446,452,458,463,469,474,480,485,490,495,501,507,513,519,524,529,535,540,546,552,558,563,569,575,581,587,593,598,604,609,615,621,627,633,639,645,651,657,663,669,675,680,685,691,697,702,708,714,720,726,732,738,744,750,755,761,767,773,779,785,790,796],"code",{"__ignoreMap":268},[273,274,277],"span",{"class":275,"line":276},"line",1,[273,278,279],{},"We are building an inventory management app on top of this template. Create\n",[273,281,283],{"class":275,"line":282},2,[273,284,285],{},"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 four 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. `products` 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), sku (text, not null),\n",[273,334,336],{"class":275,"line":335},11,[273,337,338],{},"   category (text, nullable), description (text, nullable),\n",[273,340,342],{"class":275,"line":341},12,[273,343,344],{},"   unit_price (numeric(10,2), nullable),\n",[273,346,348],{"class":275,"line":347},13,[273,349,350],{},"   unit (text, default 'each', not null),\n",[273,352,354],{"class":275,"line":353},14,[273,355,356],{},"   reorder_point (integer, default 0, not null),\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,380],{},"   Add a unique constraint on (team_id, sku).\n",[273,382,384],{"class":275,"line":383},19,[273,385,292],{"emptyLinePlaceholder":291},[273,387,389],{"class":275,"line":388},20,[273,390,391],{},"2. `warehouses` table — id (uuid, default gen_random_uuid(), primary key),\n",[273,393,395],{"class":275,"line":394},21,[273,396,326],{},[273,398,400],{"class":275,"line":399},22,[273,401,402],{},"   name (text, not null), address (text, nullable),\n",[273,404,406],{"class":275,"line":405},23,[273,407,408],{},"   notes (text, nullable),\n",[273,410,412],{"class":275,"line":411},24,[273,413,362],{},[273,415,417],{"class":275,"line":416},25,[273,418,368],{},[273,420,422],{"class":275,"line":421},26,[273,423,374],{},[273,425,427],{"class":275,"line":426},27,[273,428,292],{"emptyLinePlaceholder":291},[273,430,432],{"class":275,"line":431},28,[273,433,434],{},"3. `stock_levels` table — id (uuid, default gen_random_uuid(), primary key),\n",[273,436,438],{"class":275,"line":437},29,[273,439,326],{},[273,441,443],{"class":275,"line":442},30,[273,444,445],{},"   product_id (uuid, references products(id) on delete cascade, not null),\n",[273,447,449],{"class":275,"line":448},31,[273,450,451],{},"   warehouse_id (uuid, references warehouses(id) on delete cascade, not null),\n",[273,453,455],{"class":275,"line":454},32,[273,456,457],{},"   quantity (integer, default 0, not null),\n",[273,459,461],{"class":275,"line":460},33,[273,462,374],{},[273,464,466],{"class":275,"line":465},34,[273,467,468],{},"   Add a unique constraint on (product_id, warehouse_id).\n",[273,470,472],{"class":275,"line":471},35,[273,473,292],{"emptyLinePlaceholder":291},[273,475,477],{"class":275,"line":476},36,[273,478,479],{},"4. `stock_movements` table — id (uuid, default gen_random_uuid(), primary key),\n",[273,481,483],{"class":275,"line":482},37,[273,484,326],{},[273,486,488],{"class":275,"line":487},38,[273,489,445],{},[273,491,493],{"class":275,"line":492},39,[273,494,451],{},[273,496,498],{"class":275,"line":497},40,[273,499,500],{},"   type (text, check type in ('inbound', 'outbound', 'transfer', 'adjustment'),\n",[273,502,504],{"class":275,"line":503},41,[273,505,506],{},"   not null),\n",[273,508,510],{"class":275,"line":509},42,[273,511,512],{},"   quantity (integer, not null),\n",[273,514,516],{"class":275,"line":515},43,[273,517,518],{},"   reference (text, nullable),\n",[273,520,522],{"class":275,"line":521},44,[273,523,408],{},[273,525,527],{"class":275,"line":526},45,[273,528,362],{},[273,530,532],{"class":275,"line":531},46,[273,533,534],{},"   created_at (timestamptz, default now()).\n",[273,536,538],{"class":275,"line":537},47,[273,539,292],{"emptyLinePlaceholder":291},[273,541,543],{"class":275,"line":542},48,[273,544,545],{},"Enable RLS on all four tables. Create policies that use the existing\n",[273,547,549],{"class":275,"line":548},49,[273,550,551],{},"`is_team_member()` function to scope access. For all tables, the policy\n",[273,553,555],{"class":275,"line":554},50,[273,556,557],{},"should check that the row's `team_id` matches a team the user belongs to.\n",[273,559,561],{"class":275,"line":560},51,[273,562,292],{"emptyLinePlaceholder":291},[273,564,566],{"class":275,"line":565},52,[273,567,568],{},"Only create a SELECT policy for team members — follow the\n",[273,570,572],{"class":275,"line":571},53,[273,573,574],{},"`announcements_team_member_read` pattern in\n",[273,576,578],{"class":275,"line":577},54,[273,579,580],{},"`supabase\u002Fmigrations\u002F00001_initial_schema.sql`. Writes go through\n",[273,582,584],{"class":275,"line":583},55,[273,585,586],{},"service-role server routes, so no insert\u002Fupdate\u002Fdelete RLS policies are\n",[273,588,590],{"class":275,"line":589},56,[273,591,592],{},"needed. Permission checks live in server routes via `authUser(event, \"key\")`.\n",[273,594,596],{"class":275,"line":595},57,[273,597,292],{"emptyLinePlaceholder":291},[273,599,601],{"class":275,"line":600},58,[273,602,603],{},"After creating the tables, add these permissions to `shared\u002Fpermissions.ts`:\n",[273,605,607],{"class":275,"line":606},59,[273,608,292],{"emptyLinePlaceholder":291},[273,610,612],{"class":275,"line":611},60,[273,613,614],{},"```\n",[273,616,618],{"class":275,"line":617},61,[273,619,620],{},"\"products.view\": [\"owner\", \"admin\", \"member\"],\n",[273,622,624],{"class":275,"line":623},62,[273,625,626],{},"\"products.create\": [\"owner\", \"admin\", \"member\"],\n",[273,628,630],{"class":275,"line":629},63,[273,631,632],{},"\"products.update\": [\"owner\", \"admin\"],\n",[273,634,636],{"class":275,"line":635},64,[273,637,638],{},"\"products.delete\": [\"owner\", \"admin\"],\n",[273,640,642],{"class":275,"line":641},65,[273,643,644],{},"\"warehouses.view\": [\"owner\", \"admin\", \"member\"],\n",[273,646,648],{"class":275,"line":647},66,[273,649,650],{},"\"warehouses.create\": [\"owner\", \"admin\"],\n",[273,652,654],{"class":275,"line":653},67,[273,655,656],{},"\"warehouses.update\": [\"owner\", \"admin\"],\n",[273,658,660],{"class":275,"line":659},68,[273,661,662],{},"\"warehouses.delete\": [\"owner\", \"admin\"],\n",[273,664,666],{"class":275,"line":665},69,[273,667,668],{},"\"inventory.view\": [\"owner\", \"admin\", \"member\"],\n",[273,670,672],{"class":275,"line":671},70,[273,673,674],{},"\"inventory.manage\": [\"owner\", \"admin\", \"member\"],\n",[273,676,678],{"class":275,"line":677},71,[273,679,614],{},[273,681,683],{"class":275,"line":682},72,[273,684,292],{"emptyLinePlaceholder":291},[273,686,688],{"class":275,"line":687},73,[273,689,690],{},"Also wire the new tables into the baked-in AI chat and activity log per\n",[273,692,694],{"class":275,"line":693},74,[273,695,696],{},"CLAUDE.md conventions:\n",[273,698,700],{"class":275,"line":699},75,[273,701,292],{"emptyLinePlaceholder":291},[273,703,705],{"class":275,"line":704},76,[273,706,707],{},"- Add `COMMENT ON COLUMN` on every non-obvious column — one short technical\n",[273,709,711],{"class":275,"line":710},77,[273,712,713],{},"  sentence. Mention format or business rules the DB does not enforce.\n",[273,715,717],{"class":275,"line":716},78,[273,718,719],{},"- `select enable_activity_log('\u003Ctable>');` for each mutation-bearing table.\n",[273,721,723],{"class":275,"line":722},79,[273,724,725],{},"- Grant chat read access on each team-scoped table:\n",[273,727,729],{"class":275,"line":728},80,[273,730,731],{},"  ```\n",[273,733,735],{"class":275,"line":734},81,[273,736,737],{},"  grant select on \u003Ctable> to chat_reader;\n",[273,739,741],{"class":275,"line":740},82,[273,742,743],{},"  create policy \"\u003Ctable>_select_chat\" on \u003Ctable>\n",[273,745,747],{"class":275,"line":746},83,[273,748,749],{},"    for select to chat_reader using (team_id = current_chat_team());\n",[273,751,753],{"class":275,"line":752},84,[273,754,731],{},[273,756,758],{"class":275,"line":757},85,[273,759,760],{},"  Skip tables without a `team_id` column (scope them through a parent).\n",[273,762,764],{"class":275,"line":763},86,[273,765,766],{},"- Register each table in `tablePermissions` in `shared\u002Fpermissions.ts`\n",[273,768,770],{"class":275,"line":769},87,[273,771,772],{},"  using the permission keys above.\n",[273,774,776],{"class":275,"line":775},88,[273,777,778],{},"- Add filter entries in `app\u002Fcomponents\u002Factivity\u002FList.vue` (`tableItems`)\n",[273,780,782],{"class":275,"line":781},89,[273,783,784],{},"  for each new table.\n",[273,786,788],{"class":275,"line":787},90,[273,789,292],{"emptyLinePlaceholder":291},[273,791,793],{"class":275,"line":792},91,[273,794,795],{},"Regenerate the TypeScript types via Supabase MCP and save them to\n",[273,797,799],{"class":275,"line":798},92,[273,800,801],{},"`shared\u002Ftypes\u002Fdatabase.types.ts`.\n",[803,804,805],"tip",{},[198,806,807,810,811,813],{},[201,808,809],{},"Adding the public API later?"," If you plan to add the ",[206,812,59],{"href":60}," plugin later, its page will guide you through adding the required permissions. You don't need to add them now.",[258,815,817],{"id":816},"products","Products",[263,819,821],{"className":265,"code":820,"language":267,"meta":268,"style":268},"Build the product catalog 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]\u002Fproducts` — uses `authUser(event, \"products.view\")`.\n  Returns all products for the team, ordered by name asc. Include the total\n  stock quantity across all warehouses by joining and summing from stock_levels.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fproducts` — uses `authUser(event, \"products.create\")`.\n  Reads { name, sku, category, description, unit_price, unit, reorder_point }\n  from the body. Validates that name and sku are required. Sets team_id from\n  the auth context and created_by from the authenticated user (user.sub).\n  Return 409 if the SKU already exists for this team.\n\n- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fproducts\u002F[productId]` — uses\n  `authUser(event, \"products.update\")`. Updates whichever fields are provided\n  in the body. Validates that the product belongs to the team before updating.\n  If sku is being changed, check uniqueness within the team.\n\n- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Fproducts\u002F[productId]` — uses\n  `authUser(event, \"products.delete\")`. Validates that the product belongs to\n  the team before deleting.\n\nUI:\n\nCreate `app\u002Fpages\u002Fapp\u002Fproducts\u002Findex.vue` — a product list page as a top-level\nroute (under `\u002Fapp`, sibling of other features), wrapped in a `UDashboardPanel`:\n\n- `UDashboardNavbar` in the header: title \"Products\",\n  `UDashboardSidebarCollapse` on the left. On the right, a \"New Product\"\n  button wrapped in `CanAccess permission=\"products.create\"`.\n\n- The body shows a table of products with columns: name, SKU, category,\n  unit price (formatted as currency), total stock, and a low-stock indicator\n  (show a warning badge when total stock \u003C= reorder_point). Use `USkeleton`\n  for loading state on initial load. Show an empty state with an icon and\n  message when there are no products.\n\n- \"New Product\" button opens a `UModal` with a form: name (required),\n  SKU (required), category, description, unit price, unit (default \"each\"),\n  reorder point (default 0). Use Zod for validation. Show loading state on\n  the submit button. Show success toast on creation. Show error toast if\n  SKU is a duplicate.\n\n- Clicking a product row opens a `USlideover` for editing. Show all fields\n  in a form. Below the form, show a \"Stock by Warehouse\" section: a read-only\n  list showing each warehouse name and the quantity of this product in that\n  warehouse. Include a delete button at the bottom wrapped in\n  `CanAccess permission=\"products.delete\"` — clicking it shows a confirmation\n  modal before deleting. Show loading on the save button.\n\n- After creating, editing, or deleting a product, refresh the product list.\n\nSidebar navigation:\n\nAdd a \"Products\" link to the top navigation group in\n`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Dashboard\" and\n\"Settings\". Use the icon `i-solar-box-bold-duotone`.\n\nThe `items` ref is already a `computed\u003CNavigationMenuItem[]>` in this\ntemplate — push the new entry into that list. All roles see the Products\nlink for now; use `can()` from `useUserRole()` if you need to gate later.\n",[270,822,823,828,832,837,841,846,851,856,860,865,870,875,880,885,889,894,899,904,909,913,918,923,928,932,937,941,946,951,955,960,965,970,974,979,984,989,994,999,1003,1008,1013,1018,1023,1028,1032,1037,1042,1047,1052,1057,1062,1066,1071,1075,1080,1084,1089,1094,1099,1103,1108,1113],{"__ignoreMap":268},[273,824,825],{"class":275,"line":276},[273,826,827],{},"Build the product catalog module — server routes and a full UI page.\n",[273,829,830],{"class":275,"line":282},[273,831,292],{"emptyLinePlaceholder":291},[273,833,834],{"class":275,"line":288},[273,835,836],{},"Server routes (all use `authUser` with the appropriate permission):\n",[273,838,839],{"class":275,"line":295},[273,840,292],{"emptyLinePlaceholder":291},[273,842,843],{"class":275,"line":301},[273,844,845],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fproducts` — uses `authUser(event, \"products.view\")`.\n",[273,847,848],{"class":275,"line":306},[273,849,850],{},"  Returns all products for the team, ordered by name asc. Include the total\n",[273,852,853],{"class":275,"line":312},[273,854,855],{},"  stock quantity across all warehouses by joining and summing from stock_levels.\n",[273,857,858],{"class":275,"line":317},[273,859,292],{"emptyLinePlaceholder":291},[273,861,862],{"class":275,"line":323},[273,863,864],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fproducts` — uses `authUser(event, \"products.create\")`.\n",[273,866,867],{"class":275,"line":329},[273,868,869],{},"  Reads { name, sku, category, description, unit_price, unit, reorder_point }\n",[273,871,872],{"class":275,"line":335},[273,873,874],{},"  from the body. Validates that name and sku are required. Sets team_id from\n",[273,876,877],{"class":275,"line":341},[273,878,879],{},"  the auth context and created_by from the authenticated user (user.sub).\n",[273,881,882],{"class":275,"line":347},[273,883,884],{},"  Return 409 if the SKU already exists for this team.\n",[273,886,887],{"class":275,"line":353},[273,888,292],{"emptyLinePlaceholder":291},[273,890,891],{"class":275,"line":359},[273,892,893],{},"- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fproducts\u002F[productId]` — uses\n",[273,895,896],{"class":275,"line":365},[273,897,898],{},"  `authUser(event, \"products.update\")`. Updates whichever fields are provided\n",[273,900,901],{"class":275,"line":371},[273,902,903],{},"  in the body. Validates that the product belongs to the team before updating.\n",[273,905,906],{"class":275,"line":377},[273,907,908],{},"  If sku is being changed, check uniqueness within the team.\n",[273,910,911],{"class":275,"line":383},[273,912,292],{"emptyLinePlaceholder":291},[273,914,915],{"class":275,"line":388},[273,916,917],{},"- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Fproducts\u002F[productId]` — uses\n",[273,919,920],{"class":275,"line":394},[273,921,922],{},"  `authUser(event, \"products.delete\")`. Validates that the product belongs to\n",[273,924,925],{"class":275,"line":399},[273,926,927],{},"  the team before deleting.\n",[273,929,930],{"class":275,"line":405},[273,931,292],{"emptyLinePlaceholder":291},[273,933,934],{"class":275,"line":411},[273,935,936],{},"UI:\n",[273,938,939],{"class":275,"line":416},[273,940,292],{"emptyLinePlaceholder":291},[273,942,943],{"class":275,"line":421},[273,944,945],{},"Create `app\u002Fpages\u002Fapp\u002Fproducts\u002Findex.vue` — a product list page as a top-level\n",[273,947,948],{"class":275,"line":426},[273,949,950],{},"route (under `\u002Fapp`, sibling of other features), wrapped in a `UDashboardPanel`:\n",[273,952,953],{"class":275,"line":431},[273,954,292],{"emptyLinePlaceholder":291},[273,956,957],{"class":275,"line":437},[273,958,959],{},"- `UDashboardNavbar` in the header: title \"Products\",\n",[273,961,962],{"class":275,"line":442},[273,963,964],{},"  `UDashboardSidebarCollapse` on the left. On the right, a \"New Product\"\n",[273,966,967],{"class":275,"line":448},[273,968,969],{},"  button wrapped in `CanAccess permission=\"products.create\"`.\n",[273,971,972],{"class":275,"line":454},[273,973,292],{"emptyLinePlaceholder":291},[273,975,976],{"class":275,"line":460},[273,977,978],{},"- The body shows a table of products with columns: name, SKU, category,\n",[273,980,981],{"class":275,"line":465},[273,982,983],{},"  unit price (formatted as currency), total stock, and a low-stock indicator\n",[273,985,986],{"class":275,"line":471},[273,987,988],{},"  (show a warning badge when total stock \u003C= reorder_point). Use `USkeleton`\n",[273,990,991],{"class":275,"line":476},[273,992,993],{},"  for loading state on initial load. Show an empty state with an icon and\n",[273,995,996],{"class":275,"line":482},[273,997,998],{},"  message when there are no products.\n",[273,1000,1001],{"class":275,"line":487},[273,1002,292],{"emptyLinePlaceholder":291},[273,1004,1005],{"class":275,"line":492},[273,1006,1007],{},"- \"New Product\" button opens a `UModal` with a form: name (required),\n",[273,1009,1010],{"class":275,"line":497},[273,1011,1012],{},"  SKU (required), category, description, unit price, unit (default \"each\"),\n",[273,1014,1015],{"class":275,"line":503},[273,1016,1017],{},"  reorder point (default 0). Use Zod for validation. Show loading state on\n",[273,1019,1020],{"class":275,"line":509},[273,1021,1022],{},"  the submit button. Show success toast on creation. Show error toast if\n",[273,1024,1025],{"class":275,"line":515},[273,1026,1027],{},"  SKU is a duplicate.\n",[273,1029,1030],{"class":275,"line":521},[273,1031,292],{"emptyLinePlaceholder":291},[273,1033,1034],{"class":275,"line":526},[273,1035,1036],{},"- Clicking a product row opens a `USlideover` for editing. Show all fields\n",[273,1038,1039],{"class":275,"line":531},[273,1040,1041],{},"  in a form. Below the form, show a \"Stock by Warehouse\" section: a read-only\n",[273,1043,1044],{"class":275,"line":537},[273,1045,1046],{},"  list showing each warehouse name and the quantity of this product in that\n",[273,1048,1049],{"class":275,"line":542},[273,1050,1051],{},"  warehouse. Include a delete button at the bottom wrapped in\n",[273,1053,1054],{"class":275,"line":548},[273,1055,1056],{},"  `CanAccess permission=\"products.delete\"` — clicking it shows a confirmation\n",[273,1058,1059],{"class":275,"line":554},[273,1060,1061],{},"  modal before deleting. Show loading on the save button.\n",[273,1063,1064],{"class":275,"line":560},[273,1065,292],{"emptyLinePlaceholder":291},[273,1067,1068],{"class":275,"line":565},[273,1069,1070],{},"- After creating, editing, or deleting a product, refresh the product list.\n",[273,1072,1073],{"class":275,"line":571},[273,1074,292],{"emptyLinePlaceholder":291},[273,1076,1077],{"class":275,"line":577},[273,1078,1079],{},"Sidebar navigation:\n",[273,1081,1082],{"class":275,"line":583},[273,1083,292],{"emptyLinePlaceholder":291},[273,1085,1086],{"class":275,"line":589},[273,1087,1088],{},"Add a \"Products\" link to the top navigation group in\n",[273,1090,1091],{"class":275,"line":595},[273,1092,1093],{},"`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Dashboard\" and\n",[273,1095,1096],{"class":275,"line":600},[273,1097,1098],{},"\"Settings\". Use the icon `i-solar-box-bold-duotone`.\n",[273,1100,1101],{"class":275,"line":606},[273,1102,292],{"emptyLinePlaceholder":291},[273,1104,1105],{"class":275,"line":611},[273,1106,1107],{},"The `items` ref is already a `computed\u003CNavigationMenuItem[]>` in this\n",[273,1109,1110],{"class":275,"line":617},[273,1111,1112],{},"template — push the new entry into that list. All roles see the Products\n",[273,1114,1115],{"class":275,"line":623},[273,1116,1117],{},"link for now; use `can()` from `useUserRole()` if you need to gate later.\n",[258,1119,233],{"id":1120},"warehouses",[263,1122,1124],{"className":265,"code":1123,"language":267,"meta":268,"style":268},"Build the warehouse management 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]\u002Fwarehouses` — uses\n  `authUser(event, \"warehouses.view\")`. Returns all warehouses for the team,\n  ordered by name asc. Include a count of distinct products stored and total\n  stock quantity by joining from stock_levels.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fwarehouses` — uses\n  `authUser(event, \"warehouses.create\")`. Reads { name, address, notes }\n  from the body. Validates that name is required. Sets team_id from auth\n  context and created_by from the authenticated user (user.sub).\n\n- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fwarehouses\u002F[warehouseId]` — uses\n  `authUser(event, \"warehouses.update\")`. Updates whichever fields are\n  provided in the body. Validates that the warehouse belongs to the team.\n\n- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Fwarehouses\u002F[warehouseId]` — uses\n  `authUser(event, \"warehouses.delete\")`. Validates that the warehouse\n  belongs to the team before deleting. Return 400 if the warehouse still\n  has stock (stock_levels with quantity > 0).\n\nUI:\n\nCreate `app\u002Fpages\u002Fapp\u002Fwarehouses\u002Findex.vue` — a warehouse 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 \"Warehouses\",\n  `UDashboardSidebarCollapse` on the left. On the right, a \"New Warehouse\"\n  button wrapped in `CanAccess permission=\"warehouses.create\"`.\n\n- The body shows a table of warehouses with columns: name, address,\n  product count, total stock quantity. Use `USkeleton` for loading state\n  on initial load. Show an empty state when there are no warehouses.\n\n- \"New Warehouse\" button opens a `UModal` with a form: name (required),\n  address, notes. Use Zod for validation. Show loading on submit.\n\n- Clicking a warehouse row opens a `USlideover` for editing. Show all\n  fields in a form. Below the form, show an \"Inventory\" section: a\n  read-only list of products in this warehouse with their quantities.\n  Include a delete button wrapped in\n  `CanAccess permission=\"warehouses.delete\"` with a confirmation modal.\n\n- After creating, editing, or deleting a warehouse, refresh the list.\n\nSidebar navigation:\n\nAdd a \"Warehouses\" link to the top navigation group in\n`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Products\" and\n\"Settings\". Use the icon `i-solar-buildings-2-bold-duotone`.\n",[270,1125,1126,1131,1135,1139,1143,1148,1153,1158,1163,1167,1172,1177,1182,1187,1191,1196,1201,1206,1210,1215,1220,1225,1230,1234,1238,1242,1247,1252,1257,1261,1266,1271,1276,1280,1285,1290,1295,1299,1304,1309,1313,1318,1323,1328,1333,1338,1342,1347,1351,1355,1359,1364,1369],{"__ignoreMap":268},[273,1127,1128],{"class":275,"line":276},[273,1129,1130],{},"Build the warehouse management module — server routes and a full UI page.\n",[273,1132,1133],{"class":275,"line":282},[273,1134,292],{"emptyLinePlaceholder":291},[273,1136,1137],{"class":275,"line":288},[273,1138,836],{},[273,1140,1141],{"class":275,"line":295},[273,1142,292],{"emptyLinePlaceholder":291},[273,1144,1145],{"class":275,"line":301},[273,1146,1147],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fwarehouses` — uses\n",[273,1149,1150],{"class":275,"line":306},[273,1151,1152],{},"  `authUser(event, \"warehouses.view\")`. Returns all warehouses for the team,\n",[273,1154,1155],{"class":275,"line":312},[273,1156,1157],{},"  ordered by name asc. Include a count of distinct products stored and total\n",[273,1159,1160],{"class":275,"line":317},[273,1161,1162],{},"  stock quantity by joining from stock_levels.\n",[273,1164,1165],{"class":275,"line":323},[273,1166,292],{"emptyLinePlaceholder":291},[273,1168,1169],{"class":275,"line":329},[273,1170,1171],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fwarehouses` — uses\n",[273,1173,1174],{"class":275,"line":335},[273,1175,1176],{},"  `authUser(event, \"warehouses.create\")`. Reads { name, address, notes }\n",[273,1178,1179],{"class":275,"line":341},[273,1180,1181],{},"  from the body. Validates that name is required. Sets team_id from auth\n",[273,1183,1184],{"class":275,"line":347},[273,1185,1186],{},"  context and created_by from the authenticated user (user.sub).\n",[273,1188,1189],{"class":275,"line":353},[273,1190,292],{"emptyLinePlaceholder":291},[273,1192,1193],{"class":275,"line":359},[273,1194,1195],{},"- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fwarehouses\u002F[warehouseId]` — uses\n",[273,1197,1198],{"class":275,"line":365},[273,1199,1200],{},"  `authUser(event, \"warehouses.update\")`. Updates whichever fields are\n",[273,1202,1203],{"class":275,"line":371},[273,1204,1205],{},"  provided in the body. Validates that the warehouse belongs to the team.\n",[273,1207,1208],{"class":275,"line":377},[273,1209,292],{"emptyLinePlaceholder":291},[273,1211,1212],{"class":275,"line":383},[273,1213,1214],{},"- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Fwarehouses\u002F[warehouseId]` — uses\n",[273,1216,1217],{"class":275,"line":388},[273,1218,1219],{},"  `authUser(event, \"warehouses.delete\")`. Validates that the warehouse\n",[273,1221,1222],{"class":275,"line":394},[273,1223,1224],{},"  belongs to the team before deleting. Return 400 if the warehouse still\n",[273,1226,1227],{"class":275,"line":399},[273,1228,1229],{},"  has stock (stock_levels with quantity > 0).\n",[273,1231,1232],{"class":275,"line":405},[273,1233,292],{"emptyLinePlaceholder":291},[273,1235,1236],{"class":275,"line":411},[273,1237,936],{},[273,1239,1240],{"class":275,"line":416},[273,1241,292],{"emptyLinePlaceholder":291},[273,1243,1244],{"class":275,"line":421},[273,1245,1246],{},"Create `app\u002Fpages\u002Fapp\u002Fwarehouses\u002Findex.vue` — a warehouse list page as a\n",[273,1248,1249],{"class":275,"line":426},[273,1250,1251],{},"top-level route (under `\u002Fapp`, sibling of other features), wrapped in a\n",[273,1253,1254],{"class":275,"line":431},[273,1255,1256],{},"`UDashboardPanel`:\n",[273,1258,1259],{"class":275,"line":437},[273,1260,292],{"emptyLinePlaceholder":291},[273,1262,1263],{"class":275,"line":442},[273,1264,1265],{},"- `UDashboardNavbar` in the header: title \"Warehouses\",\n",[273,1267,1268],{"class":275,"line":448},[273,1269,1270],{},"  `UDashboardSidebarCollapse` on the left. On the right, a \"New Warehouse\"\n",[273,1272,1273],{"class":275,"line":454},[273,1274,1275],{},"  button wrapped in `CanAccess permission=\"warehouses.create\"`.\n",[273,1277,1278],{"class":275,"line":460},[273,1279,292],{"emptyLinePlaceholder":291},[273,1281,1282],{"class":275,"line":465},[273,1283,1284],{},"- The body shows a table of warehouses with columns: name, address,\n",[273,1286,1287],{"class":275,"line":471},[273,1288,1289],{},"  product count, total stock quantity. Use `USkeleton` for loading state\n",[273,1291,1292],{"class":275,"line":476},[273,1293,1294],{},"  on initial load. Show an empty state when there are no warehouses.\n",[273,1296,1297],{"class":275,"line":482},[273,1298,292],{"emptyLinePlaceholder":291},[273,1300,1301],{"class":275,"line":487},[273,1302,1303],{},"- \"New Warehouse\" button opens a `UModal` with a form: name (required),\n",[273,1305,1306],{"class":275,"line":492},[273,1307,1308],{},"  address, notes. Use Zod for validation. Show loading on submit.\n",[273,1310,1311],{"class":275,"line":497},[273,1312,292],{"emptyLinePlaceholder":291},[273,1314,1315],{"class":275,"line":503},[273,1316,1317],{},"- Clicking a warehouse row opens a `USlideover` for editing. Show all\n",[273,1319,1320],{"class":275,"line":509},[273,1321,1322],{},"  fields in a form. Below the form, show an \"Inventory\" section: a\n",[273,1324,1325],{"class":275,"line":515},[273,1326,1327],{},"  read-only list of products in this warehouse with their quantities.\n",[273,1329,1330],{"class":275,"line":521},[273,1331,1332],{},"  Include a delete button wrapped in\n",[273,1334,1335],{"class":275,"line":526},[273,1336,1337],{},"  `CanAccess permission=\"warehouses.delete\"` with a confirmation modal.\n",[273,1339,1340],{"class":275,"line":531},[273,1341,292],{"emptyLinePlaceholder":291},[273,1343,1344],{"class":275,"line":537},[273,1345,1346],{},"- After creating, editing, or deleting a warehouse, refresh the list.\n",[273,1348,1349],{"class":275,"line":542},[273,1350,292],{"emptyLinePlaceholder":291},[273,1352,1353],{"class":275,"line":548},[273,1354,1079],{},[273,1356,1357],{"class":275,"line":554},[273,1358,292],{"emptyLinePlaceholder":291},[273,1360,1361],{"class":275,"line":560},[273,1362,1363],{},"Add a \"Warehouses\" link to the top navigation group in\n",[273,1365,1366],{"class":275,"line":565},[273,1367,1368],{},"`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Products\" and\n",[273,1370,1371],{"class":275,"line":571},[273,1372,1373],{},"\"Settings\". Use the icon `i-solar-buildings-2-bold-duotone`.\n",[258,1375,1377],{"id":1376},"stock-movements","Stock Movements",[263,1379,1381],{"className":265,"code":1380,"language":267,"meta":268,"style":268},"Build the stock movement module — the core inventory operation feature.\n\nServer routes (all use `authUser` with the appropriate permission):\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fstock-movements` — uses\n  `authUser(event, \"inventory.view\")`. Returns all stock movements for the\n  team with product name and warehouse name joined in. Order by created_at\n  desc. Support optional query params: `?type=` to filter by movement type,\n  `?product_id=` to filter by product, `?warehouse_id=` to filter by\n  warehouse.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fstock-movements` — uses\n  `authUser(event, \"inventory.manage\")`. Reads { product_id, warehouse_id,\n  type, quantity, reference, notes } from the body. Validates that\n  product_id, warehouse_id, type, and quantity are required. Quantity must\n  be a positive integer.\n\n  After inserting the movement, update the stock_levels table:\n  - For \"inbound\" and \"adjustment\": upsert into stock_levels, adding the\n    quantity (use ON CONFLICT on product_id, warehouse_id).\n  - For \"outbound\": subtract the quantity. Return 400 if the resulting\n    quantity would be negative.\n  - For \"transfer\": this requires a second warehouse_id field\n    (`to_warehouse_id`). Subtract from the source warehouse and add to\n    the destination warehouse. Return 400 if the source would go negative.\n    Create two stock_movement records — one outbound from source, one\n    inbound to destination — linked by the same reference string.\n\n  Sets team_id from auth context and created_by from the authenticated\n  user (user.sub).\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fstock-levels` — uses\n  `authUser(event, \"inventory.view\")`. Returns all stock levels for the\n  team with product name, product SKU, and warehouse name joined in.\n  Support optional query params: `?warehouse_id=` and `?low_stock=true`\n  (filter to items where quantity \u003C= product's reorder_point).\n\nUI:\n\nCreate `app\u002Fpages\u002Fapp\u002Finventory\u002Findex.vue` — the main inventory page as a\ntop-level route, wrapped in a `UDashboardPanel`:\n\n- `UDashboardNavbar` in the header: title \"Inventory\",\n  `UDashboardSidebarCollapse` on the left. On the right, a \"Record Movement\"\n  button wrapped in `CanAccess permission=\"inventory.manage\"`.\n\n- `UDashboardToolbar` below the navbar: on the left, a toggle between\n  \"Stock Levels\" and \"Movements\" views using `UTabs` with `size=\"xs\"`.\n  On the right, a warehouse filter `USelect` (populated from the\n  warehouses endpoint).\n\n- **Stock Levels view** (default): a table with columns: product name,\n  SKU, warehouse, quantity, reorder point, and status. Status shows a\n  green \"In Stock\" badge when quantity > reorder_point, a yellow \"Low Stock\"\n  badge when quantity \u003C= reorder_point and quantity > 0, and a red\n  \"Out of Stock\" badge when quantity is 0.\n\n- **Movements view**: a table with columns: date (formatted nicely),\n  type badge (color-coded — inbound = \"success\", outbound = \"error\",\n  transfer = \"info\", adjustment = \"warning\"), product name, warehouse,\n  quantity (with +\u002F- prefix based on type), reference, and who recorded it.\n\n- \"Record Movement\" button opens a `UModal` with a form: type (required —\n  select from inbound\u002Foutbound\u002Fadjustment\u002Ftransfer), product (required —\n  select from products fetched on mount), warehouse (required — select\n  from warehouses fetched on mount), quantity (required, positive integer),\n  reference, notes. When type is \"transfer\", show a second warehouse\n  select for the destination. Use Zod for validation. Show loading on\n  submit. Show error toast if stock would go negative.\n\n- After recording a movement, refresh both views.\n\nSidebar navigation:\n\nAdd an \"Inventory\" link to the top navigation group in\n`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Warehouses\" and\n\"Settings\". Use the icon `i-solar-chart-square-bold-duotone`.\n",[270,1382,1383,1388,1392,1396,1400,1405,1410,1415,1420,1425,1430,1434,1439,1444,1449,1454,1459,1463,1468,1473,1478,1483,1488,1493,1498,1503,1508,1513,1517,1522,1527,1531,1536,1541,1546,1551,1556,1560,1564,1568,1573,1578,1582,1587,1592,1597,1601,1606,1611,1616,1621,1625,1630,1635,1640,1645,1650,1654,1659,1664,1669,1674,1678,1683,1688,1693,1698,1703,1708,1713,1717,1722,1726,1730,1734,1739,1744],{"__ignoreMap":268},[273,1384,1385],{"class":275,"line":276},[273,1386,1387],{},"Build the stock movement module — the core inventory operation feature.\n",[273,1389,1390],{"class":275,"line":282},[273,1391,292],{"emptyLinePlaceholder":291},[273,1393,1394],{"class":275,"line":288},[273,1395,836],{},[273,1397,1398],{"class":275,"line":295},[273,1399,292],{"emptyLinePlaceholder":291},[273,1401,1402],{"class":275,"line":301},[273,1403,1404],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fstock-movements` — uses\n",[273,1406,1407],{"class":275,"line":306},[273,1408,1409],{},"  `authUser(event, \"inventory.view\")`. Returns all stock movements for the\n",[273,1411,1412],{"class":275,"line":312},[273,1413,1414],{},"  team with product name and warehouse name joined in. Order by created_at\n",[273,1416,1417],{"class":275,"line":317},[273,1418,1419],{},"  desc. Support optional query params: `?type=` to filter by movement type,\n",[273,1421,1422],{"class":275,"line":323},[273,1423,1424],{},"  `?product_id=` to filter by product, `?warehouse_id=` to filter by\n",[273,1426,1427],{"class":275,"line":329},[273,1428,1429],{},"  warehouse.\n",[273,1431,1432],{"class":275,"line":335},[273,1433,292],{"emptyLinePlaceholder":291},[273,1435,1436],{"class":275,"line":341},[273,1437,1438],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fstock-movements` — uses\n",[273,1440,1441],{"class":275,"line":347},[273,1442,1443],{},"  `authUser(event, \"inventory.manage\")`. Reads { product_id, warehouse_id,\n",[273,1445,1446],{"class":275,"line":353},[273,1447,1448],{},"  type, quantity, reference, notes } from the body. Validates that\n",[273,1450,1451],{"class":275,"line":359},[273,1452,1453],{},"  product_id, warehouse_id, type, and quantity are required. Quantity must\n",[273,1455,1456],{"class":275,"line":365},[273,1457,1458],{},"  be a positive integer.\n",[273,1460,1461],{"class":275,"line":371},[273,1462,292],{"emptyLinePlaceholder":291},[273,1464,1465],{"class":275,"line":377},[273,1466,1467],{},"  After inserting the movement, update the stock_levels table:\n",[273,1469,1470],{"class":275,"line":383},[273,1471,1472],{},"  - For \"inbound\" and \"adjustment\": upsert into stock_levels, adding the\n",[273,1474,1475],{"class":275,"line":388},[273,1476,1477],{},"    quantity (use ON CONFLICT on product_id, warehouse_id).\n",[273,1479,1480],{"class":275,"line":394},[273,1481,1482],{},"  - For \"outbound\": subtract the quantity. Return 400 if the resulting\n",[273,1484,1485],{"class":275,"line":399},[273,1486,1487],{},"    quantity would be negative.\n",[273,1489,1490],{"class":275,"line":405},[273,1491,1492],{},"  - For \"transfer\": this requires a second warehouse_id field\n",[273,1494,1495],{"class":275,"line":411},[273,1496,1497],{},"    (`to_warehouse_id`). Subtract from the source warehouse and add to\n",[273,1499,1500],{"class":275,"line":416},[273,1501,1502],{},"    the destination warehouse. Return 400 if the source would go negative.\n",[273,1504,1505],{"class":275,"line":421},[273,1506,1507],{},"    Create two stock_movement records — one outbound from source, one\n",[273,1509,1510],{"class":275,"line":426},[273,1511,1512],{},"    inbound to destination — linked by the same reference string.\n",[273,1514,1515],{"class":275,"line":431},[273,1516,292],{"emptyLinePlaceholder":291},[273,1518,1519],{"class":275,"line":437},[273,1520,1521],{},"  Sets team_id from auth context and created_by from the authenticated\n",[273,1523,1524],{"class":275,"line":442},[273,1525,1526],{},"  user (user.sub).\n",[273,1528,1529],{"class":275,"line":448},[273,1530,292],{"emptyLinePlaceholder":291},[273,1532,1533],{"class":275,"line":454},[273,1534,1535],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fstock-levels` — uses\n",[273,1537,1538],{"class":275,"line":460},[273,1539,1540],{},"  `authUser(event, \"inventory.view\")`. Returns all stock levels for the\n",[273,1542,1543],{"class":275,"line":465},[273,1544,1545],{},"  team with product name, product SKU, and warehouse name joined in.\n",[273,1547,1548],{"class":275,"line":471},[273,1549,1550],{},"  Support optional query params: `?warehouse_id=` and `?low_stock=true`\n",[273,1552,1553],{"class":275,"line":476},[273,1554,1555],{},"  (filter to items where quantity \u003C= product's reorder_point).\n",[273,1557,1558],{"class":275,"line":482},[273,1559,292],{"emptyLinePlaceholder":291},[273,1561,1562],{"class":275,"line":487},[273,1563,936],{},[273,1565,1566],{"class":275,"line":492},[273,1567,292],{"emptyLinePlaceholder":291},[273,1569,1570],{"class":275,"line":497},[273,1571,1572],{},"Create `app\u002Fpages\u002Fapp\u002Finventory\u002Findex.vue` — the main inventory page as a\n",[273,1574,1575],{"class":275,"line":503},[273,1576,1577],{},"top-level route, wrapped in a `UDashboardPanel`:\n",[273,1579,1580],{"class":275,"line":509},[273,1581,292],{"emptyLinePlaceholder":291},[273,1583,1584],{"class":275,"line":515},[273,1585,1586],{},"- `UDashboardNavbar` in the header: title \"Inventory\",\n",[273,1588,1589],{"class":275,"line":521},[273,1590,1591],{},"  `UDashboardSidebarCollapse` on the left. On the right, a \"Record Movement\"\n",[273,1593,1594],{"class":275,"line":526},[273,1595,1596],{},"  button wrapped in `CanAccess permission=\"inventory.manage\"`.\n",[273,1598,1599],{"class":275,"line":531},[273,1600,292],{"emptyLinePlaceholder":291},[273,1602,1603],{"class":275,"line":537},[273,1604,1605],{},"- `UDashboardToolbar` below the navbar: on the left, a toggle between\n",[273,1607,1608],{"class":275,"line":542},[273,1609,1610],{},"  \"Stock Levels\" and \"Movements\" views using `UTabs` with `size=\"xs\"`.\n",[273,1612,1613],{"class":275,"line":548},[273,1614,1615],{},"  On the right, a warehouse filter `USelect` (populated from the\n",[273,1617,1618],{"class":275,"line":554},[273,1619,1620],{},"  warehouses endpoint).\n",[273,1622,1623],{"class":275,"line":560},[273,1624,292],{"emptyLinePlaceholder":291},[273,1626,1627],{"class":275,"line":565},[273,1628,1629],{},"- **Stock Levels view** (default): a table with columns: product name,\n",[273,1631,1632],{"class":275,"line":571},[273,1633,1634],{},"  SKU, warehouse, quantity, reorder point, and status. Status shows a\n",[273,1636,1637],{"class":275,"line":577},[273,1638,1639],{},"  green \"In Stock\" badge when quantity > reorder_point, a yellow \"Low Stock\"\n",[273,1641,1642],{"class":275,"line":583},[273,1643,1644],{},"  badge when quantity \u003C= reorder_point and quantity > 0, and a red\n",[273,1646,1647],{"class":275,"line":589},[273,1648,1649],{},"  \"Out of Stock\" badge when quantity is 0.\n",[273,1651,1652],{"class":275,"line":595},[273,1653,292],{"emptyLinePlaceholder":291},[273,1655,1656],{"class":275,"line":600},[273,1657,1658],{},"- **Movements view**: a table with columns: date (formatted nicely),\n",[273,1660,1661],{"class":275,"line":606},[273,1662,1663],{},"  type badge (color-coded — inbound = \"success\", outbound = \"error\",\n",[273,1665,1666],{"class":275,"line":611},[273,1667,1668],{},"  transfer = \"info\", adjustment = \"warning\"), product name, warehouse,\n",[273,1670,1671],{"class":275,"line":617},[273,1672,1673],{},"  quantity (with +\u002F- prefix based on type), reference, and who recorded it.\n",[273,1675,1676],{"class":275,"line":623},[273,1677,292],{"emptyLinePlaceholder":291},[273,1679,1680],{"class":275,"line":629},[273,1681,1682],{},"- \"Record Movement\" button opens a `UModal` with a form: type (required —\n",[273,1684,1685],{"class":275,"line":635},[273,1686,1687],{},"  select from inbound\u002Foutbound\u002Fadjustment\u002Ftransfer), product (required —\n",[273,1689,1690],{"class":275,"line":641},[273,1691,1692],{},"  select from products fetched on mount), warehouse (required — select\n",[273,1694,1695],{"class":275,"line":647},[273,1696,1697],{},"  from warehouses fetched on mount), quantity (required, positive integer),\n",[273,1699,1700],{"class":275,"line":653},[273,1701,1702],{},"  reference, notes. When type is \"transfer\", show a second warehouse\n",[273,1704,1705],{"class":275,"line":659},[273,1706,1707],{},"  select for the destination. Use Zod for validation. Show loading on\n",[273,1709,1710],{"class":275,"line":665},[273,1711,1712],{},"  submit. Show error toast if stock would go negative.\n",[273,1714,1715],{"class":275,"line":671},[273,1716,292],{"emptyLinePlaceholder":291},[273,1718,1719],{"class":275,"line":677},[273,1720,1721],{},"- After recording a movement, refresh both views.\n",[273,1723,1724],{"class":275,"line":682},[273,1725,292],{"emptyLinePlaceholder":291},[273,1727,1728],{"class":275,"line":687},[273,1729,1079],{},[273,1731,1732],{"class":275,"line":693},[273,1733,292],{"emptyLinePlaceholder":291},[273,1735,1736],{"class":275,"line":699},[273,1737,1738],{},"Add an \"Inventory\" link to the top navigation group in\n",[273,1740,1741],{"class":275,"line":704},[273,1742,1743],{},"`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Warehouses\" and\n",[273,1745,1746],{"class":275,"line":710},[273,1747,1748],{},"\"Settings\". Use the icon `i-solar-chart-square-bold-duotone`.\n",[258,1750,1752],{"id":1751},"dashboard-and-realtime","Dashboard and Realtime",[1754,1755,1757],"h4",{"id":1756},"dashboard","Dashboard",[263,1759,1761],{"className":265,"code":1760,"language":267,"meta":268,"style":268},"Replace the placeholder dashboard with real inventory data and stats.\n\nServer route:\n\nCreate `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fstats` — uses `authUser(event, \"team.view\")`.\nReturns a JSON object with:\n\n- `product_count` — total number of products for the team\n- `warehouse_count` — total number of warehouses for the team\n- `total_stock_value` — sum of (stock_levels.quantity * products.unit_price)\n  across all products and warehouses. Return 0 if no stock.\n- `low_stock_count` — count of stock_level rows where quantity \u003C=\n  the product's reorder_point and quantity > 0\n- `out_of_stock_count` — count of stock_level rows where quantity = 0\n- `recent_movements` — the last 10 stock movements with product name,\n  warehouse name, type, quantity, and created_at joined in\n- `low_stock_items` — up to 10 products where any stock_level quantity \u003C=\n  reorder_point, with product name, SKU, warehouse name, quantity, and\n  reorder_point\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 Products\" (product_count),\n  \"Warehouses\" (warehouse_count), \"Stock Value\" (total_stock_value formatted\n  as currency), \"Low Stock Alerts\" (low_stock_count + out_of_stock_count).\n  Use `USkeleton` placeholders while loading.\n\n- Below the stats, a two-column layout:\n  - Left column: \"Low Stock Alerts\" — a list of products with low or zero\n    stock. Each item shows product name, SKU, warehouse, current quantity,\n    and reorder point. Use red text for out-of-stock items and yellow for\n    low-stock. Show an empty state if everything is well-stocked.\n  - Right column: \"Recent Movements\" — a list of the last 10 stock\n    movements. Each item shows movement type badge, product name, warehouse,\n    quantity, and timestamp. 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,1762,1763,1768,1772,1777,1781,1786,1791,1795,1800,1805,1810,1815,1820,1825,1830,1835,1840,1845,1850,1855,1859,1864,1868,1872,1876,1881,1885,1890,1895,1900,1905,1910,1914,1919,1924,1929,1934,1939,1944,1949,1954,1958,1963],{"__ignoreMap":268},[273,1764,1765],{"class":275,"line":276},[273,1766,1767],{},"Replace the placeholder dashboard with real inventory data and stats.\n",[273,1769,1770],{"class":275,"line":282},[273,1771,292],{"emptyLinePlaceholder":291},[273,1773,1774],{"class":275,"line":288},[273,1775,1776],{},"Server route:\n",[273,1778,1779],{"class":275,"line":295},[273,1780,292],{"emptyLinePlaceholder":291},[273,1782,1783],{"class":275,"line":301},[273,1784,1785],{},"Create `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fstats` — uses `authUser(event, \"team.view\")`.\n",[273,1787,1788],{"class":275,"line":306},[273,1789,1790],{},"Returns a JSON object with:\n",[273,1792,1793],{"class":275,"line":312},[273,1794,292],{"emptyLinePlaceholder":291},[273,1796,1797],{"class":275,"line":317},[273,1798,1799],{},"- `product_count` — total number of products for the team\n",[273,1801,1802],{"class":275,"line":323},[273,1803,1804],{},"- `warehouse_count` — total number of warehouses for the team\n",[273,1806,1807],{"class":275,"line":329},[273,1808,1809],{},"- `total_stock_value` — sum of (stock_levels.quantity * products.unit_price)\n",[273,1811,1812],{"class":275,"line":335},[273,1813,1814],{},"  across all products and warehouses. Return 0 if no stock.\n",[273,1816,1817],{"class":275,"line":341},[273,1818,1819],{},"- `low_stock_count` — count of stock_level rows where quantity \u003C=\n",[273,1821,1822],{"class":275,"line":347},[273,1823,1824],{},"  the product's reorder_point and quantity > 0\n",[273,1826,1827],{"class":275,"line":353},[273,1828,1829],{},"- `out_of_stock_count` — count of stock_level rows where quantity = 0\n",[273,1831,1832],{"class":275,"line":359},[273,1833,1834],{},"- `recent_movements` — the last 10 stock movements with product name,\n",[273,1836,1837],{"class":275,"line":365},[273,1838,1839],{},"  warehouse name, type, quantity, and created_at joined in\n",[273,1841,1842],{"class":275,"line":371},[273,1843,1844],{},"- `low_stock_items` — up to 10 products where any stock_level quantity \u003C=\n",[273,1846,1847],{"class":275,"line":377},[273,1848,1849],{},"  reorder_point, with product name, SKU, warehouse name, quantity, and\n",[273,1851,1852],{"class":275,"line":383},[273,1853,1854],{},"  reorder_point\n",[273,1856,1857],{"class":275,"line":388},[273,1858,292],{"emptyLinePlaceholder":291},[273,1860,1861],{"class":275,"line":394},[273,1862,1863],{},"All queries are scoped to the team's team_id.\n",[273,1865,1866],{"class":275,"line":399},[273,1867,292],{"emptyLinePlaceholder":291},[273,1869,1870],{"class":275,"line":405},[273,1871,936],{},[273,1873,1874],{"class":275,"line":411},[273,1875,292],{"emptyLinePlaceholder":291},[273,1877,1878],{"class":275,"line":416},[273,1879,1880],{},"Update `app\u002Fpages\u002Fapp\u002Findex.vue` to show:\n",[273,1882,1883],{"class":275,"line":421},[273,1884,292],{"emptyLinePlaceholder":291},[273,1886,1887],{"class":275,"line":426},[273,1888,1889],{},"- A row of four stat cards at the top using a grid layout. Each card shows\n",[273,1891,1892],{"class":275,"line":431},[273,1893,1894],{},"  an icon, a label, and the value. Cards: \"Total Products\" (product_count),\n",[273,1896,1897],{"class":275,"line":437},[273,1898,1899],{},"  \"Warehouses\" (warehouse_count), \"Stock Value\" (total_stock_value formatted\n",[273,1901,1902],{"class":275,"line":442},[273,1903,1904],{},"  as currency), \"Low Stock Alerts\" (low_stock_count + out_of_stock_count).\n",[273,1906,1907],{"class":275,"line":448},[273,1908,1909],{},"  Use `USkeleton` placeholders while loading.\n",[273,1911,1912],{"class":275,"line":454},[273,1913,292],{"emptyLinePlaceholder":291},[273,1915,1916],{"class":275,"line":460},[273,1917,1918],{},"- Below the stats, a two-column layout:\n",[273,1920,1921],{"class":275,"line":465},[273,1922,1923],{},"  - Left column: \"Low Stock Alerts\" — a list of products with low or zero\n",[273,1925,1926],{"class":275,"line":471},[273,1927,1928],{},"    stock. Each item shows product name, SKU, warehouse, current quantity,\n",[273,1930,1931],{"class":275,"line":476},[273,1932,1933],{},"    and reorder point. Use red text for out-of-stock items and yellow for\n",[273,1935,1936],{"class":275,"line":482},[273,1937,1938],{},"    low-stock. Show an empty state if everything is well-stocked.\n",[273,1940,1941],{"class":275,"line":487},[273,1942,1943],{},"  - Right column: \"Recent Movements\" — a list of the last 10 stock\n",[273,1945,1946],{"class":275,"line":492},[273,1947,1948],{},"    movements. Each item shows movement type badge, product name, warehouse,\n",[273,1950,1951],{"class":275,"line":497},[273,1952,1953],{},"    quantity, and timestamp. Show an empty state if none.\n",[273,1955,1956],{"class":275,"line":503},[273,1957,292],{"emptyLinePlaceholder":291},[273,1959,1960],{"class":275,"line":509},[273,1961,1962],{},"Remove any placeholder\u002Fscaffolding content that was in the dashboard before.\n",[273,1964,1965],{"class":275,"line":515},[273,1966,1967],{},"Keep the `UDashboardPanel` wrapper with navbar and sidebar collapse.\n",[1754,1969,251],{"id":1970},"realtime",[263,1972,1974],{"className":265,"code":1973,"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 products, warehouses, stock_levels, stock_movements;\nALTER TABLE products REPLICA IDENTITY FULL;\nALTER TABLE warehouses REPLICA IDENTITY FULL;\nALTER TABLE stock_levels REPLICA IDENTITY FULL;\nALTER TABLE stock_movements REPLICA IDENTITY FULL;\n```\n\nUpdate `app\u002Fcomposables\u002FuseRealtime.ts`:\n\n1. Add `\"products\"`, `\"warehouses\"`, `\"stock_levels\"`, and\n   `\"stock_movements\"` 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.\n\nIntegration — use `onTableDebounced` from `useRealtime()` inline in each\npage. Do NOT create separate `useRealtimeX` composable files:\n\n- In `app\u002Fpages\u002Fapp\u002Fproducts\u002Findex.vue`:\n  `const { onTableDebounced } = useRealtime()`\n  `onTableDebounced(\"products\", () => refreshProducts())`\n- In `app\u002Fpages\u002Fapp\u002Finventory\u002Findex.vue`:\n  `const { onTableDebounced } = useRealtime()`\n  `onTableDebounced([\"stock_levels\", \"stock_movements\"], () => refreshInventory())`\n- In the dashboard stats page:\n  `const { onTableDebounced } = useRealtime()`\n  `onTableDebounced([\"products\", \"stock_levels\", \"stock_movements\"], () => refreshStats())`\n",[270,1975,1976,1981,1986,1990,1995,1999,2004,2008,2013,2018,2023,2028,2033,2038,2042,2046,2051,2055,2060,2065,2070,2075,2080,2084,2089,2094,2098,2103,2108,2113,2118,2122,2127,2132,2136],{"__ignoreMap":268},[273,1977,1978],{"class":275,"line":276},[273,1979,1980],{},"Add Supabase Realtime sync for the new tables so changes appear instantly\n",[273,1982,1983],{"class":275,"line":282},[273,1984,1985],{},"across browser sessions.\n",[273,1987,1988],{"class":275,"line":288},[273,1989,292],{"emptyLinePlaceholder":291},[273,1991,1992],{"class":275,"line":295},[273,1993,1994],{},"Database migration (via Supabase MCP):\n",[273,1996,1997],{"class":275,"line":301},[273,1998,292],{"emptyLinePlaceholder":291},[273,2000,2001],{"class":275,"line":306},[273,2002,2003],{},"Enable realtime publication and full replica identity for the new tables:\n",[273,2005,2006],{"class":275,"line":312},[273,2007,292],{"emptyLinePlaceholder":291},[273,2009,2010],{"class":275,"line":317},[273,2011,2012],{},"```sql\n",[273,2014,2015],{"class":275,"line":323},[273,2016,2017],{},"ALTER PUBLICATION supabase_realtime ADD TABLE products, warehouses, stock_levels, stock_movements;\n",[273,2019,2020],{"class":275,"line":329},[273,2021,2022],{},"ALTER TABLE products REPLICA IDENTITY FULL;\n",[273,2024,2025],{"class":275,"line":335},[273,2026,2027],{},"ALTER TABLE warehouses REPLICA IDENTITY FULL;\n",[273,2029,2030],{"class":275,"line":341},[273,2031,2032],{},"ALTER TABLE stock_levels REPLICA IDENTITY FULL;\n",[273,2034,2035],{"class":275,"line":347},[273,2036,2037],{},"ALTER TABLE stock_movements REPLICA IDENTITY FULL;\n",[273,2039,2040],{"class":275,"line":353},[273,2041,614],{},[273,2043,2044],{"class":275,"line":359},[273,2045,292],{"emptyLinePlaceholder":291},[273,2047,2048],{"class":275,"line":365},[273,2049,2050],{},"Update `app\u002Fcomposables\u002FuseRealtime.ts`:\n",[273,2052,2053],{"class":275,"line":371},[273,2054,292],{"emptyLinePlaceholder":291},[273,2056,2057],{"class":275,"line":377},[273,2058,2059],{},"1. Add `\"products\"`, `\"warehouses\"`, `\"stock_levels\"`, and\n",[273,2061,2062],{"class":275,"line":383},[273,2063,2064],{},"   `\"stock_movements\"` to the `RealtimeTable` union type.\n",[273,2066,2067],{"class":275,"line":388},[273,2068,2069],{},"2. In the `setup()` function, add `.on(\"postgres_changes\", ...)` handlers\n",[273,2071,2072],{"class":275,"line":394},[273,2073,2074],{},"   for each new table, filtered by `team_id=eq.${teamId}` where the table\n",[273,2076,2077],{"class":275,"line":399},[273,2078,2079],{},"   has a team_id column.\n",[273,2081,2082],{"class":275,"line":405},[273,2083,292],{"emptyLinePlaceholder":291},[273,2085,2086],{"class":275,"line":411},[273,2087,2088],{},"Integration — use `onTableDebounced` from `useRealtime()` inline in each\n",[273,2090,2091],{"class":275,"line":416},[273,2092,2093],{},"page. Do NOT create separate `useRealtimeX` composable files:\n",[273,2095,2096],{"class":275,"line":421},[273,2097,292],{"emptyLinePlaceholder":291},[273,2099,2100],{"class":275,"line":426},[273,2101,2102],{},"- In `app\u002Fpages\u002Fapp\u002Fproducts\u002Findex.vue`:\n",[273,2104,2105],{"class":275,"line":431},[273,2106,2107],{},"  `const { onTableDebounced } = useRealtime()`\n",[273,2109,2110],{"class":275,"line":437},[273,2111,2112],{},"  `onTableDebounced(\"products\", () => refreshProducts())`\n",[273,2114,2115],{"class":275,"line":442},[273,2116,2117],{},"- In `app\u002Fpages\u002Fapp\u002Finventory\u002Findex.vue`:\n",[273,2119,2120],{"class":275,"line":448},[273,2121,2107],{},[273,2123,2124],{"class":275,"line":454},[273,2125,2126],{},"  `onTableDebounced([\"stock_levels\", \"stock_movements\"], () => refreshInventory())`\n",[273,2128,2129],{"class":275,"line":460},[273,2130,2131],{},"- In the dashboard stats page:\n",[273,2133,2134],{"class":275,"line":465},[273,2135,2107],{},[273,2137,2138],{"class":275,"line":471},[273,2139,2140],{},"  `onTableDebounced([\"products\", \"stock_levels\", \"stock_movements\"], () => refreshStats())`\n",[258,2142,2144],{"id":2143},"what-you-built","What You Built",[198,2146,2147],{},"Starting from a template that handled auth, teams, roles, and permissions, you added:",[2149,2150,2151,2156,2161,2166,2171],"ol",{},[223,2152,2153,2155],{},[201,2154,817],{}," — a full catalog with SKUs, categories, pricing, and reorder points",[223,2157,2158,2160],{},[201,2159,233],{}," — locations for storing stock with capacity tracking",[223,2162,2163,2165],{},[201,2164,1377],{}," — inbound, outbound, transfer, and adjustment operations with audit trail",[223,2167,2168,2170],{},[201,2169,1757],{}," — real stats showing stock value, low-stock alerts, and recent activity",[223,2172,2173,252],{},[201,2174,251],{},[198,2176,2177,2178,2181],{},"Every feature follows the same patterns: permission-gated server routes, team-scoped data, Nuxt UI components, and the conventions defined in ",[270,2179,2180],{},"CLAUDE.md",".",[258,2183,2185],{"id":2184},"whats-next","What's Next",[220,2187,2188,2195],{},[223,2189,2190,2194],{},[201,2191,2192],{},[206,2193,59],{"href":60}," — let external services query stock levels and record movements programmatically",[223,2196,2197,2201,2202,2205],{},[201,2198,2199],{},[206,2200,143],{"href":144}," — the baked-in assistant is already wired to your new tables via ",[270,2203,2204],{},"tablePermissions",". Try it with \"What's our lowest stock item?\" or \"How many units of X did we ship this week?\"",[2207,2208,2209],"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":2211},[2212,2213],{"id":2143,"depth":282,"text":2144},{"id":2184,"depth":282,"text":2185},"Track products, warehouses, and stock movements with full audit history","md",null,{},{"icon":94},{"title":91,"description":2214},"6YoYsnxIiTM4NYHn_OAHvDXttPKcndcV8ywkxIg23Js",[2222,2224],{"title":86,"path":87,"stem":88,"description":2223,"icon":89,"children":-1},"Build a project board with boards, columns, cards, drag-and-drop, and checklists",{"title":96,"path":97,"stem":98,"description":2225,"icon":99,"children":-1},"Build a lightweight CRM with contacts, deals, pipeline stages, and activity tracking",1777092169440]