2eaac6b84a
- 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
1543 lines
63 KiB
React
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);
|