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