import React, { createContext, useContext, useState, useEffect } from 'react'; import { toast } from 'sonner'; import { logger } from '../utils/logger'; // --- DATA CONSTANTS (Extracted from old App.jsx) --- const RAW_LOCATIONS = [ // Residential { address: "2612 Dunwick Dr, Plano, TX 75023", lat: 33.07084060970922, lng: -96.74558398435258, type: "Residential" }, { address: "2608 Dunwick Dr, Plano, TX 75023", lat: 33.070834939506284, lng: -96.74532009555337, type: "Residential" }, { address: "2604 Dunwick Dr, Plano, TX 75023", lat: 33.07081036862272, lng: -96.74507199497292, type: "Residential" }, { address: "2600 Dunwick Dr, Plano, TX 75023", lat: 33.07083115937081, lng: -96.74483066077192, type: "Residential" }, { address: "2516 Dunwick Dr, Plano, TX 75023", lat: 33.070770677180924, lng: -96.74459158203076, type: "Residential" }, { address: "2637 Rothland Ln, Plano, TX 75023", lat: 33.070426683935366, lng: -96.74557721797312, type: "Residential" }, { address: "2633 Rothland Ln, Plano, TX 75023", lat: 33.07042290378234, lng: -96.74534265015158, type: "Residential" }, { address: "2629 Rothland Ln, Plano, TX 75023", lat: 33.07042857401182, lng: -96.74508552773183, type: "Residential" }, { address: "2617 Rothland Ln, Plano, TX 75023", lat: 33.070432354164595, lng: -96.74435024782977, type: "Residential" }, { address: "2513 Rothland Ln, Plano, TX 75023", lat: 33.07016207283135, lng: -96.74304208113281, type: "Residential" }, // New Residential Data { address: "6613 Phoenix Pl, Plano, TX 75023, USA", lat: 33.063266298016536, lng: -96.76932248857813, type: "Residential" }, { address: "6609 Phoenix Pl, Plano, TX 75023, USA", lat: 33.063141994627365, lng: -96.76932089372718, type: "Residential" }, { address: "6605 Phoenix Pl, Plano, TX 75023, USA", lat: 33.06302437405446, lng: -96.76933365253466, type: "Residential" }, { address: "6601 Phoenix Pl, Plano, TX 75023, USA", lat: 33.06290541672423, lng: -96.7693424242148, type: "Residential" }, { address: "6617 Phoenix Pl, Plano, TX 75023, USA", lat: 33.063379067434504, lng: -96.7693376006764, type: "Residential" }, { address: "3913 Arizona Pl, Plano, TX 75023, USA", lat: 33.06318117622371, lng: -96.7698079594919, type: "Residential" }, { address: "3917 Arizona Pl, Plano, TX 75023, USA", lat: 33.063193097393324, lng: -96.76997201609075, type: "Residential" }, { address: "3921 Arizona Pl, Plano, TX 75023, USA", lat: 33.0631946868825, lng: -96.77014555573001, type: "Residential" }, { address: "3920 Arizona Pl, Plano, TX 75023, USA", lat: 33.06252788822516, lng: -96.77010020638036, type: "Residential" }, { address: "3916 Arizona Pl, Plano, TX 75023, USA", lat: 33.06254219373469, lng: -96.76993520147747, type: "Residential" }, { address: "3912 Arizona Pl, Plano, TX 75023, USA", lat: 33.06254378323562, lng: -96.76975502370993, type: "Residential" }, { address: "3908 Arizona Pl, Plano, TX 75023, USA", lat: 33.06253583573075, lng: -96.76959191541512, type: "Residential" }, { address: "3904 Arizona Pl, Plano, TX 75023, USA", lat: 33.062526298723945, lng: -96.769445876593, type: "Residential" }, { address: "3900 Arizona Pl, Plano, TX 75023, USA", lat: 33.06252153022017, lng: -96.76928656151433, type: "Residential" }, { address: "3917 Sailmaker Ln, Plano, TX 75023, USA", lat: 33.0622004503711, lng: -96.76993899469362, type: "Residential" }, { address: "3913 Sailmaker Ln, Plano, TX 75023, USA", lat: 33.06218773431336, lng: -96.76979295587151, type: "Residential" }, { address: "3909 Sailmaker Ln, Plano, TX 75023, USA", lat: 33.06220362938525, lng: -96.76961846792823, type: "Residential" }, { address: "3905 Sailmaker Ln, Plano, TX 75023, USA", lat: 33.06221157692012, lng: -96.76945725624147, type: "Residential" }, { address: "3901 Sailmaker Ln, Plano, TX 75023, USA", lat: 33.06219091332797, lng: -96.7692808716901, type: "Residential" }, // Commercial { address: "6909 Custer Rd, Plano, TX 75023", lat: 33.07020596727293, lng: -96.7390826077087, type: "Commercial" }, { address: "2112 Winslow Dr, Plano, TX 75023", lat: 33.069360828751016, lng: -96.73770127012321, type: "Commercial" }, { address: "7224 Independence Pkwy, Plano, TX 75025", lat: 33.07506839285908, lng: -96.74984052259599, type: "Commercial" }, // New Commercial Data { address: "Dallas/Plano Marriott at Legacy Town Center, 7121 Bishop Rd, Plano, TX 75024, United States", lat: 33.074457402913424, lng: -96.8222322519585, type: "Commercial" }, { address: "Legacy Town Center Plano, 5760 Daniel Rd, Plano, TX 75024, United States", lat: 33.07360554164325, lng: -96.82142050370054, type: "Commercial" }, { address: "WeWork Office Space & Coworking, 6900 Dallas Pkwy Suite 300, Plano, TX 75024, United States", lat: 33.07281088750022, lng: -96.8235902233433, type: "Commercial" }, { address: "Truluck's Ocean's Finest Seafood and Crab, 7161 Bishop Rd, Plano, TX 75024, United States", lat: 33.07535565705897, lng: -96.82174939646889, type: "Commercial" }, { address: "Del Frisco's Grille, 7200 Bishop Rd Suite D9, Plano, TX 75024, United States", lat: 33.07630819707901, lng: -96.82124417346868, type: "Commercial" }, { address: "Scruffy Duffies, 5865 Kincaid Rd E8, Plano, TX 75024, United States", lat: 33.07601336437884, lng: -96.82257940570773, type: "Commercial" }, { address: "Half Shells, 7201 Bishop Rd #E4, Plano, TX 75024, United States", lat: 33.07635733578195, lng: -96.82171330913596, type: "Commercial" }, { address: "Mi Cocina, 5760 Legacy Dr #B7, Plano, TX 75024, United States", lat: 33.07743082031222, lng: -96.82109531315771, type: "Commercial" }, { address: "Leasing Office space and The Kincaid at Legacy Apartment, 7200 Dallas Pkwy, Plano, TX 75024, United States", lat: 33.07619102009707, lng: -96.82340039310428, type: "Commercial" }, { address: "Issil Beauty Spa, 7140 Bishop Rd #F4, Plano, TX 75024, United States", lat: 33.07550110630488, lng: -96.82122287598153, type: "Commercial" }, // Apartments { address: "2100 Legacy Dr, Plano, TX 75023", lat: 33.07035321131275, lng: -96.73493675556306, type: "Apartments" }, { address: "2401 Brown Deer Trail, Plano, TX 75023", lat: 33.06897509321587, lng: -96.7419549494157, type: "Residential" }, ]; const PROPERTY_STYLES = [ { label: "Luxury Estate", canvassingStatus: "Customer", photos: [ { id: 'le_1', url: "/assets/images/properties/Luxury_Driveway_Residence.jpg", caption: "Front Exterior" }, { id: 'le_2', url: "/assets/images/properties/Sunny_Suburban_House.jpg", caption: "Side Elevation" }, { id: 'le_3', url: "/assets/images/properties/Residential_Roof_Texture.jpg", caption: "Roof Quality" } ], baseValue: 850000, areaBase: 4200, beds: 5, baths: 4.5, yearBase: 2010, desc: "Luxury estate with custom finishes throughout.", condition: 5 }, { label: "Standard Family Home", canvassingStatus: "Neutral", photos: [ { id: 'sf_1', url: "/assets/images/properties/Modern_White_Suburban_Home.jpg", caption: "Front View" }, { id: 'sf_2', url: "/assets/images/properties/Brick_Front_Porch_Home.jpg", caption: "Entryway" }, { id: 'sf_3', url: "/assets/images/properties/Overlapping_Roof_Shingles.jpg", caption: "Roof Condition" }, { id: 'sf_4', url: "/assets/images/properties/Red_Shingle_Cottage.jpg", caption: "Cottage Style" } ], baseValue: 450000, areaBase: 2800, beds: 4, baths: 3, yearBase: 1995, desc: "Well-maintained family home in established neighborhood.", condition: 4 }, { label: "Fixer Upper", canvassingStatus: "Hot Lead", photos: [ { id: 'fu_1', url: "/assets/images/properties/Beige_Two_Story_House.jpg", caption: "Street View" }, { id: 'fu_2', url: "/assets/images/properties/Hail_Damaged_Shingles.jpg", caption: "Hail Damage" }, { id: 'fu_3', url: "/assets/images/properties/Broken_Roof_Surface.jpg", caption: "Critical Repairs Needed" }, { id: 'fu_4', url: "/assets/images/properties/Cracked_Storm_Shingles.jpg", caption: "Storm Damage" }, { id: 'fu_5', url: "/assets/images/properties/Storm_Worn_Roof.jpg", caption: "Worn Roof" } ], baseValue: 320000, areaBase: 2400, beds: 3, baths: 2, yearBase: 1980, desc: "Great potential, needs cosmetic updates.", condition: 2 }, { label: "Renovated Modern", canvassingStatus: "Renovated", photos: [ { id: 'rm_1', url: "/assets/images/properties/Minimal_Flat_Roof_House.jpg", caption: "Modern Facade" }, { id: 'rm_2', url: "/assets/images/properties/Red_Tiled_Roof_House.jpg", caption: "New Roof" }, { id: 'rm_3', url: "/assets/images/properties/Roof_Inspection_Angle.jpg", caption: "Drone Inspection" }, { id: 'rm_4', url: "/assets/images/properties/Red_Metal_Roof_Home.jpg", caption: "Metal Roof Upgrade" } ], baseValue: 580000, areaBase: 3200, beds: 4, baths: 3.5, yearBase: 1990, desc: "Recently renovated with modern aesthetics.", condition: 5 } ]; const COMMERCIAL_VARIANTS = [ { name: "Legacy Retail Group", type: "Retail Mix", email: "leasing@legacyretail.com" }, { name: "Custer Office Park LLC", type: "Office Space", email: "mgmt@custeroffice.com" }, { name: "Plano Investments Inc", type: "Mixed Use", email: "contact@planoinvest.com" }, { name: "North Texas Prop Co", type: "Service Center", email: "info@ntxprops.com" } ]; const REAL_NAMES = [ "James Beauford", "Sarah Miller", "Robert Henderson", "Karen Smith", "Michael Chang", "Patricia O'Neil", "Dr. Anish Patel", "Marcus Johnson", "Elena Rodriguez", "Greg Alston", "Linda Grey", "Bill Thompson", "Jennifer Wu", "David Kowalski", "Omar Farooq", "Lisa Chen", "Tom Baker", "Rachel Green", "Steve Martin", "Angela White" ]; const OCCUPATIONS = [ "Software Engineer", "Petroleum Engineer", "Teacher", "Nurse Practitioner", "Small Business Owner", "Accountant", "Marketing Director", "retired", "Attorney", "Real Estate Agent", "Sales Manager", "Consultant" ]; const EMPLOYERS_BY_OCCUPATION = { "Software Engineer": ["Google", "Microsoft", "Toyota Connected", "Capital One", "Intuit"], "Petroleum Engineer": ["ExxonMobil", "Chevron", "Pioneer Natural Resources", "Denbury"], "Teacher": ["Plano ISD", "Frisco ISD", "Dallas ISD", "Richardson ISD"], "Nurse Practitioner": ["Baylor Scott & White", "Medical City Plano", "Texas Health Resources"], "Small Business Owner": ["Self Employed", "Local Boutique", "Consultancy Firm"], "Accountant": ["Deloitte", "PwC", "EY", "KPMG", "Local CPA Firm"], "Marketing Director": ["PepsiCo", "Toyota", "Frito-Lay", "Yum! Brands"], "retired": ["N/A"], "Attorney": ["Jones Day", "Baker Botts", "Norton Rose Fulbright", "Local Law Firm"], "Real Estate Agent": ["Keller Williams", "Ebby Halliday", "Compass", "Remax"], "Sales Manager": ["Oracle", "Salesforce", "AT&T", "Samsung"], "Consultant": ["McKinsey", "BCG", "Accenture", "Freelance"] }; // --- NEW INSURANCE SCHEMA CONSTANTS --- const INSURANCE_COMPANIES = [ "State Farm", "Allstate", "Liberty Mutual", "Farmers", "Nationwide", "USAA", "Chubb", "Travelers", "Progressive", "American Family" ]; const ADJUSTER_TYPES = ["Staff Adjuster", "Independent Adjuster", "Public Adjuster"]; // --- DATA GENERATION HELPERS --- function generatePolygon(lat, lng) { const dLat = 0.0001; // ~11 meters const dLng = 0.0001; // ~11 meters return [ [lat + dLat, lng - dLng], // Top Left [lat + dLat, lng + dLng], // Top Right [lat - dLat, lng + dLng], // Bottom Right [lat - dLat, lng - dLng], // Bottom Left ]; } function generateEmail(name, company = null) { if (company) return company; const domains = ["gmail.com", "yahoo.com", "outlook.com", "icloud.com"]; const domain = domains[Math.floor(Math.random() * domains.length)]; const parts = name.toLowerCase().split(' '); const last = parts[parts.length - 1].replace(/[^a-z]/g, ''); const first = parts[0].replace(/[^a-z]/g, ''); return `${first}.${last}@${domain}`; } // Weighted Random Helper function getWeightedRandom(items, weights) { const totalWeight = weights.reduce((a, b) => a + b, 0); const random = Math.random() * totalWeight; let weightSum = 0; for (let i = 0; i < items.length; i++) { weightSum += weights[i]; if (random <= weightSum) { return items[i]; } } return items[items.length - 1]; // Fallback } function generateProperties() { // 1. Generate Base Properties const allProperties = RAW_LOCATIONS.map((loc, index) => { const isCommercial = loc.type === "Commercial" || loc.type === "Apartments"; // Shuffle styles slightly differently than index-based to avoid repeating patterns const styleIndex = Math.floor(Math.random() * PROPERTY_STYLES.length); const style = isCommercial ? null : PROPERTY_STYLES[styleIndex]; let ownerName, ownerEmail, occupation, income, employerName; if (isCommercial) { const commVariant = COMMERCIAL_VARIANTS[index % COMMERCIAL_VARIANTS.length]; ownerName = commVariant.name; ownerEmail = commVariant.email; occupation = "Commercial Owner"; income = "$1M+"; employerName = "Self"; } else { ownerName = REAL_NAMES[index % REAL_NAMES.length]; ownerEmail = generateEmail(ownerName); occupation = OCCUPATIONS[index % OCCUPATIONS.length]; const potentialEmployers = EMPLOYERS_BY_OCCUPATION[occupation] || ["Self Employed"]; employerName = potentialEmployers[index % potentialEmployers.length]; income = ["$80k - $120k", "$120k - $180k", "$200k+", "$60k - $90k"][index % 4]; } // Randomize Year and Value to break lines const yearOffset = Math.floor(Math.random() * 20) - 10; // +/- 10 years const builtYear = isCommercial ? 1995 + (index % 10) : style.yearBase + yearOffset; const valueVariance = (Math.random() * 0.4) - 0.2; // +/- 20% const baseSqft = isCommercial ? 12000 + (index * 500) : style.areaBase; const sqft = Math.floor(baseSqft * (1 + (valueVariance / 2))); // Size correlates with value but not perfectly const baseVal = isCommercial ? 2500000 + (index * 100000) : style.baseValue; const value = Math.floor(baseVal * (1 + valueVariance)); // Rental Logic: Target ~10-15 rented (approx 40-50% chance given ~30 props) const isRented = Math.random() > 0.55; let tenantData = {}; if (isRented) { const tName = isCommercial ? "Global Corp Inc." : REAL_NAMES[(index + 5) % REAL_NAMES.length]; const tOcc = isCommercial ? "Retail/Office Mix" : OCCUPATIONS[(index + 3) % OCCUPATIONS.length]; const tPhone = `469-555-${2000 + index}`; const tEmail = generateEmail(tName.split(' ')[0] || "tenant", isCommercial ? "tenant.com" : null); const tEmployer = isCommercial ? "N/A" : (EMPLOYERS_BY_OCCUPATION[tOcc]?.[0] || "Self"); const tIncome = isCommercial ? "$5M+" : ["$60k", "$85k", "$110k", "$140k"][index % 4]; const tFamily = isCommercial ? "N/A" : (Math.random() > 0.4 ? "Family" : "Single"); const tLeaseType = isCommercial ? "Commercial NNN" : "Residential Standard"; tenantData = { currentlyRented: true, currentTenantName: tName, tenantPhone: tPhone, tenantEmail: tEmail, tenantOccupation: tOcc, tenantEmployer: tEmployer, tenantAnnualIncome: tIncome, livingStatus: tFamily, leaseType: tLeaseType, currentMonthlyRentAmount: Math.floor(value * 0.008 / 100) * 100, leaseStartDate: "2023-01-01", leaseEndDate: "2025-01-01", leaseSignedDate: "2022-12-15", ownerWillingToRent: false // Already rented }; } else { tenantData = { currentlyRented: false, currentTenantName: null, tenantPhone: null, tenantEmail: null, tenantOccupation: null, tenantEmployer: null, tenantAnnualIncome: null, livingStatus: null, leaseType: null, currentMonthlyRentAmount: null, leaseStartDate: null, leaseEndDate: null, leaseSignedDate: null, ownerWillingToRent: Math.random() > 0.6 }; } const rentAmount = tenantData.currentMonthlyRentAmount; // For use below if needed, though mapped in tenantData now // Weighted Roof Condition const roofCondition = getWeightedRandom( ["Good", "Fair", "Excellent", "Needs Repair"], [0.4, 0.3, 0.2, 0.1] ); // Neighborhood Rating const neighRating = getWeightedRandom([7, 8, 9, 10, 6, 5], [0.3, 0.3, 0.2, 0.1, 0.05, 0.05]); return { id: index + 1, center: [loc.lat, loc.lng], polygon: generatePolygon(loc.lat, loc.lng), // Placeholder status, will be overwritten canvassingStatus: "Neutral", photos: isCommercial ? [ { id: `com_${index}_1`, url: "/assets/images/properties/Apartment_Complex.jpg", caption: "Exterior View" }, { id: `com_${index}_2`, url: "/assets/images/properties/Mall_Rooftop_HVAC_Units.jpg", caption: "HVAC Systems" } ] : style.photos, // --- NEW FIELDS FOR ADMIN ACTION CENTER --- assignedAgentId: null, // Default unassigned lastContactDate: null, pendingSignature: false, proposalSentDate: null, proposalValue: 0, // ------------------------------------------ propertyData: { propertyId: `P-${2600 + index}`, propertyAddress: loc.address, propertyType: loc.type, totalBuiltUpArea: sqft, yearBuilt: builtYear, numberOfBedrooms: isCommercial ? 0 : style.beds, numberOfBathrooms: isCommercial ? 4 : style.baths, numberOfParkingSpaces: isCommercial ? 25 : 2, currentEstimatedMarketValue: value, propertyTaxAssessmentValue: Math.floor(value * 0.85), roofCondition: roofCondition, renovationDescription: isCommercial ? "Commercial Maintenance" : style.desc, renovationCost: Math.floor(value * (Math.random() * 0.05 + 0.01)), lastMajorRenovationDate: "2020-05-15", lastRoofRepairReplacementDate: "2018-02-10", recentMajorRepairs: "Routine Maintenance", // Spread new robust tenant data ...tenantData, expectedMonthlyRentAmount: Math.floor(value * 0.008 / 100) * 100, rentalAvailabilityDate: null, rentalTermsPreference: null, schoolDistrict: "Plano ISD", neighborhoodRating: neighRating, crimeRateIndex: "Low", publicTransportationAccess: "Bus Line 204", propertyConditionRating: isCommercial ? 4 : style.condition, furnishingStatus: "Unfurnished", notes: isCommercial ? "Prime commercial location" : style.desc, latestPurchasePrice: Math.floor(value * 0.6), latestPurchaseDate: "2015-06-20", latestSaleListingDate: null, latestSaleAskingPrice: null, lotPlotSize: isCommercial ? "1.5 Acres" : "0.22 Acres", // New Fields for Chatbot pendingSignature: Math.random() > 0.9, // 10% chance closedDate: Math.random() > 0.8 ? "2026-01-15" : null // Simple mock for revenue queries }, ownerData: { ownerId: `OWN-${200 + index}`, fullName: ownerName, primaryPhoneNumber: `972-555-${1000 + index}`, secondaryPhoneNumber: null, emailAddress: ownerEmail, mailingAddress: loc.address, occupation: occupation, employerName: employerName, annualIncomeRange: income, creditScoreRange: "720+", ownershipType: isCommercial ? "Corporate" : "Individual", ownershipPercentage: "100%", willingToRentProperty: Math.random() > 0.8, desiredMonthlyRentalPrice: null, rentalConditions: null, willingToSellProperty: Math.random() > 0.85, desiredSellingPrice: Math.floor(value * 1.1), minimumAcceptableSellingPrice: Math.floor(value * 1.05) }, // New Insurance Data Schema insuranceData: (() => { const claimFiled = Math.random() > 0.8; // 20% likely if (!claimFiled) { return { insurance_company: getWeightedRandom(INSURANCE_COMPANIES, Array(10).fill(0.1)), insurance_company_not_listed: false, damage_location: null, date_of_loss: null, claim_filed: false, claim_number: null, has_paperwork: false, adjuster_name: null, adjuster_phone: null, adjuster_ext: null, adjuster_type: null, adjuster_fax: null, adjuster_email: null, met_with_adjuster: false, claim_approved: false }; } // If claim filed, populate detailed data const lossDate = new Date(); lossDate.setDate(lossDate.getDate() - Math.floor(Math.random() * 60)); // Last 60 days return { insurance_company: getWeightedRandom(INSURANCE_COMPANIES, Array(10).fill(0.1)), insurance_company_not_listed: false, damage_location: "Roof, Gutters, Fence", date_of_loss: lossDate.toISOString().split('T')[0], claim_filed: true, claim_number: `CLM-${Math.floor(Math.random() * 1000000)}`, has_paperwork: Math.random() > 0.3, adjuster_name: REAL_NAMES[Math.floor(Math.random() * REAL_NAMES.length)], adjuster_phone: `214-555-${Math.floor(Math.random() * 9000) + 1000}`, adjuster_ext: `${Math.floor(Math.random() * 900) + 100}`, adjuster_type: ADJUSTER_TYPES[Math.floor(Math.random() * ADJUSTER_TYPES.length)], adjuster_fax: `214-555-${Math.floor(Math.random() * 9000) + 1000}`, adjuster_email: `adjuster.${Math.floor(Math.random() * 100)}@insurance.com`, met_with_adjuster: Math.random() > 0.5, claim_approved: Math.random() > 0.7 }; })() }; }); // 2. Assign Statuses with Strict Counts // User Request: 4-9 Hot Leads, 15-20 Customers const countHotLeads = Math.floor(Math.random() * (9 - 4 + 1)) + 4; // 4 to 9 const countCustomers = Math.floor(Math.random() * (20 - 15 + 1)) + 15; // 15 to 20 // Create indices array and shuffle it const indices = allProperties.map((_, i) => i); for (let i = indices.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [indices[i], indices[j]] = [indices[j], indices[i]]; } // Assign based on shuffled indices indices.forEach((propIndex, i) => { if (i < countHotLeads) { allProperties[propIndex].canvassingStatus = "Hot Lead"; } else if (i < countHotLeads + countCustomers) { allProperties[propIndex].canvassingStatus = "Customer"; } else { // Remaining are Neutral, Renovated, or Not Interested // Weighted: 70% Neutral, 20% Not Interested, 10% Renovated allProperties[propIndex].canvassingStatus = getWeightedRandom( ["Neutral", "Not Interested", "Renovated"], [0.7, 0.2, 0.1] ); } }); // --- INJECT REQUIRED MOCK DATA FOR ACTION CENTER --- // A. Unassigned Leads (assignedAgentId: null, canvassingStatus: "Lead") // Target addresses: 2600 Dunwick Dr, 3900 Arizona Pl, 2112 Winslow Dr, Dallas/Plano Marriott, Legacy Town Center const unassignedTargets = [ "2600 Dunwick Dr, Plano, TX 75023", "3900 Arizona Pl, Plano, TX 75023, USA", // Note: original data has USA "2112 Winslow Dr, Plano, TX 75023", "Dallas/Plano Marriott at Legacy Town Center, 7121 Bishop Rd, Plano, TX 75024, United States", "Legacy Town Center Plano, 5760 Daniel Rd, Plano, TX 75024, United States" ]; const unassignedValues = { "2600 Dunwick Dr, Plano, TX 75023": 53173, "3900 Arizona Pl, Plano, TX 75023, USA": 11608, "2112 Winslow Dr, Plano, TX 75023": 40669, "Dallas/Plano Marriott at Legacy Town Center, 7121 Bishop Rd, Plano, TX 75024, United States": 15766, "Legacy Town Center Plano, 5760 Daniel Rd, Plano, TX 75024, United States": 12792 }; allProperties.forEach(p => { const addr = p.propertyData.propertyAddress; if (unassignedTargets.includes(addr)) { p.canvassingStatus = "Lead"; // "Lead" = Unassigned basically in this context, or just "Neutral" but we track assignments p.assignedAgentId = null; p.propertyData.currentEstimatedMarketValue = unassignedValues[addr]; } else { // Default random assignment for others if not unassigned if (p.canvassingStatus !== "Hot Lead") { // Keep Hot Leads potentially unassigned or specific // p.assignedAgentId = "e1"; // Optional: assign everything else } } }); // B. Hot Leads Not Contacted // Data: 2629 Rothland Ln (2026-02-08), 3920 Arizona Pl (2026-02-08), 3912 Arizona Pl (Never), 3909 Sailmaker Ln (Never) const hotLeadTargets = { "2629 Rothland Ln, Plano, TX 75023": "2026-02-08T10:00:00Z", "3920 Arizona Pl, Plano, TX 75023, USA": "2026-02-08T14:30:00Z", "3912 Arizona Pl, Plano, TX 75023, USA": null, "3909 Sailmaker Ln, Plano, TX 75023, USA": null }; allProperties.forEach(p => { if (hotLeadTargets.hasOwnProperty(p.propertyData.propertyAddress)) { p.canvassingStatus = "Hot Lead"; p.lastContactDate = hotLeadTargets[p.propertyData.propertyAddress]; // Set values if needed match User Data? // The prompt gave values: 2629 ($20,298), 3920 ($84,288), 3912 ($73,307), 3909 ($72,563) if (p.propertyData.propertyAddress.includes("2629 Rothland")) p.propertyData.currentEstimatedMarketValue = 20298; if (p.propertyData.propertyAddress.includes("3920 Arizona")) p.propertyData.currentEstimatedMarketValue = 84288; if (p.propertyData.propertyAddress.includes("3912 Arizona")) p.propertyData.currentEstimatedMarketValue = 73307; if (p.propertyData.propertyAddress.includes("3909 Sailmaker")) p.propertyData.currentEstimatedMarketValue = 72563; } }); // C. Pending Signatures // Data: Client 1 (2612 Dunwick), Client 2 (2608 Dunwick), Client 9 (2617 Rothland), Client 12 (6609 Phoenix) const signatureTargets = { "2612 Dunwick Dr, Plano, TX 75023": { date: "2026-02-05", val: 8521 }, "2608 Dunwick Dr, Plano, TX 75023": { date: "2026-02-09", val: 16468 }, "2617 Rothland Ln, Plano, TX 75023": { date: "2026-02-08", val: 18392 }, "6609 Phoenix Pl, Plano, TX 75023, USA": { date: "2026-02-06", val: 31768 } }; allProperties.forEach(p => { if (signatureTargets.hasOwnProperty(p.propertyData.propertyAddress)) { p.pendingSignature = true; p.proposalSentDate = signatureTargets[p.propertyData.propertyAddress].date; p.proposalValue = signatureTargets[p.propertyData.propertyAddress].val; p.propertyData.pendingSignature = true; // Sync with inner object if needed, but we extended outer } else { p.pendingSignature = false; } }); // ------------------------------------------------------- // 3. Assign Tenants (Strict Counts & Data Depth) // User Request: 10-15 Total Rented. 5-8 with "Complete" info, rest "Sparse" (Mandatory only). // Current Date Context: Feb 5, 2026. const totalRented = Math.floor(Math.random() * (15 - 10 + 1)) + 10; // 10 to 15 const fullDetailCount = Math.floor(Math.random() * (8 - 5 + 1)) + 5; // 5 to 8 const sparseDetailCount = totalRented - fullDetailCount; // Reshuffle for tenant assignment (independent of status) const tenantIndices = allProperties.map((_, i) => i); for (let i = tenantIndices.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [tenantIndices[i], tenantIndices[j]] = [tenantIndices[j], tenantIndices[i]]; } tenantIndices.forEach((propIndex, i) => { if (i < totalRented) { const prop = allProperties[propIndex]; const isFullDetail = i < fullDetailCount; const isCommercial = prop.propertyData.propertyType === "Commercial" || prop.propertyData.propertyType === "Apartments"; const val = prop.propertyData.currentEstimatedMarketValue; // Generate Base Data (Mandatory) const tName = isCommercial ? "Global Corp Inc." : REAL_NAMES[(propIndex + 5) % REAL_NAMES.length]; const tPhone = `469-555-${3000 + propIndex}`; // Optional Data (Full vs Sparse) const tEmail = isFullDetail ? generateEmail(tName.split(' ')[0], isCommercial ? "enterprises.com" : null) : ""; const tOcc = isFullDetail ? (isCommercial ? "Retail/Office Mix" : OCCUPATIONS[(propIndex + 3) % OCCUPATIONS.length]) : ""; const tEmployer = isFullDetail ? (isCommercial ? "N/A" : (EMPLOYERS_BY_OCCUPATION[tOcc]?.[0] || "Self")) : ""; const tIncome = isFullDetail ? (isCommercial ? "$5M+" : ["$60k", "$85k", "$110k", "$140k"][propIndex % 4]) : ""; const tFamily = isFullDetail ? (isCommercial ? "N/A" : (Math.random() > 0.4 ? "Family" : "Single")) : "Single"; const tLeaseType = isFullDetail ? (isCommercial ? "Commercial NNN" : "Residential Standard") : "Residential Standard"; // Dates (Relative to Feb 2026) // Start: Jan 2025 - Jan 2026 const startYear = Math.random() > 0.5 ? 2025 : 2026; const tStart = `${startYear}-01-15`; // Signed: 1 month before start const tSigned = `${startYear - 1}-12-15`; // End: 1 year after start (2026 or 2027) const tEnd = `${startYear + 1}-01-14`; const tRent = Math.floor(val * 0.008 / 100) * 100; // Apply updates allProperties[propIndex].propertyData = { ...allProperties[propIndex].propertyData, currentlyRented: true, currentTenantName: tName, tenantPhone: tPhone, // Optional / Detail Dependent tenantEmail: tEmail, tenantOccupation: tOcc, tenantEmployer: tEmployer, tenantAnnualIncome: tIncome, livingStatus: tFamily, leaseType: tLeaseType, // Dates & Financials (Assign sparse ones too if needed, or leave blank? User said sparse has MANDATORY only. But Rent amount is needed to be "Rented" usually. I'll add Rent/Dates to Full, and maybe just Rent to Sparse?) // Re-reading: "The remaining entries may include only the mandatory fields." // I will add rent/dates to ALL rented properties because otherwise the map might look broken for "Rented" status without a lease end date. But strictly, I could leave them empty. // Let's populate Dates/Rent for ALL, but Personal Info (Email/Job/Income) only for Full. currentMonthlyRentAmount: tRent, leaseStartDate: tStart, leaseEndDate: tEnd, leaseSignedDate: tSigned, ownerWillingToRent: false }; } }); // 4. Refine Renovation Dates (Recent for 'Renovated' status) // User Request: Not older than 6 months from Feb 2026 (Aug 2025 - Feb 2026) allProperties.forEach(prop => { if (prop.canvassingStatus === "Renovated") { const monthsBack = Math.floor(Math.random() * 6); // 0 to 5 months ago // Base date: Feb 2026. // 0 months ago = Feb 2026. 1 = Jan 2026... 5 = Sep 2025. // Let's pick a random day. const year = monthsBack <= 1 ? 2026 : 2025; let month = 2 - monthsBack; // Feb(2) - 0 = 2. Feb(2) - 5 = -3. if (month <= 0) month += 12; // Wrap around to previous year const day = Math.floor(Math.random() * 28) + 1; const monthStr = month.toString().padStart(2, '0'); const dayStr = day.toString().padStart(2, '0'); prop.propertyData.lastMajorRenovationDate = `${year}-${monthStr}-${dayStr}`; prop.propertyData.recentMajorRepairs = "Full Interior & Exterior Renovation"; prop.propertyData.renovationDescription = "Modernized kitchen, new flooring, fresh paint, roof inspection"; } }); return allProperties; } // --- NEW DATA (Users & Meetings) --- const MOCK_USERS = [ // Customers { id: 'c1', type: 'customer', username: 'alice', email: 'alice@example.com', password: 'password', name: 'Alice Customer', propertyId: 'P-2600' }, { id: 'c2', type: 'customer', username: 'bob', email: 'bob@example.com', password: 'password', name: 'Bob Buyer' }, { id: 'c3', type: 'customer', username: 'charlie', email: 'charlie@example.com', password: 'password', name: 'Charlie Client' }, // Field Agents (5 Agents) { id: 'e1', type: 'employee', empId: 'FA001', email: 'agent1@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Frank Agent' }, { id: 'e2', type: 'employee', empId: 'FA002', email: 'agent2@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Fiona Field' }, { id: 'e3', type: 'employee', empId: 'FA003', email: 'agent3@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Fred Flyer' }, { id: 'e4', type: 'employee', empId: 'FA004', email: 'agent4@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Felicity Fast' }, { id: 'e5', type: 'employee', empId: 'FA005', email: 'agent5@plano.com', password: 'password', role: 'FIELD_AGENT', name: 'Felix Fixer' }, // Admins (3 Admins) { id: 'a1', type: 'employee', empId: 'ADM01', email: 'admin@plano.com', password: 'password', role: 'ADMIN', name: 'Adam Admin' }, { id: 'a2', type: 'employee', empId: 'ADM02', email: 'admin2@plano.com', password: 'password', role: 'ADMIN', name: 'Amanda Manager' }, { id: 'a3', type: 'employee', empId: 'ADM03', email: 'admin3@plano.com', password: 'password', role: 'ADMIN', name: 'Arthur Director' }, // --- NEW ROLES (Owners Box) --- // Owner { id: 'own_001', type: 'owner', username: 'justin', // For login email: 'justin@lynkeduppro.com', password: 'password', // Keeping simple for dev role: 'OWNER', name: 'Justin Owner', companyName: 'LynkedUp Pro', phone: '972-555-1234' }, // Contractor { id: 'con_001', type: 'contractor', username: 'mike', email: 'mike@texasbuilders.com', password: 'password', role: 'CONTRACTOR', name: 'Mike Contractor', companyName: 'Texas Builders LLC', licenseNumber: 'TX-GC-123456', phone: '214-555-5678' }, // Subcontractor { id: 'sub_001', type: 'subcontractor', username: 'carlos', email: 'carlos@electricpro.com', password: 'password', role: 'SUBCONTRACTOR', name: 'Carlos Subcontractor', companyName: 'Electric Pro Services', tradeType: 'electrical', phone: '469-555-9012' }, // Vendor (Supplier) { id: 'ven_001', type: 'vendor', username: 'abc_supply', email: 'sales@abcsupply.com', password: 'password', role: 'VENDOR', name: 'ABC Supply Co.', companyName: 'ABC Supply', tradeType: 'materials', phone: '972-555-8888' }, // Vendor (Supplier) { id: 'ven_001', type: 'vendor', username: 'abc_supply', email: 'sales@abcsupply.com', password: 'password', role: 'VENDOR', name: 'ABC Supply Co.', companyName: 'ABC Supply', tradeType: 'materials', phone: '972-555-8888' } ]; // --- NEW MOCK SALES HISTORY FOR LEADERBOARD --- const MOCK_SALES_HISTORY = [ // Frank Agent (High Revenue, Low Volume - "The Sniper") { id: 'tx_1', agentId: 'e1', date: '2026-02-10', amount: 450000, status: 'closed_won' }, { id: 'tx_2', agentId: 'e1', date: '2026-02-05', amount: 520000, status: 'closed_won' }, { id: 'tx_3', agentId: 'e1', date: '2026-01-28', amount: 380000, status: 'closed_won' }, { id: 'tx_4', agentId: 'e1', date: '2026-02-01', amount: 0, status: 'closed_lost' }, // Lost deal // Fiona Field (High Volume, Mid Revenue - "The Grinder") { id: 'tx_5', agentId: 'e2', date: '2026-02-09', amount: 250000, status: 'closed_won' }, { id: 'tx_6', agentId: 'e2', date: '2026-02-08', amount: 240000, status: 'closed_won' }, { id: 'tx_7', agentId: 'e2', date: '2026-02-06', amount: 260000, status: 'closed_won' }, { id: 'tx_8', agentId: 'e2', date: '2026-01-30', amount: 230000, status: 'closed_won' }, { id: 'tx_9', agentId: 'e2', date: '2026-02-02', amount: 255000, status: 'closed_won' }, { id: 'tx_10', agentId: 'e2', date: '2026-01-25', amount: 0, status: 'closed_lost' }, { id: 'tx_11', agentId: 'e2', date: '2026-01-20', amount: 0, status: 'closed_lost' }, // Fred Flyer (New/Low Performance) { id: 'tx_12', agentId: 'e3', date: '2026-02-07', amount: 180000, status: 'closed_won' }, { id: 'tx_13', agentId: 'e3', date: '2026-02-03', amount: 0, status: 'closed_lost' }, { id: 'tx_14', agentId: 'e3', date: '2026-01-29', amount: 0, status: 'closed_lost' }, // Felicity Fast (Consistent) { id: 'tx_15', agentId: 'e4', date: '2026-02-10', amount: 310000, status: 'closed_won' }, { id: 'tx_16', agentId: 'e4', date: '2026-02-04', amount: 305000, status: 'closed_won' }, { id: 'tx_17', agentId: 'e4', date: '2026-01-27', amount: 295000, status: 'closed_won' }, // Felix Fixer (Closer - High Win Rate) { id: 'tx_18', agentId: 'e5', date: '2026-02-08', amount: 410000, status: 'closed_won' }, { id: 'tx_19', agentId: 'e5', date: '2026-02-01', amount: 390000, status: 'closed_won' }, ]; // Helper to generate meetings function generateMockMeetings() { const agents = ['e1', 'e2', 'e3', 'e4', 'e5']; const statuses = ['Scheduled', 'Rescheduled', 'Completed', 'Converted', 'Cancelled']; const meetings = []; let meetingIdCounter = 1; agents.forEach(agentId => { // Generate ~7 meetings per agent for (let i = 0; i < 7; i++) { const isPast = i < 4; // First 4 are past/completed/converted const status = isPast ? (Math.random() > 0.3 ? 'Completed' : (Math.random() > 0.5 ? 'Converted' : 'Cancelled')) // Past mix : (Math.random() > 0.3 ? 'Scheduled' : 'Rescheduled'); // Future mix // Date Logic const date = new Date(); date.setDate(date.getDate() + (isPast ? -1 * (i + 1) * 2 : (i + 1) * 2)); // Spread out days // Deal Value (only for relevant statuses) let dealValue = null; if (['Completed', 'Converted'].includes(status)) { dealValue = Math.floor(Math.random() * (50000 - 5000) + 5000); // 5k to 50k } // Time Logic (09:00 to 16:00) const hour = 9 + (i % 8); const timeString = `${hour.toString().padStart(2, '0')}:00`; // New Data Fields for Chatbot Context const issues = [ "Leaking skylight in master bath", "Missing shingles after recent storm", "Water spots on ceiling", "Gutter detachment on north side", "General roof inspection requested" ]; const comments = [ "Customer is worried about upcoming rain.", "Insurance adjuster already visited.", "Prefer morning appointments.", "Dog in backyard, please knock front door.", "Previous contractor did poor work." ]; meetings.push({ id: `m${meetingIdCounter++}`, agentId: agentId, customerName: REAL_NAMES[(meetingIdCounter * 3) % REAL_NAMES.length], // Pick random names propertyId: `P-${2600 + (meetingIdCounter * 2)}`, date: date.toISOString().split('T')[0], time: timeString, status: status, dealValue: dealValue, notes: status === 'Converted' ? 'Contract signed for full roof replacement.' : status === 'Rescheduled' ? 'Client verified availability.' : 'Standard consultation.', // New Fields issueDescription: issues[i % issues.length], customerComments: comments[i % comments.length], outcome: status === 'Converted' ? 'Signed Contract' : status === 'Completed' ? 'Quote Sent' : 'Pending' }); } }); // Generate Alice's Demo Meetings const aliceMeetings = [ // Upcoming { id: `m-alice-1`, agentId: 'e1', customerName: 'Alice Customer', customerId: 'c1', // Explicit ID link propertyId: 'P-Alice-1', date: new Date(Date.now() + 86400000 * 2).toISOString().split('T')[0], // 2 days from now time: '14:00', status: 'Scheduled', agentName: 'Frank Agent', notes: 'Initial Roof Inspection', issueDescription: 'Noticed missing shingles after last storm.', customerComments: 'Gate code is 1234.', outcome: 'Pending' }, // History { id: `m-alice-2`, agentId: 'e2', customerName: 'Alice Customer', customerId: 'c1', propertyId: 'P-Alice-1', date: new Date(Date.now() - 86400000 * 15).toISOString().split('T')[0], // 15 days ago time: '10:00', status: 'Completed', agentName: 'Fiona Field', dealValue: 1500, notes: 'Minor repairs completed. Payment received.', issueDescription: 'Leak in garage.', customerComments: 'Please call before arriving.', outcome: 'Repairs Completed' }, { id: `m-alice-3`, agentId: 'e1', customerName: 'Alice Customer', customerId: 'c1', propertyId: 'P-Alice-1', date: new Date(Date.now() - 86400000 * 45).toISOString().split('T')[0], // 45 days ago time: '11:00', status: 'Completed', agentName: 'Frank Agent', dealValue: 0, notes: 'Consultation - Quote provided.', issueDescription: 'General inspection.', customerComments: '', outcome: 'Quote Sent' } ]; meetings.push(...aliceMeetings); return meetings; } const MOCK_MEETINGS = generateMockMeetings(); // --- NEW OWNERS BOX DATA SCHEMAS --- const MOCK_PERSONNEL = [ { id: 'p1', name: 'Jesus Gonzales', role: 'W2_EMPLOYEE', email: 'jesus@lynkeduppro.com', phone: '214-555-0101', team: 'Sales Team A', manager: 'Justin Owner', sensitiveData: { ssn: '123-45-6789', bankAccount: '9876543210' }, compliance: { w9Signed: true, payPlanSigned: true, driverLicense: { uploaded: true, expirationDate: '2026-08-15', status: 'compliant' } }, status: 'active' }, { id: 'p2', name: 'Sarah Sales', role: '1099_SALES_REP', email: 'sarah@lynkeduppro.com', phone: '214-555-0102', team: 'Sales Team A', manager: 'Jesus Gonzales', sensitiveData: { ssn: '987-65-4321', bankAccount: '1234567890' }, compliance: { w9Signed: true, payPlanSigned: false, driverLicense: { uploaded: true, expirationDate: '2025-12-01', status: 'expiring' } }, status: 'active' } ]; const MOCK_VENDORS = [ { id: 'v1', vendorName: 'Texas Roofing Crew A', vendorType: 'roofing_crew', primaryContact: { name: 'Juan Perez', email: 'juan@texasroofing.com', phone: '469-555-0201' }, sensitiveData: { ein: '12-3456789', bankAccount: '1122334455' }, compliance: { w9: { uploaded: true, status: 'approved' }, subcontractorAgreement: { uploaded: true, status: 'approved' }, coi: { uploaded: true, expirationDate: '2026-06-30', status: 'compliant' } }, spend: { totalSpend: 154000, lastPayment: { date: '2026-02-01', amount: 12500 }, ytdSpend: 45000, pendingInvoices: 2500 }, performance: { rating: 4.8, onTimeRate: 0.95, jobsCompleted: 34, activeJobs: 2 }, status: 'active' }, { id: 'v2', vendorName: 'Sparky Electric', vendorType: 'electrical', primaryContact: { name: 'Mike Spark', email: 'mike@sparky.com', phone: '469-555-0202' }, sensitiveData: { ein: '98-7654321', bankAccount: '5544332211' }, compliance: { w9: { uploaded: true, status: 'approved' }, subcontractorAgreement: { uploaded: false, status: 'pending' }, coi: { uploaded: true, expirationDate: '2026-01-15', status: 'expired' } }, spend: { totalSpend: 4500, lastPayment: { date: '2025-12-15', amount: 800 }, ytdSpend: 0, pendingInvoices: 0 }, performance: { rating: 4.2, onTimeRate: 0.88, jobsCompleted: 5, activeJobs: 0 }, status: 'non_compliant' }, { id: 'v3', vendorName: 'ABC Supply Co.', vendorType: 'material_supplier', primaryContact: { name: 'Sales Desk', email: 'sales@abcsupply.com', phone: '972-555-8888' }, sensitiveData: { ein: '55-4433221', bankAccount: '9988776655' }, compliance: { w9: { uploaded: true, status: 'approved' }, subcontractorAgreement: { uploaded: true, status: 'approved' }, coi: { uploaded: true, status: 'compliant', expirationDate: '2027-01-01' } }, spend: { totalSpend: 250000, lastPayment: { date: '2026-02-10', amount: 15000 }, ytdSpend: 85000, pendingInvoices: 12000 }, performance: { rating: 5.0, onTimeRate: 0.99, jobsCompleted: 100, // Deliveries activeJobs: 5 }, status: 'active' } ]; const MOCK_ORDERS = [ { id: 'ORD-001', vendorId: 'v3', projectId: 'proj_001', projectAddress: '2604 Dunwick Dr, Plano, TX 75023', orderDate: '2026-02-10', dueDate: '2026-02-20', deliveryDate: null, status: 'shipped', items: [ { id: 'item_001', description: 'Roofing Shingles - Architectural', quantity: 50, unit: 'bundles', unitPrice: 45.00, total: 2250.00, specifications: 'GAF Timberline HDZ, Charcoal' }, { id: 'item_002', description: 'Roofing Underlayment', quantity: 10, unit: 'rolls', unitPrice: 85.00, total: 850.00, specifications: 'Synthetic, 10 sq per roll' } ], subtotal: 3100.00, tax: 248.00, total: 3348.00, deliveryAddress: { street: '2604 Dunwick Dr', city: 'Plano', state: 'TX', zip: '75023', instructions: 'Deliver to side yard, call foreman on arrival' }, timeline: [ { date: '2026-02-10', event: 'Order Placed', status: 'completed' }, { date: '2026-02-11', event: 'Order Confirmed', status: 'completed' }, { date: '2026-02-15', event: 'Shipped', status: 'completed' }, { date: '2026-02-20', event: 'Estimated Delivery', status: 'pending' } ], documents: [ { id: 'doc_001', type: 'PO', name: 'PO-2604-Dunwick.pdf', url: '#' }, { id: 'doc_002', type: 'Invoice', name: 'INV-001.pdf', url: '#' } ], notes: 'Rush order for weather window', contactPerson: 'John Smith', contactPhone: '469-555-0123' }, { id: 'ORD-002', vendorId: 'v3', projectId: 'proj_002', projectAddress: 'Plano, TX 75023', orderDate: '2026-02-05', dueDate: '2026-02-15', deliveryDate: '2026-02-14', status: 'delivered', items: [ { id: 'item_003', description: 'Gutter System - Seamless Aluminum', quantity: 150, unit: 'linear feet', unitPrice: 12.50, total: 1875.00, specifications: 'White, 5-inch K-style' } ], subtotal: 1875.00, tax: 150.00, total: 2025.00, deliveryAddress: { street: 'Gutter Replacement Project', city: 'Plano', state: 'TX', zip: '75023', instructions: 'Standard delivery' }, timeline: [ { date: '2026-02-05', event: 'Order Placed', status: 'completed' }, { date: '2026-02-06', event: 'Order Confirmed', status: 'completed' }, { date: '2026-02-12', event: 'Shipped', status: 'completed' }, { date: '2026-02-14', event: 'Delivered', status: 'completed' } ], documents: [ { id: 'doc_003', type: 'PO', name: 'PO-Gutter-Project.pdf', url: '#' }, { id: 'doc_004', type: 'Delivery Receipt', name: 'DR-002.pdf', url: '#' } ], notes: null, contactPerson: 'Mike Johnson', contactPhone: '469-555-0456' }, { id: 'ORD-003', vendorId: 'v3', projectId: 'proj_003', projectAddress: 'Dallas, TX 75201', orderDate: '2026-02-12', dueDate: '2026-02-25', deliveryDate: null, status: 'confirmed', items: [ { id: 'item_004', description: 'Roofing Nails - Coil', quantity: 20, unit: 'boxes', unitPrice: 35.00, total: 700.00, specifications: '1-1/4 inch, galvanized' }, { id: 'item_005', description: 'Flashing - Step', quantity: 100, unit: 'pieces', unitPrice: 4.50, total: 450.00, specifications: 'Aluminum, 8-inch' } ], subtotal: 1150.00, tax: 92.00, total: 1242.00, deliveryAddress: { street: 'Commercial Building', city: 'Dallas', state: 'TX', zip: '75201', instructions: 'Loading dock access required' }, timeline: [ { date: '2026-02-12', event: 'Order Placed', status: 'completed' }, { date: '2026-02-13', event: 'Order Confirmed', status: 'completed' }, { date: '2026-02-20', event: 'Estimated Ship Date', status: 'pending' }, { date: '2026-02-25', event: 'Estimated Delivery', status: 'pending' } ], documents: [ { id: 'doc_005', type: 'PO', name: 'PO-Dallas-Commercial.pdf', url: '#' } ], notes: 'Confirm delivery time 24hrs in advance', contactPerson: 'Sarah Williams', contactPhone: '214-555-0789' } ]; const MOCK_VENDOR_INVOICES = [ { id: 'INV-2026-001', vendorId: 'v3', orderId: 'ORD-002', projectId: 'proj_002', project: 'Plano, TX 75023', invoiceDate: '2026-02-14', dueDate: '2026-03-01', amount: 2025.00, status: 'paid', paymentTerms: 'Net 15', description: 'Gutter System Delivery' }, { id: 'INV-2026-002', vendorId: 'v3', orderId: 'ORD-001', projectId: 'proj_001', project: '2604 Dunwick Dr, Plano, TX 75023', invoiceDate: '2026-02-15', dueDate: '2026-03-02', amount: 3348.00, status: 'pending', paymentTerms: 'Net 15', description: 'Roofing Materials Delivery' }, { id: 'INV-2026-003', vendorId: 'v3', orderId: 'ORD-003', projectId: 'proj_003', project: 'Dallas, TX 75201', invoiceDate: '2026-02-13', dueDate: '2026-02-28', amount: 1242.00, status: 'pending', paymentTerms: 'Net 15', description: 'Roofing Accessories' }, { id: 'INV-2025-045', vendorId: 'v3', orderId: 'ORD-OLD-001', projectId: 'proj_old_001', project: 'Frisco, TX 75034', invoiceDate: '2025-12-20', dueDate: '2026-01-05', amount: 4500.00, status: 'paid', paymentTerms: 'Net 15', description: 'Complete Roofing Package' } ]; const MOCK_DOCUMENTS = [ { id: 'd1', entityId: 'p1', entityType: 'personnel', entityName: 'Jesus Gonzales', documentType: 'DL', fileName: 'JG_DriverLicense.pdf', fileUrl: '#', uploadedBy: 'Jesus Gonzales', uploadedAt: '2025-08-15', expirationDate: '2026-08-15', status: 'approved', aiConfidence: 0.98, reviewedBy: 'Admin', reviewedAt: '2025-08-16' }, { id: 'd2', entityId: 'v2', entityType: 'vendor', entityName: 'Sparky Electric', documentType: 'COI', fileName: 'Sparky_COI_2025.pdf', fileUrl: '#', uploadedBy: 'Mike Spark', uploadedAt: '2025-01-10', expirationDate: '2026-01-15', status: 'expired', aiConfidence: 0.95, reviewedBy: 'Admin', reviewedAt: '2025-01-11' }, { id: 'd3', entityId: 'p2', entityType: 'personnel', entityName: 'Sarah Sales', documentType: 'PAY_PLAN', fileName: 'Sarah_PayPlan_Signed.pdf', fileUrl: '#', uploadedBy: 'Sarah Sales', uploadedAt: '2026-02-14', expirationDate: null, status: 'pending_review', aiConfidence: 0.88, reviewedBy: null, reviewedAt: null } ]; const MOCK_PROJECTS = [ { id: 'proj_1', propertyId: 'P-2602', address: '2604 Dunwick Dr, Plano, TX 75023', contractorId: 'con_001', subcontractorIds: ['sub_001'], projectType: 'Roof Replacement', status: 'active', budget: 25000, spent: 12500, margin: 0.35, startDate: '2026-02-01', endDate: '2026-02-15', completionPercentage: 65, milestones: [ { id: 'ms_1', name: 'Material Delivery', dueDate: '2026-02-02', status: 'completed', assignedTo: 'ven_001' // Supplier }, { id: 'ms_2', name: 'Roof Tear-off', dueDate: '2026-02-04', status: 'completed', assignedTo: 'con_001' }, { id: 'ms_3', name: 'Dry-In Inspection', dueDate: '2026-02-06', status: 'completed', assignedTo: 'con_001' }, { id: 'ms_4', name: 'Shingle Install', dueDate: '2026-02-10', status: 'in_progress', assignedTo: 'con_001' }, { id: 'ms_5', name: 'Final Inspection', dueDate: '2026-02-14', status: 'pending', assignedTo: 'con_001' } ], invoices: [ { id: 'inv_1', amount: 10000, submittedBy: 'con_001', status: 'paid', dueDate: '2026-02-05', datePaid: '2026-02-06' }, { id: 'inv_2', amount: 5000, submittedBy: 'sub_001', status: 'pending', dueDate: '2026-02-10' }, { id: 'inv_3', amount: 12000, submittedBy: 'ven_001', // ABC Supply status: 'pending', dueDate: '2026-02-12' } ], documents: [ { name: 'Scope of Work', url: '#', type: 'contract' }, { name: 'Permit #22-401', url: '#', type: 'permit' }, { name: 'Material Order', url: '#', type: 'invoice' } ] }, { id: 'proj_2', propertyId: 'P-2608', address: '2617 Rothland Ln, Plano, TX 75023', contractorId: 'con_001', subcontractorIds: [], projectType: 'Gutter Replacement', status: 'scheduled', budget: 4500, spent: 0, margin: 0.40, startDate: '2026-02-20', endDate: '2026-02-22', completionPercentage: 0, milestones: [ { id: 'ms_2_1', name: 'Material Delivery', dueDate: '2026-02-19', status: 'pending', assignedTo: 'ven_001' }, { id: 'ms_2_2', name: 'Installation', dueDate: '2026-02-20', status: 'pending', assignedTo: 'con_001' } ], invoices: [], documents: [] } ]; // --- CONTEXT SETUP --- const MockStoreContext = createContext(); export const MockStoreProvider = ({ children }) => { const [properties, setProperties] = useState([]); const [users, setUsers] = useState(MOCK_USERS); const [meetings, setMeetings] = useState(MOCK_MEETINGS); const [salesHistory, setSalesHistory] = useState(MOCK_SALES_HISTORY); // New Data States const [personnel, setPersonnel] = useState(MOCK_PERSONNEL); const [vendors, setVendors] = useState(MOCK_VENDORS); const [documents, setDocuments] = useState(MOCK_DOCUMENTS); const [projects, setProjects] = useState(MOCK_PROJECTS); const [orders, setOrders] = useState(MOCK_ORDERS); const [vendorInvoices, setVendorInvoices] = useState(MOCK_VENDOR_INVOICES); // Initialize properties once useEffect(() => { const data = generateProperties(); setProperties(data); }, []); const updatePropertyStatus = (id, newStatus) => { setProperties(prev => prev.map(p => p.id === id ? { ...p, canvassingStatus: newStatus } : p )); }; const addMeeting = (meeting) => { setMeetings(prev => [...prev, { ...meeting, id: `m${prev.length + 1}` }]); }; // --- NEW ACTIONS for ADMIN CENTER --- const assignAgent = (propertyId, agentId) => { let found = false; setProperties(prev => prev.map(p => { if (p.id === propertyId || p.propertyData.propertyId === propertyId) { found = true; return { ...p, assignedAgentId: agentId, canvassingStatus: "Assigned" }; } return p; })); if (!found) { toast.error("Failed to assign agent: Property not found."); return false; } const agent = users.find(u => u.id === agentId); toast.success(`Agent ${agent ? agent.name : 'assigned'} linked to property.`); logger.info(`Property ${propertyId} assigned to Agent ${agentId}`); return true; }; const markLeadContacted = (propertyId) => { let found = false; setProperties(prev => prev.map(p => { if (p.id === propertyId || p.propertyData.propertyId === propertyId) { found = true; return { ...p, lastContactDate: new Date().toISOString() }; } return p; })); if (!found) { toast.error("Action failed: Property not found."); return false; } toast.success("Lead marked as contacted."); return true; }; const sendReminder = (propertyId) => { const prop = properties.find(p => p.id === propertyId || p.propertyData.propertyId === propertyId); if (!prop) { toast.error("Failed to send reminder: Client not found."); return false; } const name = prop?.ownerData?.fullName || "Client"; toast.success(`Reminder sent to ${name}`); logger.info(`Reminder sent to property ${propertyId}`); return true; }; return ( { setUsers(prev => prev.map(u => u.id === updatedUser.id ? updatedUser : u)); }, // Export new actions assignAgent, markLeadContacted, sendReminder }}> {children} ); }; export const useMockStore = () => useContext(MockStoreContext);