Spaces:
Build error
Build error
| 'use client' | |
| import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react' | |
| import type { Vendor } from '@/lib/vendors' | |
| import { vendorData } from '@/lib/vendors' | |
| // ββ Types ββ | |
| export interface ShortlistEntry { | |
| vendorId: string | |
| notes: string | |
| addedAt: string | |
| } | |
| export type ContractStatus = 'draft' | 'pending' | 'active' | 'amended' | 'disputed' | 'completed' | |
| export interface Contract { | |
| id: string | |
| vendorId: string | |
| clientName: string | |
| type: string | |
| status: ContractStatus | |
| amount: number | |
| date: string | |
| version: number | |
| sections: { title: string; content: string; items?: string[] }[] | |
| deliverables: { id: string; desc: string; due: string; qty: number; criteria: string; completed: boolean }[] | |
| } | |
| export interface AuditEntry { | |
| time: string | |
| action: string | |
| actor: string | |
| detail: string | |
| type: 'create'|'send'|'view'|'amend'|'update'|'sign'|'decline'|'system' | |
| } | |
| export interface PlannerTask { | |
| id: string | |
| task: string | |
| status: 'completed'|'active'|'upcoming' | |
| date: string | |
| category: string | |
| } | |
| export interface BudgetItem { | |
| category: string | |
| budgeted: number | |
| spent: number | |
| } | |
| // ββ Context ββ | |
| interface AppState { | |
| shortlist: ShortlistEntry[] | |
| contracts: Contract[] | |
| plannerTasks: PlannerTask[] | |
| budget: BudgetItem[] | |
| guestCount: number | |
| // Auth mock | |
| userRole: 'client'|'vendor'|'admin'|null | |
| userName: string|null | |
| // Actions | |
| toggleShortlist: (vendorId: string) => void | |
| removeFromShortlist: (vendorId: string) => void | |
| updateShortlistNotes: (vendorId: string, notes: string) => void | |
| getShortlistVendors: () => Vendor[] | |
| isShortlisted: (vendorId: string) => boolean | |
| updateContractStatus: (id: string, status: ContractStatus, auditAction: string) => void | |
| getContractById: (id: string) => Contract|undefined | |
| getContractAuditLog: (id: string) => AuditEntry[] | |
| addTask: (task: string, category: string) => void | |
| toggleTask: (id: string) => void | |
| updateGuestCount: (n: number) => void | |
| updateBudgetItem: (category: string, spent: number) => void | |
| setRole: (role: 'client'|'vendor'|'admin'|null) => void | |
| setUserName: (name: string) => void | |
| } | |
| const AppContext = createContext<AppState|null>(null) | |
| // ββ Default data ββ | |
| const defaultContracts: Contract[] = [ | |
| { | |
| id:'CTR-001', vendorId:'v1', clientName:'Amaya & Ruwan', type:'Full Day Venue', | |
| status:'active', amount:550000, date:'2026-11-01', version:1, | |
| sections:[ | |
| { title:'1. Venue Access', content:'Client has access to the Grand Atrium, garden, and bridal suite from 10:00 AM to 8:00 PM on the wedding date. Setup crew may enter at 8:00 AM.' }, | |
| { title:'2. Guest Count', content:'Maximum capacity is 350 guests. Final headcount must be confirmed 14 days before the event.' }, | |
| { title:'3. Payment Terms', content:'Total: Rs. 550,000. Deposit of Rs. 200,000 paid. Remaining Rs. 350,000 due 7 days before the event.' }, | |
| ], | |
| deliverables: [ | |
| { id:'d1', desc:'Grand Atrium access', due:'2027-01-15', qty:1, criteria:'Clean, setup complete by 10:00 AM', completed:false }, | |
| { id:'d2', desc:'Bridal suite access', due:'2027-01-15', qty:1, criteria:'Private room with AC, mirrors, refreshments', completed:false }, | |
| ], | |
| }, | |
| { | |
| id:'CTR-002', vendorId:'v2', clientName:'Amaya & Ruwan', type:'Full Day Photography', | |
| status:'pending', amount:150000, date:'2026-11-08', version:2, | |
| sections:[ | |
| { title:'1. Scope', content:'Full-day wedding photography coverage. Two photographers. Minimum 400 edited high-resolution images delivered within 30 days post-event.' }, | |
| { title:'2. Deliverables', items:['400+ edited images','Online gallery (12 months)','USB drive','30-page premium layflat album'] }, | |
| { title:'3. Payment', content:'Total: Rs. 150,000. Rs. 50,000 deposit due on signing. Rs. 100,000 balance due 7 days before event.' }, | |
| ], | |
| deliverables: [ | |
| { id:'d3', desc:'Edited high-resolution images', due:'2027-02-15', qty:400, criteria:'Color-corrected, professionally edited', completed:false }, | |
| { id:'d4', desc:'Online gallery', due:'2027-02-20', qty:1, criteria:'Password protected, accessible 12 months', completed:false }, | |
| { id:'d5', desc:'Premium layflat album', due:'2027-03-15', qty:1, criteria:'30 pages, premium paper, leather cover', completed:false }, | |
| ], | |
| }, | |
| { | |
| id:'CTR-003', vendorId:'v3', clientName:'Amaya & Ruwan', type:'Classic Florals', | |
| status:'draft', amount:120000, date:'2026-12-01', version:1, | |
| sections:[ | |
| { title:'1. Arrangements', content:'Bridal bouquet, 3 bridesmaid bouquets, ceremony arch, 15 table centerpieces, aisle petal decor.' }, | |
| { title:'2. Payment', content:'Total: Rs. 120,000. Rs. 40,000 deposit on signing. Balance due 14 days before event.' }, | |
| ], | |
| deliverables:[ | |
| { id:'d6', desc:'Bridal bouquet', due:'2027-01-15', qty:1, criteria:'Premium seasonal blooms, hand-tied', completed:false }, | |
| { id:'d7', desc:'Ceremony arch', due:'2027-01-15', qty:1, criteria:'Full floral coverage, stable structure', completed:false }, | |
| ], | |
| }, | |
| ] | |
| const defaultTasks: PlannerTask[] = [ | |
| { id:'t1', task:'Book venue', status:'completed', date:'2026-11-01', category:'Venue' }, | |
| { id:'t2', task:'Finalize guest list', status:'completed', date:'2026-11-05', category:'Planning' }, | |
| { id:'t3', task:'Sign photographer contract', status:'active', date:'2026-11-15', category:'Contract' }, | |
| { id:'t4', task:'Choose floral arrangements', status:'active', date:'2026-11-20', category:'DΓ©cor' }, | |
| { id:'t5', task:'Book caterer tasting', status:'upcoming', date:'2026-12-01', category:'Catering' }, | |
| { id:'t6', task:'Order wedding invitations', status:'upcoming', date:'2026-12-10', category:'Stationery' }, | |
| { id:'t7', task:'Final dress fitting', status:'upcoming', date:'2026-12-15', category:'Attire' }, | |
| { id:'t8', task:'Confirm DJ playlist', status:'upcoming', date:'2026-12-20', category:'Music' }, | |
| ] | |
| const defaultBudget: BudgetItem[] = [ | |
| { category:'Venue', budgeted:550000, spent:400000 }, | |
| { category:'Photography', budgeted:150000, spent:50000 }, | |
| { category:'Catering', budgeted:250000, spent:25000 }, | |
| { category:'Florals', budgeted:120000, spent:0 }, | |
| { category:'Music & DJ', budgeted:75000, spent:75000 }, | |
| { category:'Attire', budgeted:120000, spent:45000 }, | |
| ] | |
| // ββ Audit log generator ββ | |
| function generateAudit(id: string): AuditEntry[] { | |
| const contract = defaultContracts.find(c=>c.id===id) | |
| if (!contract) return [] | |
| const base: AuditEntry[] = [ | |
| { time:'2026-11-08 14:32', action:'Contract created', actor:'Evermore', detail:'Initial contract generated', type:'create' }, | |
| { time:'2026-11-08 14:33', action:'Sent to client', actor:'Evermore', detail:'Sent via platform', type:'send' }, | |
| { time:'2026-11-08 15:10', action:'Viewed by client', actor:'You', detail:'Viewed on web app', type:'view' }, | |
| ] | |
| if (contract.version > 1) { | |
| base.push({ time:'2026-11-09 09:45', action:'Amendment requested', actor:'You', detail:'Added 100 images to deliverable', type:'amend' }) | |
| base.push({ time:'2026-11-09 11:20', action:'Vendor responded', actor:'Evermore', detail:'Accepted amendment. Version updated to v2', type:'update' }) | |
| base.push({ time:'2026-11-09 11:20', action:'Version updated', actor:'System', detail:'Previous version archived', type:'system' }) | |
| } | |
| if (contract.status === 'active') { | |
| base.push({ time:'2026-11-15 10:00', action:'Contract signed', actor:'You', detail:'E-signed. IP: 192.168.1.1, UA: Chrome/120', type:'sign' }) | |
| base.push({ time:'2026-11-15 10:05', action:'Countersigned', actor:'Evermore', detail:'Vendor signed. Contract now active', type:'sign' }) | |
| } | |
| return base | |
| } | |
| const auditCache: Record<string, AuditEntry[]> = {} | |
| defaultContracts.forEach(c => { auditCache[c.id] = generateAudit(c.id) }) | |
| // ββ Provider ββ | |
| function loadFromStorage<T>(key: string, fallback: T): T { | |
| if (typeof window === 'undefined') return fallback | |
| try { | |
| const stored = localStorage.getItem(key) | |
| return stored ? JSON.parse(stored) : fallback | |
| } catch { return fallback } | |
| } | |
| export function AppProvider({ children }: { children: ReactNode }) { | |
| const [shortlist, setShortlist] = useState<ShortlistEntry[]>([]) | |
| const [contracts, setContracts] = useState<Contract[]>(defaultContracts) | |
| const [plannerTasks, setPlannerTasks] = useState<PlannerTask[]>(defaultTasks) | |
| const [budget, setBudget] = useState<BudgetItem[]>(defaultBudget) | |
| const [guestCount, setGuestCount] = useState(180) | |
| const [userRole, setUserRole] = useState<'client'|'vendor'|'admin'|null>(null) | |
| const [userName, setUserNameState] = useState<string|null>(null) | |
| // Load from localStorage on mount | |
| useEffect(() => { | |
| setShortlist(loadFromStorage<ShortlistEntry[]>('evermore_shortlist', [])) | |
| setPlannerTasks(loadFromStorage<PlannerTask[]>('evermore_tasks', defaultTasks)) | |
| setBudget(loadFromStorage<BudgetItem[]>('evermore_budget', defaultBudget)) | |
| setGuestCount(loadFromStorage<number>('evermore_guests', 180)) | |
| setUserRole(loadFromStorage<'client'|'vendor'|'admin'|null>('evermore_role', null)) | |
| setUserNameState(loadFromStorage<string|null>('evermore_name', null)) | |
| }, []) | |
| // Persist | |
| useEffect(() => { localStorage.setItem('evermore_shortlist', JSON.stringify(shortlist)) }, [shortlist]) | |
| useEffect(() => { localStorage.setItem('evermore_tasks', JSON.stringify(plannerTasks)) }, [plannerTasks]) | |
| useEffect(() => { localStorage.setItem('evermore_budget', JSON.stringify(budget)) }, [budget]) | |
| useEffect(() => { localStorage.setItem('evermore_guests', JSON.stringify(guestCount)) }, [guestCount]) | |
| useEffect(() => { if (userRole !== null) localStorage.setItem('evermore_role', userRole); else localStorage.removeItem('evermore_role') }, [userRole]) | |
| useEffect(() => { if (userName) localStorage.setItem('evermore_name', userName); else localStorage.removeItem('evermore_name') }, [userName]) | |
| const toggleShortlist = useCallback((vendorId: string) => { | |
| setShortlist(prev => { | |
| if (prev.find(s => s.vendorId === vendorId)) { | |
| return prev.filter(s => s.vendorId !== vendorId) | |
| } | |
| return [...prev, { vendorId, notes: '', addedAt: new Date().toISOString() }] | |
| }) | |
| }, []) | |
| const removeFromShortlist = useCallback((vendorId: string) => { | |
| setShortlist(prev => prev.filter(s => s.vendorId !== vendorId)) | |
| }, []) | |
| const updateShortlistNotes = useCallback((vendorId: string, notes: string) => { | |
| setShortlist(prev => prev.map(s => s.vendorId === vendorId ? { ...s, notes } : s)) | |
| }, []) | |
| const getShortlistVendors = useCallback(() => { | |
| return shortlist.map(s => vendorData.find(v => v.id === s.vendorId)).filter(Boolean) as Vendor[] | |
| }, [shortlist]) | |
| const isShortlisted = useCallback((vendorId: string) => { | |
| return shortlist.some(s => s.vendorId === vendorId) | |
| }, [shortlist]) | |
| const updateContractStatus = useCallback((id: string, status: ContractStatus, auditAction: string) => { | |
| setContracts(prev => prev.map(c => c.id === id ? { ...c, status } : c)) | |
| if (!auditCache[id]) auditCache[id] = [] | |
| auditCache[id].push({ | |
| time: new Date().toLocaleString('en-US', { month:'short', day:'numeric', year:'numeric', hour:'2-digit', minute:'2-digit' }), | |
| action: auditAction, | |
| actor: 'You', | |
| detail: `Contract status changed to: ${status}`, | |
| type: status === 'active' ? 'sign' : status === 'disputed' ? 'amend' : 'update', | |
| }) | |
| }, []) | |
| const getContractById = useCallback((id: string) => contracts.find(c => c.id === id), [contracts]) | |
| const getContractAuditLog = useCallback((id: string) => auditCache[id] || [], []) | |
| const addTask = useCallback((task: string, category: string) => { | |
| setPlannerTasks(prev => [...prev, { | |
| id: 't' + Date.now().toString(36), | |
| task, category, status: 'upcoming', | |
| date: new Date().toLocaleDateString('en-US', { month:'short', day:'numeric' }), | |
| }]) | |
| }, []) | |
| const toggleTask = useCallback((id: string) => { | |
| setPlannerTasks(prev => prev.map(t => t.id === id ? { | |
| ...t, | |
| status: t.status === 'completed' ? 'active' : 'completed', | |
| } : t)) | |
| }, []) | |
| const updateGuestCount = useCallback((n: number) => setGuestCount(n), []) | |
| const updateBudgetItem = useCallback((category: string, spent: number) => { | |
| setBudget(prev => prev.map(b => b.category === category ? { ...b, spent } : b)) | |
| }, []) | |
| const setRole = useCallback((role: 'client'|'vendor'|'admin'|null) => setUserRole(role), []) | |
| const setUserName = useCallback((name: string) => setUserNameState(name), []) | |
| return ( | |
| <AppContext.Provider value={{ | |
| shortlist, contracts, plannerTasks, budget, guestCount, userRole, userName, | |
| toggleShortlist, removeFromShortlist, updateShortlistNotes, getShortlistVendors, isShortlisted, | |
| updateContractStatus, getContractById, getContractAuditLog, | |
| addTask, toggleTask, updateGuestCount, updateBudgetItem, | |
| setRole, setUserName, | |
| }}> | |
| {children} | |
| </AppContext.Provider> | |
| ) | |
| } | |
| export function useApp(): AppState { | |
| const ctx = useContext(AppContext) | |
| if (!ctx) throw new Error('useApp must be used inside AppProvider') | |
| return ctx | |
| } |