[{"data":1,"prerenderedAt":1980},["ShallowReactive",2],{"navigation":3,"\u002Fexamples\u002Fjob-management":189,"\u002Fexamples\u002Fjob-management-surround":1975},[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":81,"body":191,"description":1968,"extension":1969,"links":1970,"meta":1971,"navigation":1972,"path":82,"seo":1973,"stem":83,"__hash__":1974},"docs\u002F5.examples\u002F2.job-management.md",{"type":192,"value":193,"toc":1964},"minimark",[194,210,213,216,219,247,1898,1902,1905,1926,1933,1936,1940,1960],[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 job management app is the kind of tool a service business uses to track clients and work orders. Think plumbing, cleaning, landscaping, consulting, repair services. Any business that sends people out to do work and needs to keep track of who, what, where, and when.",[198,217,218],{},"By the end of this example you will have:",[220,221,222,229,235,241],"ul",{},[223,224,225,228],"li",{},[201,226,227],{},"Client management"," — full CRUD for the people and businesses you serve",[223,230,231,234],{},[201,232,233],{},"Job management"," — work orders with status tracking, assignments, priorities, pricing, and notes",[223,236,237,240],{},[201,238,239],{},"Dashboard"," — real stats showing active jobs, revenue, upcoming work",[223,242,243,246],{},[201,244,245],{},"Realtime"," — all changes sync instantly across connected browsers",[248,249,251,256,729,741,745,1010,1014,1522,1526,1530,1730,1733],"steps",{"level":250},"2",[252,253,255],"h2",{"id":254},"database-schema-and-permissions","Database Schema and Permissions",[257,258,263],"pre",{"className":259,"code":260,"language":261,"meta":262,"style":262},"language-txt shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","We are building a job management app on top of this template. Create the\ndatabase schema and add the permissions we need.\n\nDatabase (via Supabase MCP):\n\nCreate three tables:\n\n1. `clients` 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   address (text, nullable), notes (text, nullable), created_by (uuid,\n   references profiles(id), not null), created_at (timestamptz, default\n   now()), updated_at (timestamptz, default now()).\n\n2. `jobs` table — id (uuid, default gen_random_uuid(), primary key),\n   team_id (uuid, references teams(id) on delete cascade, not null),\n   client_id (uuid, references clients(id) on delete cascade, not null),\n   assigned_to (uuid, references profiles(id) on delete set null, nullable),\n   title (text, not null), description (text, nullable),\n   status (text, check status in ('scheduled', 'in_progress', 'completed',\n   'invoiced', 'cancelled'), default 'scheduled', not null),\n   priority (text, check priority in ('low', 'normal', 'high', 'urgent'),\n   default 'normal', not null),\n   scheduled_date (date, nullable), completed_date (date, nullable),\n   price (numeric(10,2), nullable),\n   created_by (uuid, references profiles(id), not null),\n   created_at (timestamptz, default now()), updated_at (timestamptz, default now()).\n\n3. `job_notes` table — id (uuid, default gen_random_uuid(), primary key),\n   job_id (uuid, references jobs(id) on delete cascade, not null),\n   user_id (uuid, references profiles(id), not null),\n   content (text, not null),\n   created_at (timestamptz, default now()).\n\nEnable RLS on all three tables. Create policies that use the existing\n`is_team_member()` function to scope access. For `clients` and `jobs`,\nthe policy should check that the row's `team_id` matches a team the user\nbelongs to. For `job_notes`, join through the `jobs` table to check\nteam membership.\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\"clients.view\": [\"owner\", \"admin\", \"member\"],\n\"clients.create\": [\"owner\", \"admin\", \"member\"],\n\"clients.update\": [\"owner\", \"admin\"],\n\"clients.delete\": [\"owner\", \"admin\"],\n\"jobs.view\": [\"owner\", \"admin\", \"member\"],\n\"jobs.create\": [\"owner\", \"admin\", \"member\"],\n\"jobs.update\": [\"owner\", \"admin\", \"member\"],\n\"jobs.delete\": [\"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","",[264,265,266,274,280,287,293,298,304,309,315,321,327,333,339,345,350,356,361,367,373,379,385,391,397,403,409,415,421,427,432,438,444,450,456,462,467,473,479,485,491,497,502,508,514,520,526,532,537,543,548,554,560,566,572,578,584,590,596,602,607,612,618,624,629,635,641,647,653,659,665,671,677,682,688,694,700,706,712,717,723],"code",{"__ignoreMap":262},[267,268,271],"span",{"class":269,"line":270},"line",1,[267,272,273],{},"We are building a job management app on top of this template. Create the\n",[267,275,277],{"class":269,"line":276},2,[267,278,279],{},"database schema and add the permissions we need.\n",[267,281,283],{"class":269,"line":282},3,[267,284,286],{"emptyLinePlaceholder":285},true,"\n",[267,288,290],{"class":269,"line":289},4,[267,291,292],{},"Database (via Supabase MCP):\n",[267,294,296],{"class":269,"line":295},5,[267,297,286],{"emptyLinePlaceholder":285},[267,299,301],{"class":269,"line":300},6,[267,302,303],{},"Create three tables:\n",[267,305,307],{"class":269,"line":306},7,[267,308,286],{"emptyLinePlaceholder":285},[267,310,312],{"class":269,"line":311},8,[267,313,314],{},"1. `clients` table — id (uuid, default gen_random_uuid(), primary key),\n",[267,316,318],{"class":269,"line":317},9,[267,319,320],{},"   team_id (uuid, references teams(id) on delete cascade, not null),\n",[267,322,324],{"class":269,"line":323},10,[267,325,326],{},"   name (text, not null), email (text, nullable), phone (text, nullable),\n",[267,328,330],{"class":269,"line":329},11,[267,331,332],{},"   address (text, nullable), notes (text, nullable), created_by (uuid,\n",[267,334,336],{"class":269,"line":335},12,[267,337,338],{},"   references profiles(id), not null), created_at (timestamptz, default\n",[267,340,342],{"class":269,"line":341},13,[267,343,344],{},"   now()), updated_at (timestamptz, default now()).\n",[267,346,348],{"class":269,"line":347},14,[267,349,286],{"emptyLinePlaceholder":285},[267,351,353],{"class":269,"line":352},15,[267,354,355],{},"2. `jobs` table — id (uuid, default gen_random_uuid(), primary key),\n",[267,357,359],{"class":269,"line":358},16,[267,360,320],{},[267,362,364],{"class":269,"line":363},17,[267,365,366],{},"   client_id (uuid, references clients(id) on delete cascade, not null),\n",[267,368,370],{"class":269,"line":369},18,[267,371,372],{},"   assigned_to (uuid, references profiles(id) on delete set null, nullable),\n",[267,374,376],{"class":269,"line":375},19,[267,377,378],{},"   title (text, not null), description (text, nullable),\n",[267,380,382],{"class":269,"line":381},20,[267,383,384],{},"   status (text, check status in ('scheduled', 'in_progress', 'completed',\n",[267,386,388],{"class":269,"line":387},21,[267,389,390],{},"   'invoiced', 'cancelled'), default 'scheduled', not null),\n",[267,392,394],{"class":269,"line":393},22,[267,395,396],{},"   priority (text, check priority in ('low', 'normal', 'high', 'urgent'),\n",[267,398,400],{"class":269,"line":399},23,[267,401,402],{},"   default 'normal', not null),\n",[267,404,406],{"class":269,"line":405},24,[267,407,408],{},"   scheduled_date (date, nullable), completed_date (date, nullable),\n",[267,410,412],{"class":269,"line":411},25,[267,413,414],{},"   price (numeric(10,2), nullable),\n",[267,416,418],{"class":269,"line":417},26,[267,419,420],{},"   created_by (uuid, references profiles(id), not null),\n",[267,422,424],{"class":269,"line":423},27,[267,425,426],{},"   created_at (timestamptz, default now()), updated_at (timestamptz, default now()).\n",[267,428,430],{"class":269,"line":429},28,[267,431,286],{"emptyLinePlaceholder":285},[267,433,435],{"class":269,"line":434},29,[267,436,437],{},"3. `job_notes` table — id (uuid, default gen_random_uuid(), primary key),\n",[267,439,441],{"class":269,"line":440},30,[267,442,443],{},"   job_id (uuid, references jobs(id) on delete cascade, not null),\n",[267,445,447],{"class":269,"line":446},31,[267,448,449],{},"   user_id (uuid, references profiles(id), not null),\n",[267,451,453],{"class":269,"line":452},32,[267,454,455],{},"   content (text, not null),\n",[267,457,459],{"class":269,"line":458},33,[267,460,461],{},"   created_at (timestamptz, default now()).\n",[267,463,465],{"class":269,"line":464},34,[267,466,286],{"emptyLinePlaceholder":285},[267,468,470],{"class":269,"line":469},35,[267,471,472],{},"Enable RLS on all three tables. Create policies that use the existing\n",[267,474,476],{"class":269,"line":475},36,[267,477,478],{},"`is_team_member()` function to scope access. For `clients` and `jobs`,\n",[267,480,482],{"class":269,"line":481},37,[267,483,484],{},"the policy should check that the row's `team_id` matches a team the user\n",[267,486,488],{"class":269,"line":487},38,[267,489,490],{},"belongs to. For `job_notes`, join through the `jobs` table to check\n",[267,492,494],{"class":269,"line":493},39,[267,495,496],{},"team membership.\n",[267,498,500],{"class":269,"line":499},40,[267,501,286],{"emptyLinePlaceholder":285},[267,503,505],{"class":269,"line":504},41,[267,506,507],{},"Only create a SELECT policy for team members — follow the\n",[267,509,511],{"class":269,"line":510},42,[267,512,513],{},"`announcements_team_member_read` pattern in\n",[267,515,517],{"class":269,"line":516},43,[267,518,519],{},"`supabase\u002Fmigrations\u002F00001_initial_schema.sql`. Writes go through\n",[267,521,523],{"class":269,"line":522},44,[267,524,525],{},"service-role server routes, so no insert\u002Fupdate\u002Fdelete RLS policies are\n",[267,527,529],{"class":269,"line":528},45,[267,530,531],{},"needed. Permission checks live in server routes via `authUser(event, \"key\")`.\n",[267,533,535],{"class":269,"line":534},46,[267,536,286],{"emptyLinePlaceholder":285},[267,538,540],{"class":269,"line":539},47,[267,541,542],{},"After creating the tables, add these permissions to `shared\u002Fpermissions.ts`:\n",[267,544,546],{"class":269,"line":545},48,[267,547,286],{"emptyLinePlaceholder":285},[267,549,551],{"class":269,"line":550},49,[267,552,553],{},"```\n",[267,555,557],{"class":269,"line":556},50,[267,558,559],{},"\"clients.view\": [\"owner\", \"admin\", \"member\"],\n",[267,561,563],{"class":269,"line":562},51,[267,564,565],{},"\"clients.create\": [\"owner\", \"admin\", \"member\"],\n",[267,567,569],{"class":269,"line":568},52,[267,570,571],{},"\"clients.update\": [\"owner\", \"admin\"],\n",[267,573,575],{"class":269,"line":574},53,[267,576,577],{},"\"clients.delete\": [\"owner\", \"admin\"],\n",[267,579,581],{"class":269,"line":580},54,[267,582,583],{},"\"jobs.view\": [\"owner\", \"admin\", \"member\"],\n",[267,585,587],{"class":269,"line":586},55,[267,588,589],{},"\"jobs.create\": [\"owner\", \"admin\", \"member\"],\n",[267,591,593],{"class":269,"line":592},56,[267,594,595],{},"\"jobs.update\": [\"owner\", \"admin\", \"member\"],\n",[267,597,599],{"class":269,"line":598},57,[267,600,601],{},"\"jobs.delete\": [\"owner\", \"admin\"],\n",[267,603,605],{"class":269,"line":604},58,[267,606,553],{},[267,608,610],{"class":269,"line":609},59,[267,611,286],{"emptyLinePlaceholder":285},[267,613,615],{"class":269,"line":614},60,[267,616,617],{},"Also wire the new tables into the baked-in AI chat and activity log per\n",[267,619,621],{"class":269,"line":620},61,[267,622,623],{},"CLAUDE.md conventions:\n",[267,625,627],{"class":269,"line":626},62,[267,628,286],{"emptyLinePlaceholder":285},[267,630,632],{"class":269,"line":631},63,[267,633,634],{},"- Add `COMMENT ON COLUMN` on every non-obvious column — one short technical\n",[267,636,638],{"class":269,"line":637},64,[267,639,640],{},"  sentence. Mention format or business rules the DB does not enforce.\n",[267,642,644],{"class":269,"line":643},65,[267,645,646],{},"- `select enable_activity_log('\u003Ctable>');` for each mutation-bearing table.\n",[267,648,650],{"class":269,"line":649},66,[267,651,652],{},"- Grant chat read access on each team-scoped table:\n",[267,654,656],{"class":269,"line":655},67,[267,657,658],{},"  ```\n",[267,660,662],{"class":269,"line":661},68,[267,663,664],{},"  grant select on \u003Ctable> to chat_reader;\n",[267,666,668],{"class":269,"line":667},69,[267,669,670],{},"  create policy \"\u003Ctable>_select_chat\" on \u003Ctable>\n",[267,672,674],{"class":269,"line":673},70,[267,675,676],{},"    for select to chat_reader using (team_id = current_chat_team());\n",[267,678,680],{"class":269,"line":679},71,[267,681,658],{},[267,683,685],{"class":269,"line":684},72,[267,686,687],{},"  Skip tables without a `team_id` column (scope them through a parent).\n",[267,689,691],{"class":269,"line":690},73,[267,692,693],{},"- Register each table in `tablePermissions` in `shared\u002Fpermissions.ts`\n",[267,695,697],{"class":269,"line":696},74,[267,698,699],{},"  using the permission keys above.\n",[267,701,703],{"class":269,"line":702},75,[267,704,705],{},"- Add filter entries in `app\u002Fcomponents\u002Factivity\u002FList.vue` (`tableItems`)\n",[267,707,709],{"class":269,"line":708},76,[267,710,711],{},"  for each new table.\n",[267,713,715],{"class":269,"line":714},77,[267,716,286],{"emptyLinePlaceholder":285},[267,718,720],{"class":269,"line":719},78,[267,721,722],{},"Regenerate the TypeScript types via Supabase MCP and save them to\n",[267,724,726],{"class":269,"line":725},79,[267,727,728],{},"`shared\u002Ftypes\u002Fdatabase.types.ts`.\n",[730,731,732],"tip",{},[198,733,734,737,738,740],{},[201,735,736],{},"Adding the public API later?"," If you plan to add the ",[206,739,59],{"href":60}," plugin later, its page will guide you through adding the required permissions. You don't need to add them now.",[252,742,744],{"id":743},"clients","Clients",[257,746,748],{"className":259,"code":747,"language":261,"meta":262,"style":262},"Build the client 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]\u002Fclients` — uses `authUser(event, \"clients.view\")`.\n  Returns all clients for the team, ordered by name asc. Include a count of\n  jobs per client by joining the jobs table.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fclients` — uses `authUser(event, \"clients.create\")`.\n  Reads { name, email, phone, address, notes } from the body. Validates that\n  name is required. Sets team_id from the auth context and created_by from\n  the authenticated user (user.sub).\n\n- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fclients\u002F[clientId]` — uses\n  `authUser(event, \"clients.update\")`. Updates whichever fields are provided\n  in the body. Validates that the client belongs to the team before updating.\n\n- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Fclients\u002F[clientId]` — uses\n  `authUser(event, \"clients.delete\")`. Validates that the client belongs to\n  the team before deleting.\n\nUI:\n\nCreate `app\u002Fpages\u002Fapp\u002Fclients\u002Findex.vue` — a client list page as a top-level\nroute (under `\u002Fapp`, sibling of other features), wrapped\nin a `UDashboardPanel`:\n\n- `UDashboardNavbar` in the header: title \"Clients\",\n  `UDashboardSidebarCollapse` on the left. On the right, a \"New Client\"\n  button wrapped in `CanAccess permission=\"clients.create\"`.\n\n- The body shows a table of clients with columns: name, email, phone,\n  address, and job count. Use `USkeleton` for loading state on initial load.\n  Show an empty state with an icon and message when there are no clients.\n\n- \"New Client\" button opens a `UModal` with a form: name (required), email,\n  phone, address, notes. Use Zod for validation. Show loading state on the\n  submit button. Show success toast on creation.\n\n- Clicking a client row opens a `USlideover` for editing. Show all fields\n  in a form. Include a delete button at the bottom wrapped in\n  `CanAccess permission=\"clients.delete\"` — clicking it shows a confirmation\n  modal before deleting. Show loading on the save button.\n\n- After creating, editing, or deleting a client, refresh the client list.\n\nSidebar navigation:\n\nAdd a \"Clients\" 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\nThe `items` ref is already a `computed\u003CNavigationMenuItem[]>` in this\ntemplate — push the new entry into that list. All roles see the Clients\nlink for now; use `can()` from `useUserRole()` if you need to gate later.\n",[264,749,750,755,759,764,768,773,778,783,787,792,797,802,807,811,816,821,826,830,835,840,845,849,854,858,863,868,873,877,882,887,892,896,901,906,911,915,920,925,930,934,939,944,949,954,958,963,967,972,976,981,986,991,995,1000,1005],{"__ignoreMap":262},[267,751,752],{"class":269,"line":270},[267,753,754],{},"Build the client management module — server routes and a full UI page.\n",[267,756,757],{"class":269,"line":276},[267,758,286],{"emptyLinePlaceholder":285},[267,760,761],{"class":269,"line":282},[267,762,763],{},"Server routes (all use `authUser` with the appropriate permission):\n",[267,765,766],{"class":269,"line":289},[267,767,286],{"emptyLinePlaceholder":285},[267,769,770],{"class":269,"line":295},[267,771,772],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fclients` — uses `authUser(event, \"clients.view\")`.\n",[267,774,775],{"class":269,"line":300},[267,776,777],{},"  Returns all clients for the team, ordered by name asc. Include a count of\n",[267,779,780],{"class":269,"line":306},[267,781,782],{},"  jobs per client by joining the jobs table.\n",[267,784,785],{"class":269,"line":311},[267,786,286],{"emptyLinePlaceholder":285},[267,788,789],{"class":269,"line":317},[267,790,791],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fclients` — uses `authUser(event, \"clients.create\")`.\n",[267,793,794],{"class":269,"line":323},[267,795,796],{},"  Reads { name, email, phone, address, notes } from the body. Validates that\n",[267,798,799],{"class":269,"line":329},[267,800,801],{},"  name is required. Sets team_id from the auth context and created_by from\n",[267,803,804],{"class":269,"line":335},[267,805,806],{},"  the authenticated user (user.sub).\n",[267,808,809],{"class":269,"line":341},[267,810,286],{"emptyLinePlaceholder":285},[267,812,813],{"class":269,"line":347},[267,814,815],{},"- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fclients\u002F[clientId]` — uses\n",[267,817,818],{"class":269,"line":352},[267,819,820],{},"  `authUser(event, \"clients.update\")`. Updates whichever fields are provided\n",[267,822,823],{"class":269,"line":358},[267,824,825],{},"  in the body. Validates that the client belongs to the team before updating.\n",[267,827,828],{"class":269,"line":363},[267,829,286],{"emptyLinePlaceholder":285},[267,831,832],{"class":269,"line":369},[267,833,834],{},"- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Fclients\u002F[clientId]` — uses\n",[267,836,837],{"class":269,"line":375},[267,838,839],{},"  `authUser(event, \"clients.delete\")`. Validates that the client belongs to\n",[267,841,842],{"class":269,"line":381},[267,843,844],{},"  the team before deleting.\n",[267,846,847],{"class":269,"line":387},[267,848,286],{"emptyLinePlaceholder":285},[267,850,851],{"class":269,"line":393},[267,852,853],{},"UI:\n",[267,855,856],{"class":269,"line":399},[267,857,286],{"emptyLinePlaceholder":285},[267,859,860],{"class":269,"line":405},[267,861,862],{},"Create `app\u002Fpages\u002Fapp\u002Fclients\u002Findex.vue` — a client list page as a top-level\n",[267,864,865],{"class":269,"line":411},[267,866,867],{},"route (under `\u002Fapp`, sibling of other features), wrapped\n",[267,869,870],{"class":269,"line":417},[267,871,872],{},"in a `UDashboardPanel`:\n",[267,874,875],{"class":269,"line":423},[267,876,286],{"emptyLinePlaceholder":285},[267,878,879],{"class":269,"line":429},[267,880,881],{},"- `UDashboardNavbar` in the header: title \"Clients\",\n",[267,883,884],{"class":269,"line":434},[267,885,886],{},"  `UDashboardSidebarCollapse` on the left. On the right, a \"New Client\"\n",[267,888,889],{"class":269,"line":440},[267,890,891],{},"  button wrapped in `CanAccess permission=\"clients.create\"`.\n",[267,893,894],{"class":269,"line":446},[267,895,286],{"emptyLinePlaceholder":285},[267,897,898],{"class":269,"line":452},[267,899,900],{},"- The body shows a table of clients with columns: name, email, phone,\n",[267,902,903],{"class":269,"line":458},[267,904,905],{},"  address, and job count. Use `USkeleton` for loading state on initial load.\n",[267,907,908],{"class":269,"line":464},[267,909,910],{},"  Show an empty state with an icon and message when there are no clients.\n",[267,912,913],{"class":269,"line":469},[267,914,286],{"emptyLinePlaceholder":285},[267,916,917],{"class":269,"line":475},[267,918,919],{},"- \"New Client\" button opens a `UModal` with a form: name (required), email,\n",[267,921,922],{"class":269,"line":481},[267,923,924],{},"  phone, address, notes. Use Zod for validation. Show loading state on the\n",[267,926,927],{"class":269,"line":487},[267,928,929],{},"  submit button. Show success toast on creation.\n",[267,931,932],{"class":269,"line":493},[267,933,286],{"emptyLinePlaceholder":285},[267,935,936],{"class":269,"line":499},[267,937,938],{},"- Clicking a client row opens a `USlideover` for editing. Show all fields\n",[267,940,941],{"class":269,"line":504},[267,942,943],{},"  in a form. Include a delete button at the bottom wrapped in\n",[267,945,946],{"class":269,"line":510},[267,947,948],{},"  `CanAccess permission=\"clients.delete\"` — clicking it shows a confirmation\n",[267,950,951],{"class":269,"line":516},[267,952,953],{},"  modal before deleting. Show loading on the save button.\n",[267,955,956],{"class":269,"line":522},[267,957,286],{"emptyLinePlaceholder":285},[267,959,960],{"class":269,"line":528},[267,961,962],{},"- After creating, editing, or deleting a client, refresh the client list.\n",[267,964,965],{"class":269,"line":534},[267,966,286],{"emptyLinePlaceholder":285},[267,968,969],{"class":269,"line":539},[267,970,971],{},"Sidebar navigation:\n",[267,973,974],{"class":269,"line":545},[267,975,286],{"emptyLinePlaceholder":285},[267,977,978],{"class":269,"line":550},[267,979,980],{},"Add a \"Clients\" link to the top navigation group in\n",[267,982,983],{"class":269,"line":556},[267,984,985],{},"`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Dashboard\" and\n",[267,987,988],{"class":269,"line":562},[267,989,990],{},"\"Settings\". Use the icon `i-solar-users-group-two-rounded-bold-duotone`.\n",[267,992,993],{"class":269,"line":568},[267,994,286],{"emptyLinePlaceholder":285},[267,996,997],{"class":269,"line":574},[267,998,999],{},"The `items` ref is already a `computed\u003CNavigationMenuItem[]>` in this\n",[267,1001,1002],{"class":269,"line":580},[267,1003,1004],{},"template — push the new entry into that list. All roles see the Clients\n",[267,1006,1007],{"class":269,"line":586},[267,1008,1009],{},"link for now; use `can()` from `useUserRole()` if you need to gate later.\n",[252,1011,1013],{"id":1012},"jobs","Jobs",[257,1015,1017],{"className":259,"code":1016,"language":261,"meta":262,"style":262},"Build the job management module — this is the main feature of the app.\n\nServer routes (all use `authUser` with the appropriate permission):\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs` — uses `authUser(event, \"jobs.view\")`.\n  Returns all jobs for the team with the client name and assignee name\n  joined in. Order by scheduled_date desc, with nulls last. Support\n  optional query params: `?status=` to filter by status, `?assigned_to=`\n  to filter by assignee UUID.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs` — uses `authUser(event, \"jobs.create\")`.\n  Reads { title, client_id, assigned_to, description, status, priority,\n  scheduled_date, price } from the body. Validates that title and client_id\n  are required. Sets team_id from auth context and created_by from the\n  authenticated user (user.sub).\n\n- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs\u002F[jobId]` — uses\n  `authUser(event, \"jobs.update\")`. Updates whichever fields are provided.\n  If status is being changed to \"completed\", automatically set\n  completed_date to today. Validates that the job belongs to the team.\n\n- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs\u002F[jobId]` — uses\n  `authUser(event, \"jobs.delete\")`. Validates that the job belongs to\n  the team before deleting.\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs\u002F[jobId]\u002Fnotes` — uses\n  `authUser(event, \"jobs.view\")`. Returns all notes for the job with\n  the author's name joined in, ordered by created_at asc.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs\u002F[jobId]\u002Fnotes` — uses\n  `authUser(event, \"jobs.view\")`. Any team member can add notes. Reads\n  { content } from the body. Sets user_id from the authenticated user.\n\nUI:\n\nCreate `app\u002Fpages\u002Fapp\u002Fjobs\u002Findex.vue` — the main jobs page as a top-level\nroute (under `\u002Fapp`, sibling of other features), wrapped\nin a `UDashboardPanel`:\n\n- `UDashboardNavbar` in the header: title \"Jobs\",\n  `UDashboardSidebarCollapse` on the left. On the right, a \"New Job\"\n  button wrapped in `CanAccess permission=\"jobs.create\"`.\n\n- `UDashboardToolbar` below the navbar: on the left, show status counts\n  (e.g. \"3 Scheduled, 5 In Progress, 12 Completed\"). On the right, status\n  filter tabs using a `USelect` or `UTabs` with `size=\"xs\"`:\n  All, Scheduled, In Progress, Completed, Invoiced, Cancelled.\n\n- Job list in the body as a table with columns: title, client name,\n  assignee name (or \"Unassigned\"), status badge (color-coded — use\n  different colors for each status), priority badge, scheduled date\n  (formatted nicely), and price (formatted as currency).\n\n- Status badges should use these colors: scheduled = \"info\",\n  in_progress = \"warning\", completed = \"success\", invoiced = \"neutral\",\n  cancelled = \"error\".\n\n- Priority badges: low = \"neutral\", normal = \"info\", high = \"warning\",\n  urgent = \"error\".\n\n- Each row has an inline status dropdown that lets you change the status\n  directly from the table. Use `UDropdownMenu` with the status options.\n  Show per-row loading state while the status is being updated.\n\n- \"New Job\" button opens a `UModal` with a form: title (required),\n  client (required — select from existing clients fetched on mount),\n  assignee (optional — select from team members fetched on mount),\n  status, priority, scheduled date, price, description. Use Zod for\n  validation. Show loading on submit.\n\n- For the client select: fetch clients via `GET \u002Fapi\u002Fteams\u002F{teamId}\u002Fclients`\n  on mount and show in a `USelect` with the client name as the label.\n\n- For the assignee select: fetch team members via\n  `GET \u002Fapi\u002Fteams\u002F{teamId}\u002Fmembers` on mount and show in a `USelect`\n  with the member name as the label. Use `placeholder=\"Unassigned\"` —\n  do NOT use an empty-string value for \"Unassigned\" in a `USelect`, this\n  causes Reka UI's Select component to crash.\n\n- For date fields (scheduled date), use a `UPopover` with a `UCalendar`\n  inside — not a native date input. Import `CalendarDate` from\n  `@internationalized\u002Fdate` (install the package if needed). Use\n  `useTemplateRef` for the popover reference. Convert the `CalendarDate`\n  to an ISO string on submit.\n\n- Clicking a job row opens a `USlideover` for the job detail. Show all\n  fields in an editable form at the top. Below the form, show a \"Notes\"\n  section: a scrollable list of notes (author name, content, timestamp)\n  with a text input at the bottom to add a new note. Notes are fetched\n  when the slideover opens. The note input should have a send button\n  that submits on click or Enter.\n\n- Include a delete button at the bottom of the slideover wrapped in\n  `CanAccess permission=\"jobs.delete\"` with a confirmation modal.\n\n- After creating, editing, or deleting a job, refresh the job list.\n\nSidebar navigation:\n\nAdd a \"Jobs\" link to the top navigation group in\n`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Clients\" and\n\"Settings\". Use the icon `i-solar-clipboard-list-bold-duotone`.\n",[264,1018,1019,1024,1028,1032,1036,1041,1046,1051,1056,1061,1065,1070,1075,1080,1085,1090,1094,1099,1104,1109,1114,1118,1123,1128,1132,1136,1141,1146,1151,1155,1160,1165,1170,1174,1178,1182,1187,1191,1195,1199,1204,1209,1214,1218,1223,1228,1233,1238,1242,1247,1252,1257,1262,1266,1271,1276,1281,1285,1290,1295,1299,1304,1309,1314,1318,1323,1328,1333,1338,1343,1347,1352,1357,1361,1366,1371,1376,1381,1386,1390,1396,1402,1408,1414,1420,1425,1431,1437,1443,1449,1455,1461,1466,1472,1478,1483,1489,1494,1499,1504,1510,1516],{"__ignoreMap":262},[267,1020,1021],{"class":269,"line":270},[267,1022,1023],{},"Build the job management module — this is the main feature of the app.\n",[267,1025,1026],{"class":269,"line":276},[267,1027,286],{"emptyLinePlaceholder":285},[267,1029,1030],{"class":269,"line":282},[267,1031,763],{},[267,1033,1034],{"class":269,"line":289},[267,1035,286],{"emptyLinePlaceholder":285},[267,1037,1038],{"class":269,"line":295},[267,1039,1040],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs` — uses `authUser(event, \"jobs.view\")`.\n",[267,1042,1043],{"class":269,"line":300},[267,1044,1045],{},"  Returns all jobs for the team with the client name and assignee name\n",[267,1047,1048],{"class":269,"line":306},[267,1049,1050],{},"  joined in. Order by scheduled_date desc, with nulls last. Support\n",[267,1052,1053],{"class":269,"line":311},[267,1054,1055],{},"  optional query params: `?status=` to filter by status, `?assigned_to=`\n",[267,1057,1058],{"class":269,"line":317},[267,1059,1060],{},"  to filter by assignee UUID.\n",[267,1062,1063],{"class":269,"line":323},[267,1064,286],{"emptyLinePlaceholder":285},[267,1066,1067],{"class":269,"line":329},[267,1068,1069],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs` — uses `authUser(event, \"jobs.create\")`.\n",[267,1071,1072],{"class":269,"line":335},[267,1073,1074],{},"  Reads { title, client_id, assigned_to, description, status, priority,\n",[267,1076,1077],{"class":269,"line":341},[267,1078,1079],{},"  scheduled_date, price } from the body. Validates that title and client_id\n",[267,1081,1082],{"class":269,"line":347},[267,1083,1084],{},"  are required. Sets team_id from auth context and created_by from the\n",[267,1086,1087],{"class":269,"line":352},[267,1088,1089],{},"  authenticated user (user.sub).\n",[267,1091,1092],{"class":269,"line":358},[267,1093,286],{"emptyLinePlaceholder":285},[267,1095,1096],{"class":269,"line":363},[267,1097,1098],{},"- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs\u002F[jobId]` — uses\n",[267,1100,1101],{"class":269,"line":369},[267,1102,1103],{},"  `authUser(event, \"jobs.update\")`. Updates whichever fields are provided.\n",[267,1105,1106],{"class":269,"line":375},[267,1107,1108],{},"  If status is being changed to \"completed\", automatically set\n",[267,1110,1111],{"class":269,"line":381},[267,1112,1113],{},"  completed_date to today. Validates that the job belongs to the team.\n",[267,1115,1116],{"class":269,"line":387},[267,1117,286],{"emptyLinePlaceholder":285},[267,1119,1120],{"class":269,"line":393},[267,1121,1122],{},"- `DELETE \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs\u002F[jobId]` — uses\n",[267,1124,1125],{"class":269,"line":399},[267,1126,1127],{},"  `authUser(event, \"jobs.delete\")`. Validates that the job belongs to\n",[267,1129,1130],{"class":269,"line":405},[267,1131,844],{},[267,1133,1134],{"class":269,"line":411},[267,1135,286],{"emptyLinePlaceholder":285},[267,1137,1138],{"class":269,"line":417},[267,1139,1140],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs\u002F[jobId]\u002Fnotes` — uses\n",[267,1142,1143],{"class":269,"line":423},[267,1144,1145],{},"  `authUser(event, \"jobs.view\")`. Returns all notes for the job with\n",[267,1147,1148],{"class":269,"line":429},[267,1149,1150],{},"  the author's name joined in, ordered by created_at asc.\n",[267,1152,1153],{"class":269,"line":434},[267,1154,286],{"emptyLinePlaceholder":285},[267,1156,1157],{"class":269,"line":440},[267,1158,1159],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fjobs\u002F[jobId]\u002Fnotes` — uses\n",[267,1161,1162],{"class":269,"line":446},[267,1163,1164],{},"  `authUser(event, \"jobs.view\")`. Any team member can add notes. Reads\n",[267,1166,1167],{"class":269,"line":452},[267,1168,1169],{},"  { content } from the body. Sets user_id from the authenticated user.\n",[267,1171,1172],{"class":269,"line":458},[267,1173,286],{"emptyLinePlaceholder":285},[267,1175,1176],{"class":269,"line":464},[267,1177,853],{},[267,1179,1180],{"class":269,"line":469},[267,1181,286],{"emptyLinePlaceholder":285},[267,1183,1184],{"class":269,"line":475},[267,1185,1186],{},"Create `app\u002Fpages\u002Fapp\u002Fjobs\u002Findex.vue` — the main jobs page as a top-level\n",[267,1188,1189],{"class":269,"line":481},[267,1190,867],{},[267,1192,1193],{"class":269,"line":487},[267,1194,872],{},[267,1196,1197],{"class":269,"line":493},[267,1198,286],{"emptyLinePlaceholder":285},[267,1200,1201],{"class":269,"line":499},[267,1202,1203],{},"- `UDashboardNavbar` in the header: title \"Jobs\",\n",[267,1205,1206],{"class":269,"line":504},[267,1207,1208],{},"  `UDashboardSidebarCollapse` on the left. On the right, a \"New Job\"\n",[267,1210,1211],{"class":269,"line":510},[267,1212,1213],{},"  button wrapped in `CanAccess permission=\"jobs.create\"`.\n",[267,1215,1216],{"class":269,"line":516},[267,1217,286],{"emptyLinePlaceholder":285},[267,1219,1220],{"class":269,"line":522},[267,1221,1222],{},"- `UDashboardToolbar` below the navbar: on the left, show status counts\n",[267,1224,1225],{"class":269,"line":528},[267,1226,1227],{},"  (e.g. \"3 Scheduled, 5 In Progress, 12 Completed\"). On the right, status\n",[267,1229,1230],{"class":269,"line":534},[267,1231,1232],{},"  filter tabs using a `USelect` or `UTabs` with `size=\"xs\"`:\n",[267,1234,1235],{"class":269,"line":539},[267,1236,1237],{},"  All, Scheduled, In Progress, Completed, Invoiced, Cancelled.\n",[267,1239,1240],{"class":269,"line":545},[267,1241,286],{"emptyLinePlaceholder":285},[267,1243,1244],{"class":269,"line":550},[267,1245,1246],{},"- Job list in the body as a table with columns: title, client name,\n",[267,1248,1249],{"class":269,"line":556},[267,1250,1251],{},"  assignee name (or \"Unassigned\"), status badge (color-coded — use\n",[267,1253,1254],{"class":269,"line":562},[267,1255,1256],{},"  different colors for each status), priority badge, scheduled date\n",[267,1258,1259],{"class":269,"line":568},[267,1260,1261],{},"  (formatted nicely), and price (formatted as currency).\n",[267,1263,1264],{"class":269,"line":574},[267,1265,286],{"emptyLinePlaceholder":285},[267,1267,1268],{"class":269,"line":580},[267,1269,1270],{},"- Status badges should use these colors: scheduled = \"info\",\n",[267,1272,1273],{"class":269,"line":586},[267,1274,1275],{},"  in_progress = \"warning\", completed = \"success\", invoiced = \"neutral\",\n",[267,1277,1278],{"class":269,"line":592},[267,1279,1280],{},"  cancelled = \"error\".\n",[267,1282,1283],{"class":269,"line":598},[267,1284,286],{"emptyLinePlaceholder":285},[267,1286,1287],{"class":269,"line":604},[267,1288,1289],{},"- Priority badges: low = \"neutral\", normal = \"info\", high = \"warning\",\n",[267,1291,1292],{"class":269,"line":609},[267,1293,1294],{},"  urgent = \"error\".\n",[267,1296,1297],{"class":269,"line":614},[267,1298,286],{"emptyLinePlaceholder":285},[267,1300,1301],{"class":269,"line":620},[267,1302,1303],{},"- Each row has an inline status dropdown that lets you change the status\n",[267,1305,1306],{"class":269,"line":626},[267,1307,1308],{},"  directly from the table. Use `UDropdownMenu` with the status options.\n",[267,1310,1311],{"class":269,"line":631},[267,1312,1313],{},"  Show per-row loading state while the status is being updated.\n",[267,1315,1316],{"class":269,"line":637},[267,1317,286],{"emptyLinePlaceholder":285},[267,1319,1320],{"class":269,"line":643},[267,1321,1322],{},"- \"New Job\" button opens a `UModal` with a form: title (required),\n",[267,1324,1325],{"class":269,"line":649},[267,1326,1327],{},"  client (required — select from existing clients fetched on mount),\n",[267,1329,1330],{"class":269,"line":655},[267,1331,1332],{},"  assignee (optional — select from team members fetched on mount),\n",[267,1334,1335],{"class":269,"line":661},[267,1336,1337],{},"  status, priority, scheduled date, price, description. Use Zod for\n",[267,1339,1340],{"class":269,"line":667},[267,1341,1342],{},"  validation. Show loading on submit.\n",[267,1344,1345],{"class":269,"line":673},[267,1346,286],{"emptyLinePlaceholder":285},[267,1348,1349],{"class":269,"line":679},[267,1350,1351],{},"- For the client select: fetch clients via `GET \u002Fapi\u002Fteams\u002F{teamId}\u002Fclients`\n",[267,1353,1354],{"class":269,"line":684},[267,1355,1356],{},"  on mount and show in a `USelect` with the client name as the label.\n",[267,1358,1359],{"class":269,"line":690},[267,1360,286],{"emptyLinePlaceholder":285},[267,1362,1363],{"class":269,"line":696},[267,1364,1365],{},"- For the assignee select: fetch team members via\n",[267,1367,1368],{"class":269,"line":702},[267,1369,1370],{},"  `GET \u002Fapi\u002Fteams\u002F{teamId}\u002Fmembers` on mount and show in a `USelect`\n",[267,1372,1373],{"class":269,"line":708},[267,1374,1375],{},"  with the member name as the label. Use `placeholder=\"Unassigned\"` —\n",[267,1377,1378],{"class":269,"line":714},[267,1379,1380],{},"  do NOT use an empty-string value for \"Unassigned\" in a `USelect`, this\n",[267,1382,1383],{"class":269,"line":719},[267,1384,1385],{},"  causes Reka UI's Select component to crash.\n",[267,1387,1388],{"class":269,"line":725},[267,1389,286],{"emptyLinePlaceholder":285},[267,1391,1393],{"class":269,"line":1392},80,[267,1394,1395],{},"- For date fields (scheduled date), use a `UPopover` with a `UCalendar`\n",[267,1397,1399],{"class":269,"line":1398},81,[267,1400,1401],{},"  inside — not a native date input. Import `CalendarDate` from\n",[267,1403,1405],{"class":269,"line":1404},82,[267,1406,1407],{},"  `@internationalized\u002Fdate` (install the package if needed). Use\n",[267,1409,1411],{"class":269,"line":1410},83,[267,1412,1413],{},"  `useTemplateRef` for the popover reference. Convert the `CalendarDate`\n",[267,1415,1417],{"class":269,"line":1416},84,[267,1418,1419],{},"  to an ISO string on submit.\n",[267,1421,1423],{"class":269,"line":1422},85,[267,1424,286],{"emptyLinePlaceholder":285},[267,1426,1428],{"class":269,"line":1427},86,[267,1429,1430],{},"- Clicking a job row opens a `USlideover` for the job detail. Show all\n",[267,1432,1434],{"class":269,"line":1433},87,[267,1435,1436],{},"  fields in an editable form at the top. Below the form, show a \"Notes\"\n",[267,1438,1440],{"class":269,"line":1439},88,[267,1441,1442],{},"  section: a scrollable list of notes (author name, content, timestamp)\n",[267,1444,1446],{"class":269,"line":1445},89,[267,1447,1448],{},"  with a text input at the bottom to add a new note. Notes are fetched\n",[267,1450,1452],{"class":269,"line":1451},90,[267,1453,1454],{},"  when the slideover opens. The note input should have a send button\n",[267,1456,1458],{"class":269,"line":1457},91,[267,1459,1460],{},"  that submits on click or Enter.\n",[267,1462,1464],{"class":269,"line":1463},92,[267,1465,286],{"emptyLinePlaceholder":285},[267,1467,1469],{"class":269,"line":1468},93,[267,1470,1471],{},"- Include a delete button at the bottom of the slideover wrapped in\n",[267,1473,1475],{"class":269,"line":1474},94,[267,1476,1477],{},"  `CanAccess permission=\"jobs.delete\"` with a confirmation modal.\n",[267,1479,1481],{"class":269,"line":1480},95,[267,1482,286],{"emptyLinePlaceholder":285},[267,1484,1486],{"class":269,"line":1485},96,[267,1487,1488],{},"- After creating, editing, or deleting a job, refresh the job list.\n",[267,1490,1492],{"class":269,"line":1491},97,[267,1493,286],{"emptyLinePlaceholder":285},[267,1495,1497],{"class":269,"line":1496},98,[267,1498,971],{},[267,1500,1502],{"class":269,"line":1501},99,[267,1503,286],{"emptyLinePlaceholder":285},[267,1505,1507],{"class":269,"line":1506},100,[267,1508,1509],{},"Add a \"Jobs\" link to the top navigation group in\n",[267,1511,1513],{"class":269,"line":1512},101,[267,1514,1515],{},"`app\u002Fcomponents\u002Flayout\u002Fsidebar\u002FLinks.vue`, between \"Clients\" and\n",[267,1517,1519],{"class":269,"line":1518},102,[267,1520,1521],{},"\"Settings\". Use the icon `i-solar-clipboard-list-bold-duotone`.\n",[252,1523,1525],{"id":1524},"dashboard-and-realtime","Dashboard and Realtime",[1527,1528,239],"h4",{"id":1529},"dashboard",[257,1531,1533],{"className":259,"code":1532,"language":261,"meta":262,"style":262},"Replace the placeholder dashboard with real 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- `client_count` — total number of clients for the team\n- `active_jobs` — count of jobs with status \"scheduled\" or \"in_progress\"\n- `completed_this_month` — count of jobs completed in the current calendar month\n- `revenue_this_month` — sum of prices for jobs with status \"completed\" or\n  \"invoiced\" where completed_date is in the current calendar month. Return 0\n  if no matching jobs.\n- `upcoming_jobs` — the next 5 jobs with status \"scheduled\", ordered by\n  scheduled_date asc. Include client name and assignee name joined in.\n- `recent_jobs` — the last 5 jobs with status \"completed\" or \"invoiced\",\n  ordered by completed_date desc. Include client name joined in.\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 Clients\" (client_count),\n  \"Active Jobs\" (active_jobs), \"Completed This Month\" (completed_this_month),\n  \"Revenue This Month\" (revenue_this_month formatted as currency).\n  Use `USkeleton` placeholders while loading.\n\n- Below the stats, a two-column layout:\n  - Left column: \"Upcoming Jobs\" — a list of the next 5 scheduled jobs.\n    Each item shows the job title, client name, scheduled date, and\n    assignee avatar or name. Clicking a job navigates to the jobs page.\n    Show an empty state if no upcoming jobs.\n  - Right column: \"Recently Completed\" — a list of the last 5\n    completed\u002Finvoiced jobs. Each item shows the job title, client name,\n    completed date, and price. 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",[264,1534,1535,1540,1544,1549,1553,1558,1563,1567,1572,1577,1582,1587,1592,1597,1602,1607,1612,1617,1621,1626,1630,1634,1638,1643,1647,1652,1657,1662,1667,1672,1676,1681,1686,1691,1696,1701,1706,1711,1716,1720,1725],{"__ignoreMap":262},[267,1536,1537],{"class":269,"line":270},[267,1538,1539],{},"Replace the placeholder dashboard with real data and stats.\n",[267,1541,1542],{"class":269,"line":276},[267,1543,286],{"emptyLinePlaceholder":285},[267,1545,1546],{"class":269,"line":282},[267,1547,1548],{},"Server route:\n",[267,1550,1551],{"class":269,"line":289},[267,1552,286],{"emptyLinePlaceholder":285},[267,1554,1555],{"class":269,"line":295},[267,1556,1557],{},"Create `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fstats` — uses `authUser(event, \"team.view\")`.\n",[267,1559,1560],{"class":269,"line":300},[267,1561,1562],{},"Returns a JSON object with:\n",[267,1564,1565],{"class":269,"line":306},[267,1566,286],{"emptyLinePlaceholder":285},[267,1568,1569],{"class":269,"line":311},[267,1570,1571],{},"- `client_count` — total number of clients for the team\n",[267,1573,1574],{"class":269,"line":317},[267,1575,1576],{},"- `active_jobs` — count of jobs with status \"scheduled\" or \"in_progress\"\n",[267,1578,1579],{"class":269,"line":323},[267,1580,1581],{},"- `completed_this_month` — count of jobs completed in the current calendar month\n",[267,1583,1584],{"class":269,"line":329},[267,1585,1586],{},"- `revenue_this_month` — sum of prices for jobs with status \"completed\" or\n",[267,1588,1589],{"class":269,"line":335},[267,1590,1591],{},"  \"invoiced\" where completed_date is in the current calendar month. Return 0\n",[267,1593,1594],{"class":269,"line":341},[267,1595,1596],{},"  if no matching jobs.\n",[267,1598,1599],{"class":269,"line":347},[267,1600,1601],{},"- `upcoming_jobs` — the next 5 jobs with status \"scheduled\", ordered by\n",[267,1603,1604],{"class":269,"line":352},[267,1605,1606],{},"  scheduled_date asc. Include client name and assignee name joined in.\n",[267,1608,1609],{"class":269,"line":358},[267,1610,1611],{},"- `recent_jobs` — the last 5 jobs with status \"completed\" or \"invoiced\",\n",[267,1613,1614],{"class":269,"line":363},[267,1615,1616],{},"  ordered by completed_date desc. Include client name joined in.\n",[267,1618,1619],{"class":269,"line":369},[267,1620,286],{"emptyLinePlaceholder":285},[267,1622,1623],{"class":269,"line":375},[267,1624,1625],{},"All queries are scoped to the team's team_id.\n",[267,1627,1628],{"class":269,"line":381},[267,1629,286],{"emptyLinePlaceholder":285},[267,1631,1632],{"class":269,"line":387},[267,1633,853],{},[267,1635,1636],{"class":269,"line":393},[267,1637,286],{"emptyLinePlaceholder":285},[267,1639,1640],{"class":269,"line":399},[267,1641,1642],{},"Update `app\u002Fpages\u002Fapp\u002Findex.vue` to show:\n",[267,1644,1645],{"class":269,"line":405},[267,1646,286],{"emptyLinePlaceholder":285},[267,1648,1649],{"class":269,"line":411},[267,1650,1651],{},"- A row of four stat cards at the top using a grid layout. Each card shows\n",[267,1653,1654],{"class":269,"line":417},[267,1655,1656],{},"  an icon, a label, and the value. Cards: \"Total Clients\" (client_count),\n",[267,1658,1659],{"class":269,"line":423},[267,1660,1661],{},"  \"Active Jobs\" (active_jobs), \"Completed This Month\" (completed_this_month),\n",[267,1663,1664],{"class":269,"line":429},[267,1665,1666],{},"  \"Revenue This Month\" (revenue_this_month formatted as currency).\n",[267,1668,1669],{"class":269,"line":434},[267,1670,1671],{},"  Use `USkeleton` placeholders while loading.\n",[267,1673,1674],{"class":269,"line":440},[267,1675,286],{"emptyLinePlaceholder":285},[267,1677,1678],{"class":269,"line":446},[267,1679,1680],{},"- Below the stats, a two-column layout:\n",[267,1682,1683],{"class":269,"line":452},[267,1684,1685],{},"  - Left column: \"Upcoming Jobs\" — a list of the next 5 scheduled jobs.\n",[267,1687,1688],{"class":269,"line":458},[267,1689,1690],{},"    Each item shows the job title, client name, scheduled date, and\n",[267,1692,1693],{"class":269,"line":464},[267,1694,1695],{},"    assignee avatar or name. Clicking a job navigates to the jobs page.\n",[267,1697,1698],{"class":269,"line":469},[267,1699,1700],{},"    Show an empty state if no upcoming jobs.\n",[267,1702,1703],{"class":269,"line":475},[267,1704,1705],{},"  - Right column: \"Recently Completed\" — a list of the last 5\n",[267,1707,1708],{"class":269,"line":481},[267,1709,1710],{},"    completed\u002Finvoiced jobs. Each item shows the job title, client name,\n",[267,1712,1713],{"class":269,"line":487},[267,1714,1715],{},"    completed date, and price. Show an empty state if none.\n",[267,1717,1718],{"class":269,"line":493},[267,1719,286],{"emptyLinePlaceholder":285},[267,1721,1722],{"class":269,"line":499},[267,1723,1724],{},"Remove any placeholder\u002Fscaffolding content that was in the dashboard before.\n",[267,1726,1727],{"class":269,"line":504},[267,1728,1729],{},"Keep the `UDashboardPanel` wrapper with navbar and sidebar collapse.\n",[1527,1731,245],{"id":1732},"realtime",[257,1734,1736],{"className":259,"code":1735,"language":261,"meta":262,"style":262},"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 clients, jobs, job_notes;\nALTER TABLE clients REPLICA IDENTITY FULL;\nALTER TABLE jobs REPLICA IDENTITY FULL;\nALTER TABLE job_notes REPLICA IDENTITY FULL;\n```\n\nUpdate `app\u002Fcomposables\u002FuseRealtime.ts`:\n\n1. Add `\"clients\"`, `\"jobs\"`, and `\"job_notes\"` to the `RealtimeTable`\n   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\u002Fclients\u002Findex.vue`:\n  `const { onTableDebounced } = useRealtime()`\n  `onTableDebounced(\"clients\", () => refreshClients())`\n- In `app\u002Fpages\u002Fapp\u002Fjobs\u002Findex.vue`:\n  `const { onTableDebounced } = useRealtime()`\n  `onTableDebounced([\"jobs\", \"job_notes\"], () => refreshJobs())`\n- In the dashboard stats page:\n  `const { onTableDebounced } = useRealtime()`\n  `onTableDebounced([\"clients\", \"jobs\"], () => refreshStats())`\n",[264,1737,1738,1743,1748,1752,1757,1761,1766,1770,1775,1780,1785,1790,1795,1799,1803,1808,1812,1817,1822,1827,1832,1837,1841,1846,1851,1855,1860,1865,1870,1875,1879,1884,1889,1893],{"__ignoreMap":262},[267,1739,1740],{"class":269,"line":270},[267,1741,1742],{},"Add Supabase Realtime sync for the new tables so changes appear instantly\n",[267,1744,1745],{"class":269,"line":276},[267,1746,1747],{},"across browser sessions.\n",[267,1749,1750],{"class":269,"line":282},[267,1751,286],{"emptyLinePlaceholder":285},[267,1753,1754],{"class":269,"line":289},[267,1755,1756],{},"Database migration (via Supabase MCP):\n",[267,1758,1759],{"class":269,"line":295},[267,1760,286],{"emptyLinePlaceholder":285},[267,1762,1763],{"class":269,"line":300},[267,1764,1765],{},"Enable realtime publication and full replica identity for the new tables:\n",[267,1767,1768],{"class":269,"line":306},[267,1769,286],{"emptyLinePlaceholder":285},[267,1771,1772],{"class":269,"line":311},[267,1773,1774],{},"```sql\n",[267,1776,1777],{"class":269,"line":317},[267,1778,1779],{},"ALTER PUBLICATION supabase_realtime ADD TABLE clients, jobs, job_notes;\n",[267,1781,1782],{"class":269,"line":323},[267,1783,1784],{},"ALTER TABLE clients REPLICA IDENTITY FULL;\n",[267,1786,1787],{"class":269,"line":329},[267,1788,1789],{},"ALTER TABLE jobs REPLICA IDENTITY FULL;\n",[267,1791,1792],{"class":269,"line":335},[267,1793,1794],{},"ALTER TABLE job_notes REPLICA IDENTITY FULL;\n",[267,1796,1797],{"class":269,"line":341},[267,1798,553],{},[267,1800,1801],{"class":269,"line":347},[267,1802,286],{"emptyLinePlaceholder":285},[267,1804,1805],{"class":269,"line":352},[267,1806,1807],{},"Update `app\u002Fcomposables\u002FuseRealtime.ts`:\n",[267,1809,1810],{"class":269,"line":358},[267,1811,286],{"emptyLinePlaceholder":285},[267,1813,1814],{"class":269,"line":363},[267,1815,1816],{},"1. Add `\"clients\"`, `\"jobs\"`, and `\"job_notes\"` to the `RealtimeTable`\n",[267,1818,1819],{"class":269,"line":369},[267,1820,1821],{},"   union type.\n",[267,1823,1824],{"class":269,"line":375},[267,1825,1826],{},"2. In the `setup()` function, add `.on(\"postgres_changes\", ...)` handlers\n",[267,1828,1829],{"class":269,"line":381},[267,1830,1831],{},"   for each new table, filtered by `team_id=eq.${teamId}` where the table\n",[267,1833,1834],{"class":269,"line":387},[267,1835,1836],{},"   has a team_id column.\n",[267,1838,1839],{"class":269,"line":393},[267,1840,286],{"emptyLinePlaceholder":285},[267,1842,1843],{"class":269,"line":399},[267,1844,1845],{},"Integration — use `onTableDebounced` from `useRealtime()` inline in each\n",[267,1847,1848],{"class":269,"line":405},[267,1849,1850],{},"page. Do NOT create separate `useRealtimeX` composable files:\n",[267,1852,1853],{"class":269,"line":411},[267,1854,286],{"emptyLinePlaceholder":285},[267,1856,1857],{"class":269,"line":417},[267,1858,1859],{},"- In `app\u002Fpages\u002Fapp\u002Fclients\u002Findex.vue`:\n",[267,1861,1862],{"class":269,"line":423},[267,1863,1864],{},"  `const { onTableDebounced } = useRealtime()`\n",[267,1866,1867],{"class":269,"line":429},[267,1868,1869],{},"  `onTableDebounced(\"clients\", () => refreshClients())`\n",[267,1871,1872],{"class":269,"line":434},[267,1873,1874],{},"- In `app\u002Fpages\u002Fapp\u002Fjobs\u002Findex.vue`:\n",[267,1876,1877],{"class":269,"line":440},[267,1878,1864],{},[267,1880,1881],{"class":269,"line":446},[267,1882,1883],{},"  `onTableDebounced([\"jobs\", \"job_notes\"], () => refreshJobs())`\n",[267,1885,1886],{"class":269,"line":452},[267,1887,1888],{},"- In the dashboard stats page:\n",[267,1890,1891],{"class":269,"line":458},[267,1892,1864],{},[267,1894,1895],{"class":269,"line":464},[267,1896,1897],{},"  `onTableDebounced([\"clients\", \"jobs\"], () => refreshStats())`\n",[252,1899,1901],{"id":1900},"what-you-built","What You Built",[198,1903,1904],{},"Starting from a template that handled auth, teams, roles, and permissions, you added:",[1906,1907,1908,1913,1917,1922],"ol",{},[223,1909,1910,1912],{},[201,1911,744],{}," — a full CRUD module for managing the people and businesses you serve",[223,1914,1915,234],{},[201,1916,1013],{},[223,1918,1919,1921],{},[201,1920,239],{}," — real stats showing active jobs, revenue, and upcoming work",[223,1923,1924,246],{},[201,1925,245],{},[198,1927,1928,1929,1932],{},"Every feature follows the same patterns: permission-gated server routes, team-scoped data, Nuxt UI components, and the conventions defined in ",[264,1930,1931],{},"CLAUDE.md",".",[198,1934,1935],{},"The patterns you learned — how to create tables, write server routes, build UI pages, add permissions, and wire up realtime — apply to any domain. Inventory management, support tickets, invoices, bookings, fleet tracking — the architecture is identical. Only the tables and business logic change.",[252,1937,1939],{"id":1938},"whats-next","What's Next",[220,1941,1942,1949],{},[223,1943,1944,1948],{},[201,1945,1946],{},[206,1947,59],{"href":60}," — let external services authenticate and interact with your data",[223,1950,1951,1955,1956,1959],{},[201,1952,1953],{},[206,1954,143],{"href":144}," — the baked-in assistant is already wired to your new tables via ",[264,1957,1958],{},"tablePermissions",". Try it with \"Which jobs are due this week?\" or \"How many clients did we add this month?\"",[1961,1962,1963],"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":262,"searchDepth":270,"depth":276,"links":1965},[1966,1967],{"id":1900,"depth":276,"text":1901},{"id":1938,"depth":276,"text":1939},"Build a complete job management system — clients, jobs, dashboard, and realtime sync","md",null,{},{"icon":84},{"title":81,"description":1968},"368RKLEYzYljLXsD3vEPJ_7HRf8us-pSbTm9dSPx4Ms",[1976,1978],{"title":75,"path":76,"stem":77,"description":1977,"icon":57,"children":-1},"How to build full features from the example tutorials.",{"title":86,"path":87,"stem":88,"description":1979,"icon":89,"children":-1},"Build a project board with boards, columns, cards, drag-and-drop, and checklists",1777092169440]