Files
LynkedUpPro_CRM/src/data/mockStore.jsx
T
Satyam 2eaac6b84a feat: Multi-role platform expansion, mobile nav redesign, and UI polish
- Add Owner, Contractor, Vendor, Subcontractor dashboards and role-based routing
- Owner now has superuser access to all Admin pages (dashboard, schedule, leaderboard)
- Redesign landing page mobile menu as slide-in sidebar (replaces broken Framer Motion overlay)
- Add body scroll lock to app sidebar for mobile consistency
- Fix Team Schedule to match Admin Dashboard design language (ambient glows, gradient header, SpotlightCard, zinc palette)
- Fix login page tab overflow — use abbreviated labels and grid layout for 6 role tabs
- Fix Recharts ResponsiveContainer warnings — replace height="100%" with fixed pixel heights
- Fix lightbox image viewer — unified nav bar, touch swipe support, no more overlapping text
- Add AI Assistant page, People/Vendor/Document management for Owner role
- Expand mock data store with contractor, vendor, and subcontractor data
2026-02-17 01:23:06 +05:30

1543 lines
63 KiB
React

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 (
<MockStoreContext.Provider value={{
properties,
users,
meetings,
salesHistory, // Exported for Leaderboard
// New Owners Box Data
personnel,
vendors,
documents,
projects,
orders,
vendorInvoices,
updatePropertyStatus,
addMeeting,
updateUser: (updatedUser) => {
setUsers(prev => prev.map(u => u.id === updatedUser.id ? updatedUser : u));
},
// Export new actions
assignAgent,
markLeadContacted,
sendReminder
}}>
{children}
</MockStoreContext.Provider>
);
};
export const useMockStore = () => useContext(MockStoreContext);