// Mock data for the contractor onboarding. The UI is fully interactive against // these. The cheaper model replaces each with a real endpoint (see // onboarding-agency-types.ts for the exact shapes) — DO NOT delete the types; // only swap the data source. // // Vibn builds CUSTOM TOOLS for local businesses. The consultant picks the tool // categories they want to build; we intersect those with each SMB type's // `softwareNeeds` (market_data_assets/smb_to_software_mapping_final.json) to // recommend local businesses they could target. import type { CityRef, TerritoryOpportunity, ToolCategory, } from "./onboarding-agency-types"; // ── Cities (DEV FALLBACK ONLY) ─────────────────────────────────────────────── // Production city lookup hits Places API (New) Autocomplete (global, 200M+ // places) via GET /api/agency/cities — see CityLookup, which calls that endpoint // and only falls back to this list when offline. export const STARTER_CITIES: CityRef[] = [ { id: "victoria-bc", name: "Victoria", region: "BC", country: "Canada", countryCode: "CA", }, { id: "vancouver-bc", name: "Vancouver", region: "BC", country: "Canada", countryCode: "CA", }, { id: "nanaimo-bc", name: "Nanaimo", region: "BC", country: "Canada", countryCode: "CA", }, { id: "kelowna-bc", name: "Kelowna", region: "BC", country: "Canada", countryCode: "CA", }, { id: "calgary-ab", name: "Calgary", region: "AB", country: "Canada", countryCode: "CA", }, { id: "edmonton-ab", name: "Edmonton", region: "AB", country: "Canada", countryCode: "CA", }, { id: "toronto-on", name: "Toronto", region: "ON", country: "Canada", countryCode: "CA", }, { id: "ottawa-on", name: "Ottawa", region: "ON", country: "Canada", countryCode: "CA", }, { id: "montreal-qc", name: "Montreal", region: "QC", country: "Canada", countryCode: "CA", }, { id: "halifax-ns", name: "Halifax", region: "NS", country: "Canada", countryCode: "CA", }, { id: "seattle-wa", name: "Seattle", region: "WA", country: "United States", countryCode: "US", }, { id: "portland-or", name: "Portland", region: "OR", country: "United States", countryCode: "US", }, { id: "san-francisco-ca", name: "San Francisco", region: "CA", country: "United States", countryCode: "US", }, { id: "austin-tx", name: "Austin", region: "TX", country: "United States", countryCode: "US", }, { id: "denver-co", name: "Denver", region: "CO", country: "United States", countryCode: "US", }, { id: "chicago-il", name: "Chicago", region: "IL", country: "United States", countryCode: "US", }, { id: "new-york-ny", name: "New York", region: "NY", country: "United States", countryCode: "US", }, { id: "london-uk", name: "London", region: "England", country: "United Kingdom", countryCode: "GB", }, { id: "sydney-au", name: "Sydney", region: "NSW", country: "Australia", countryCode: "AU", }, ]; export const DEFAULT_CITY: CityRef = STARTER_CITIES[0]; export const cityLabel = (c: CityRef) => `${c.name}, ${c.region}`; /** GET /api/agency/cities?q= — mock typeahead over the seed list. */ export function searchCities(query: string): CityRef[] { const q = query.trim().toLowerCase(); if (!q) return STARTER_CITIES.slice(0, 8); return STARTER_CITIES.filter( (c) => cityLabel(c).toLowerCase().includes(q) || c.country.toLowerCase().includes(q), ).slice(0, 8); } // ── Tool categories the consultant can build ───────────────────────────────── // GET /api/agency/tool-categories. `label` MUST match the strings in // smb_to_software_mapping so the targeting intersection works. These are the // common horizontal tools; the backend can expose the full ~524 from the mapping. export const TOOL_CATEGORIES: ToolCategory[] = [ { id: "reporting", label: "Reporting / Dashboard Software", short: "Reporting & dashboards", icon: "chart", }, { id: "scheduling", label: "Appointment Scheduling Software", short: "Booking & scheduling", icon: "calendar", }, { id: "crm", label: "CRM Software", short: "Customer CRM", icon: "users" }, { id: "invoicing", label: "Invoicing & Payments Software", short: "Invoicing & payments", icon: "receipt", }, { id: "billing", label: "Billing Software", short: "Billing", icon: "receipt", }, { id: "inventory", label: "Inventory Management Software", short: "Inventory", icon: "box", }, { id: "pos", label: "Retail POS System", short: "Point of sale", icon: "card", }, { id: "ordering", label: "Online Ordering Software", short: "Online ordering", icon: "cart", }, { id: "reservations", label: "Reservations & Online Bookings", short: "Reservations", icon: "calendar", }, { id: "fsm", label: "Field Service Management (FSM)", short: "Field service / dispatch", icon: "clipboard", }, { id: "staff", label: "Employee Scheduling Software", short: "Staff scheduling", icon: "users", }, { id: "membership", label: "Membership Management Software", short: "Memberships", icon: "badge", }, { id: "marketing", label: "Marketing Automation Software", short: "Marketing automation", icon: "megaphone", }, { id: "accounting", label: "Accounting Software", short: "Accounting", icon: "chart", }, { id: "orders", label: "Order Management Software", short: "Order management", icon: "clipboard", }, ]; // ── SMB types + their software needs (sample from the mapping) ──────────────── // Real targeting reads the full smb_to_software_mapping; this is a grounded // sample so the intersection responds to selections offline. Counts are mock. type SmbTarget = { gcid: string; displayName: string; softwareNeeds: string[]; businessCount: number; weakPct: number; claimed: number; }; const SMB_TARGETS: SmbTarget[] = [ { gcid: "gcid:dentist", displayName: "Dentists", businessCount: 312, weakPct: 0.71, claimed: 0, softwareNeeds: [ "Dental Practice Management", "EHR / EMR Software", "Medical Billing Software", "Patient Scheduling Software", "Appointment Scheduling Software", ], }, { gcid: "gcid:plumber", displayName: "Plumbers", businessCount: 287, weakPct: 0.86, claimed: 0, softwareNeeds: [ "Plumbing Software", "Field Service Management (FSM)", "Scheduling Software", "Billing Software", "Invoicing & Payments Software", ], }, { gcid: "gcid:hvac_contractor", displayName: "HVAC contractors", businessCount: 248, weakPct: 0.83, claimed: 1, softwareNeeds: [ "HVAC Software", "Field Service Management (FSM)", "Accounting Software", "Invoicing & Payments Software", ], }, { gcid: "gcid:hair_salon", displayName: "Hair salons", businessCount: 524, weakPct: 0.74, claimed: 6, softwareNeeds: [ "Salon / Spa Management Software", "Appointment Scheduling Software", "Billing Software", "CRM Software", ], }, { gcid: "gcid:beauty_salon", displayName: "Beauty salons", businessCount: 411, weakPct: 0.76, claimed: 3, softwareNeeds: [ "Salon / Spa Management Software", "Appointment Scheduling Software", "CRM Software", "Retail POS System", "Accounting Software", ], }, { gcid: "gcid:auto_repair_shop", displayName: "Auto repair shops", businessCount: 198, weakPct: 0.8, claimed: 1, softwareNeeds: [ "Auto Repair Shop Software", "Invoicing & Payments Software", "Appointment Scheduling Software", "Inventory Management Software", "CRM Software", ], }, { gcid: "gcid:gym", displayName: "Gyms & fitness studios", businessCount: 134, weakPct: 0.58, claimed: 2, softwareNeeds: [ "Fitness & Gym Management Software", "Membership Management Software", "Appointment Scheduling Software", "Billing Software", "CRM Software", ], }, { gcid: "gcid:restaurant", displayName: "Restaurants", businessCount: 642, weakPct: 0.69, claimed: 5, softwareNeeds: [ "Restaurant POS Software", "Inventory Management Software", "Employee Scheduling Software", "Online Ordering Software", ], }, { gcid: "gcid:medical_spa", displayName: "Med spas", businessCount: 96, weakPct: 0.62, claimed: 0, softwareNeeds: [ "Medical Spa Software", "Appointment Scheduling Software", "CRM Software", "Retail POS System", "Marketing Automation Software", ], }, { gcid: "gcid:electrician", displayName: "Electricians", businessCount: 233, weakPct: 0.82, claimed: 1, softwareNeeds: [ "Electrical Contractor Software", "Field Service Management (FSM)", "Accounting Software", "Invoicing & Payments Software", ], }, { gcid: "gcid:veterinarian", displayName: "Veterinarians", businessCount: 88, weakPct: 0.64, claimed: 0, softwareNeeds: [ "Veterinary Management Software", "Appointment Scheduling Software", "Inventory Management Software", "Medical Billing Software", ], }, { gcid: "gcid:coffee_shop", displayName: "Coffee shops", businessCount: 276, weakPct: 0.67, claimed: 4, softwareNeeds: [ "Restaurant POS Software", "Inventory Management Software", "Employee Scheduling Software", "Marketing Automation Software", "Online Ordering Software", ], }, { gcid: "gcid:florist", displayName: "Florists", businessCount: 109, weakPct: 0.78, claimed: 0, softwareNeeds: [ "Florist Software", "Retail POS System", "Inventory Management Software", "Online Ordering Software", "Order Management Software", ], }, { gcid: "gcid:landscaper", displayName: "Landscapers", businessCount: 264, weakPct: 0.85, claimed: 1, softwareNeeds: [ "Landscape / Lawn Care Software", "Field Service Management (FSM)", "Appointment Scheduling Software", "Billing Software", "CRM Software", ], }, { gcid: "gcid:accounting_firm", displayName: "Accounting firms", businessCount: 187, weakPct: 0.59, claimed: 2, softwareNeeds: [ "Accounting Practice Management", "Accounting Software", "CRM Software", ], }, { gcid: "gcid:bakery", displayName: "Bakeries", businessCount: 142, weakPct: 0.72, claimed: 0, softwareNeeds: [ "Bakery Software", "Inventory Management Software", "Order Management Software", "Employee Scheduling Software", ], }, ]; // ── AI expertise → tool categories ─────────────────────────────────────────── // POST /api/agency/analyze-expertise { text } -> { tools }. The real version is // an LLM call; this is a keyword stub so the flow works offline. Maps free-text // expertise to canonical tool-category labels that join to the SMB mapping. const EXPERTISE_KEYWORDS: Array<{ re: RegExp; label: string }> = [ { re: /report|dashboard|analytic|insight|\bbi\b/i, label: "Reporting / Dashboard Software", }, { re: /crm|customer relationship|\bleads?\b|\bclients?\b/i, label: "CRM Software", }, { re: /schedul|book|appointment|calendar/i, label: "Appointment Scheduling Software", }, { re: /invoic|payment/i, label: "Invoicing & Payments Software" }, { re: /billing/i, label: "Billing Software" }, { re: /inventor|stock/i, label: "Inventory Management Software" }, { re: /\bpos\b|point of sale|checkout|register/i, label: "Retail POS System", }, { re: /online order|ordering|takeout/i, label: "Online Ordering Software" }, { re: /reservation/i, label: "Reservations & Online Bookings" }, { re: /field service|dispatch|\bfsm\b|work order/i, label: "Field Service Management (FSM)", }, { re: /staff schedul|employee schedul|\brota\b|\bshifts?\b/i, label: "Employee Scheduling Software", }, { re: /member/i, label: "Membership Management Software" }, { re: /marketing|campaign|email automat|newsletter/i, label: "Marketing Automation Software", }, { re: /account|bookkeep/i, label: "Accounting Software" }, { re: /order management|fulfilment|fulfillment/i, label: "Order Management Software", }, ]; export function extractTools(text: string): string[] { const out: string[] = []; for (const k of EXPERTISE_KEYWORDS) { if (k.re.test(text) && !out.includes(k.label)) out.push(k.label); } return out; } /** * POST /api/agency/targets { city, tools } * Recommends SMB types whose softwareNeeds intersect the consultant's tools, * scored by demand × gap × low Vibn saturation, biased toward how many of the * consultant's tools each SMB needs. */ export function mockTargets( city: CityRef, tools: string[], ): TerritoryOpportunity[] { const want = new Set(tools); // Reporting / dashboards apply to virtually every business, so treat it as a // universal need rather than listing it on every SMB type. const UNIVERSAL = ["Reporting / Dashboard Software"]; return SMB_TARGETS.map((s, i) => { const needs = [ ...s.softwareNeeds, ...UNIVERSAL.filter((u) => !s.softwareNeeds.includes(u)), ]; const matchedTools = needs.filter((n) => want.has(n)); return { s, matchedTools, i }; }) .filter((x) => x.matchedTools.length > 0) .map(({ s, matchedTools, i }) => { const weak = Math.round(s.businessCount * s.weakPct); const saturation = s.claimed / Math.max(1, s.businessCount / 20); const demand = s.weakPct * (1 - Math.min(1, saturation)); const fit = Math.min(1, matchedTools.length / Math.max(1, tools.length)); const score = Math.max( 28, Math.min(98, Math.round((demand * 0.7 + fit * 0.3) * 100)), ); const status: TerritoryOpportunity["status"] = s.claimed === 0 ? "open" : s.claimed <= 2 ? "contested" : "claimed"; return { id: `${s.gcid}-${i}`, gcid: s.gcid, niche: s.displayName, city: city.name, businessCount: s.businessCount, weakWebsiteCount: weak, vibnClaimedCount: s.claimed, opportunityScore: score, status, matchedTools, }; }) .sort((a, b) => b.opportunityScore - a.opportunityScore); }