imeshuek commited on
Commit
f4158d4
·
verified ·
1 Parent(s): 0ebda10

Upload src/lib/app-context.tsx

Browse files
Files changed (1) hide show
  1. src/lib/app-context.tsx +291 -0
src/lib/app-context.tsx ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'
3
+ import type { Vendor } from '@/lib/vendors'
4
+ import { vendorData } from '@/lib/vendors'
5
+
6
+ // ── Types ──
7
+
8
+ export interface ShortlistEntry {
9
+ vendorId: string
10
+ notes: string
11
+ addedAt: string
12
+ }
13
+
14
+ export type ContractStatus = 'draft' | 'pending' | 'active' | 'amended' | 'disputed' | 'completed'
15
+
16
+ export interface Contract {
17
+ id: string
18
+ vendorId: string
19
+ clientName: string
20
+ type: string
21
+ status: ContractStatus
22
+ amount: number
23
+ date: string
24
+ version: number
25
+ sections: { title: string; content: string; items?: string[] }[]
26
+ deliverables: { id: string; desc: string; due: string; qty: number; criteria: string; completed: boolean }[]
27
+ }
28
+
29
+ export interface AuditEntry {
30
+ time: string
31
+ action: string
32
+ actor: string
33
+ detail: string
34
+ type: 'create'|'send'|'view'|'amend'|'update'|'sign'|'decline'|'system'
35
+ }
36
+
37
+ export interface PlannerTask {
38
+ id: string
39
+ task: string
40
+ status: 'completed'|'active'|'upcoming'
41
+ date: string
42
+ category: string
43
+ }
44
+
45
+ export interface BudgetItem {
46
+ category: string
47
+ budgeted: number
48
+ spent: number
49
+ }
50
+
51
+ // ── Context ──
52
+
53
+ interface AppState {
54
+ shortlist: ShortlistEntry[]
55
+ contracts: Contract[]
56
+ plannerTasks: PlannerTask[]
57
+ budget: BudgetItem[]
58
+ guestCount: number
59
+ // Auth mock
60
+ userRole: 'client'|'vendor'|'admin'|null
61
+ userName: string|null
62
+
63
+ // Actions
64
+ toggleShortlist: (vendorId: string) => void
65
+ removeFromShortlist: (vendorId: string) => void
66
+ updateShortlistNotes: (vendorId: string, notes: string) => void
67
+ getShortlistVendors: () => Vendor[]
68
+ isShortlisted: (vendorId: string) => boolean
69
+
70
+ updateContractStatus: (id: string, status: ContractStatus, auditAction: string) => void
71
+ getContractById: (id: string) => Contract|undefined
72
+ getContractAuditLog: (id: string) => AuditEntry[]
73
+
74
+ addTask: (task: string, category: string) => void
75
+ toggleTask: (id: string) => void
76
+ updateGuestCount: (n: number) => void
77
+ updateBudgetItem: (category: string, spent: number) => void
78
+
79
+ setRole: (role: 'client'|'vendor'|'admin'|null) => void
80
+ setUserName: (name: string) => void
81
+ }
82
+
83
+ const AppContext = createContext<AppState|null>(null)
84
+
85
+ // ── Default data ──
86
+
87
+ const defaultContracts: Contract[] = [
88
+ {
89
+ id:'CTR-001', vendorId:'v1', clientName:'Amaya & Ruwan', type:'Full Day Venue',
90
+ status:'active', amount:550000, date:'2026-11-01', version:1,
91
+ sections:[
92
+ { 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.' },
93
+ { title:'2. Guest Count', content:'Maximum capacity is 350 guests. Final headcount must be confirmed 14 days before the event.' },
94
+ { 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.' },
95
+ ],
96
+ deliverables: [
97
+ { id:'d1', desc:'Grand Atrium access', due:'2027-01-15', qty:1, criteria:'Clean, setup complete by 10:00 AM', completed:false },
98
+ { id:'d2', desc:'Bridal suite access', due:'2027-01-15', qty:1, criteria:'Private room with AC, mirrors, refreshments', completed:false },
99
+ ],
100
+ },
101
+ {
102
+ id:'CTR-002', vendorId:'v2', clientName:'Amaya & Ruwan', type:'Full Day Photography',
103
+ status:'pending', amount:150000, date:'2026-11-08', version:2,
104
+ sections:[
105
+ { title:'1. Scope', content:'Full-day wedding photography coverage. Two photographers. Minimum 400 edited high-resolution images delivered within 30 days post-event.' },
106
+ { title:'2. Deliverables', items:['400+ edited images','Online gallery (12 months)','USB drive','30-page premium layflat album'] },
107
+ { title:'3. Payment', content:'Total: Rs. 150,000. Rs. 50,000 deposit due on signing. Rs. 100,000 balance due 7 days before event.' },
108
+ ],
109
+ deliverables: [
110
+ { id:'d3', desc:'Edited high-resolution images', due:'2027-02-15', qty:400, criteria:'Color-corrected, professionally edited', completed:false },
111
+ { id:'d4', desc:'Online gallery', due:'2027-02-20', qty:1, criteria:'Password protected, accessible 12 months', completed:false },
112
+ { id:'d5', desc:'Premium layflat album', due:'2027-03-15', qty:1, criteria:'30 pages, premium paper, leather cover', completed:false },
113
+ ],
114
+ },
115
+ {
116
+ id:'CTR-003', vendorId:'v3', clientName:'Amaya & Ruwan', type:'Classic Florals',
117
+ status:'draft', amount:120000, date:'2026-12-01', version:1,
118
+ sections:[
119
+ { title:'1. Arrangements', content:'Bridal bouquet, 3 bridesmaid bouquets, ceremony arch, 15 table centerpieces, aisle petal decor.' },
120
+ { title:'2. Payment', content:'Total: Rs. 120,000. Rs. 40,000 deposit on signing. Balance due 14 days before event.' },
121
+ ],
122
+ deliverables:[
123
+ { id:'d6', desc:'Bridal bouquet', due:'2027-01-15', qty:1, criteria:'Premium seasonal blooms, hand-tied', completed:false },
124
+ { id:'d7', desc:'Ceremony arch', due:'2027-01-15', qty:1, criteria:'Full floral coverage, stable structure', completed:false },
125
+ ],
126
+ },
127
+ ]
128
+
129
+ const defaultTasks: PlannerTask[] = [
130
+ { id:'t1', task:'Book venue', status:'completed', date:'2026-11-01', category:'Venue' },
131
+ { id:'t2', task:'Finalize guest list', status:'completed', date:'2026-11-05', category:'Planning' },
132
+ { id:'t3', task:'Sign photographer contract', status:'active', date:'2026-11-15', category:'Contract' },
133
+ { id:'t4', task:'Choose floral arrangements', status:'active', date:'2026-11-20', category:'Décor' },
134
+ { id:'t5', task:'Book caterer tasting', status:'upcoming', date:'2026-12-01', category:'Catering' },
135
+ { id:'t6', task:'Order wedding invitations', status:'upcoming', date:'2026-12-10', category:'Stationery' },
136
+ { id:'t7', task:'Final dress fitting', status:'upcoming', date:'2026-12-15', category:'Attire' },
137
+ { id:'t8', task:'Confirm DJ playlist', status:'upcoming', date:'2026-12-20', category:'Music' },
138
+ ]
139
+
140
+ const defaultBudget: BudgetItem[] = [
141
+ { category:'Venue', budgeted:550000, spent:400000 },
142
+ { category:'Photography', budgeted:150000, spent:50000 },
143
+ { category:'Catering', budgeted:250000, spent:25000 },
144
+ { category:'Florals', budgeted:120000, spent:0 },
145
+ { category:'Music & DJ', budgeted:75000, spent:75000 },
146
+ { category:'Attire', budgeted:120000, spent:45000 },
147
+ ]
148
+
149
+ // ── Audit log generator ──
150
+
151
+ function generateAudit(id: string): AuditEntry[] {
152
+ const contract = defaultContracts.find(c=>c.id===id)
153
+ if (!contract) return []
154
+ const base: AuditEntry[] = [
155
+ { time:'2026-11-08 14:32', action:'Contract created', actor:'Evermore', detail:'Initial contract generated', type:'create' },
156
+ { time:'2026-11-08 14:33', action:'Sent to client', actor:'Evermore', detail:'Sent via platform', type:'send' },
157
+ { time:'2026-11-08 15:10', action:'Viewed by client', actor:'You', detail:'Viewed on web app', type:'view' },
158
+ ]
159
+ if (contract.version > 1) {
160
+ base.push({ time:'2026-11-09 09:45', action:'Amendment requested', actor:'You', detail:'Added 100 images to deliverable', type:'amend' })
161
+ base.push({ time:'2026-11-09 11:20', action:'Vendor responded', actor:'Evermore', detail:'Accepted amendment. Version updated to v2', type:'update' })
162
+ base.push({ time:'2026-11-09 11:20', action:'Version updated', actor:'System', detail:'Previous version archived', type:'system' })
163
+ }
164
+ if (contract.status === 'active') {
165
+ 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' })
166
+ base.push({ time:'2026-11-15 10:05', action:'Countersigned', actor:'Evermore', detail:'Vendor signed. Contract now active', type:'sign' })
167
+ }
168
+ return base
169
+ }
170
+
171
+ const auditCache: Record<string, AuditEntry[]> = {}
172
+ defaultContracts.forEach(c => { auditCache[c.id] = generateAudit(c.id) })
173
+
174
+ // ── Provider ──
175
+
176
+ function loadFromStorage<T>(key: string, fallback: T): T {
177
+ if (typeof window === 'undefined') return fallback
178
+ try {
179
+ const stored = localStorage.getItem(key)
180
+ return stored ? JSON.parse(stored) : fallback
181
+ } catch { return fallback }
182
+ }
183
+
184
+ export function AppProvider({ children }: { children: ReactNode }) {
185
+ const [shortlist, setShortlist] = useState<ShortlistEntry[]>([])
186
+ const [contracts, setContracts] = useState<Contract[]>(defaultContracts)
187
+ const [plannerTasks, setPlannerTasks] = useState<PlannerTask[]>(defaultTasks)
188
+ const [budget, setBudget] = useState<BudgetItem[]>(defaultBudget)
189
+ const [guestCount, setGuestCount] = useState(180)
190
+ const [userRole, setUserRole] = useState<'client'|'vendor'|'admin'|null>(null)
191
+ const [userName, setUserNameState] = useState<string|null>(null)
192
+
193
+ // Load from localStorage on mount
194
+ useEffect(() => {
195
+ setShortlist(loadFromStorage<ShortlistEntry[]>('evermore_shortlist', []))
196
+ setPlannerTasks(loadFromStorage<PlannerTask[]>('evermore_tasks', defaultTasks))
197
+ setBudget(loadFromStorage<BudgetItem[]>('evermore_budget', defaultBudget))
198
+ setGuestCount(loadFromStorage<number>('evermore_guests', 180))
199
+ setUserRole(loadFromStorage<'client'|'vendor'|'admin'|null>('evermore_role', null))
200
+ setUserNameState(loadFromStorage<string|null>('evermore_name', null))
201
+ }, [])
202
+
203
+ // Persist
204
+ useEffect(() => { localStorage.setItem('evermore_shortlist', JSON.stringify(shortlist)) }, [shortlist])
205
+ useEffect(() => { localStorage.setItem('evermore_tasks', JSON.stringify(plannerTasks)) }, [plannerTasks])
206
+ useEffect(() => { localStorage.setItem('evermore_budget', JSON.stringify(budget)) }, [budget])
207
+ useEffect(() => { localStorage.setItem('evermore_guests', JSON.stringify(guestCount)) }, [guestCount])
208
+ useEffect(() => { if (userRole !== null) localStorage.setItem('evermore_role', userRole); else localStorage.removeItem('evermore_role') }, [userRole])
209
+ useEffect(() => { if (userName) localStorage.setItem('evermore_name', userName); else localStorage.removeItem('evermore_name') }, [userName])
210
+
211
+ const toggleShortlist = useCallback((vendorId: string) => {
212
+ setShortlist(prev => {
213
+ if (prev.find(s => s.vendorId === vendorId)) {
214
+ return prev.filter(s => s.vendorId !== vendorId)
215
+ }
216
+ return [...prev, { vendorId, notes: '', addedAt: new Date().toISOString() }]
217
+ })
218
+ }, [])
219
+
220
+ const removeFromShortlist = useCallback((vendorId: string) => {
221
+ setShortlist(prev => prev.filter(s => s.vendorId !== vendorId))
222
+ }, [])
223
+
224
+ const updateShortlistNotes = useCallback((vendorId: string, notes: string) => {
225
+ setShortlist(prev => prev.map(s => s.vendorId === vendorId ? { ...s, notes } : s))
226
+ }, [])
227
+
228
+ const getShortlistVendors = useCallback(() => {
229
+ return shortlist.map(s => vendorData.find(v => v.id === s.vendorId)).filter(Boolean) as Vendor[]
230
+ }, [shortlist])
231
+
232
+ const isShortlisted = useCallback((vendorId: string) => {
233
+ return shortlist.some(s => s.vendorId === vendorId)
234
+ }, [shortlist])
235
+
236
+ const updateContractStatus = useCallback((id: string, status: ContractStatus, auditAction: string) => {
237
+ setContracts(prev => prev.map(c => c.id === id ? { ...c, status } : c))
238
+ if (!auditCache[id]) auditCache[id] = []
239
+ auditCache[id].push({
240
+ time: new Date().toLocaleString('en-US', { month:'short', day:'numeric', year:'numeric', hour:'2-digit', minute:'2-digit' }),
241
+ action: auditAction,
242
+ actor: 'You',
243
+ detail: `Contract status changed to: ${status}`,
244
+ type: status === 'active' ? 'sign' : status === 'disputed' ? 'amend' : 'update',
245
+ })
246
+ }, [])
247
+
248
+ const getContractById = useCallback((id: string) => contracts.find(c => c.id === id), [contracts])
249
+ const getContractAuditLog = useCallback((id: string) => auditCache[id] || [], [])
250
+
251
+ const addTask = useCallback((task: string, category: string) => {
252
+ setPlannerTasks(prev => [...prev, {
253
+ id: 't' + Date.now().toString(36),
254
+ task, category, status: 'upcoming',
255
+ date: new Date().toLocaleDateString('en-US', { month:'short', day:'numeric' }),
256
+ }])
257
+ }, [])
258
+
259
+ const toggleTask = useCallback((id: string) => {
260
+ setPlannerTasks(prev => prev.map(t => t.id === id ? {
261
+ ...t,
262
+ status: t.status === 'completed' ? 'active' : 'completed',
263
+ } : t))
264
+ }, [])
265
+
266
+ const updateGuestCount = useCallback((n: number) => setGuestCount(n), [])
267
+ const updateBudgetItem = useCallback((category: string, spent: number) => {
268
+ setBudget(prev => prev.map(b => b.category === category ? { ...b, spent } : b))
269
+ }, [])
270
+
271
+ const setRole = useCallback((role: 'client'|'vendor'|'admin'|null) => setUserRole(role), [])
272
+ const setUserName = useCallback((name: string) => setUserNameState(name), [])
273
+
274
+ return (
275
+ <AppContext.Provider value={{
276
+ shortlist, contracts, plannerTasks, budget, guestCount, userRole, userName,
277
+ toggleShortlist, removeFromShortlist, updateShortlistNotes, getShortlistVendors, isShortlisted,
278
+ updateContractStatus, getContractById, getContractAuditLog,
279
+ addTask, toggleTask, updateGuestCount, updateBudgetItem,
280
+ setRole, setUserName,
281
+ }}>
282
+ {children}
283
+ </AppContext.Provider>
284
+ )
285
+ }
286
+
287
+ export function useApp(): AppState {
288
+ const ctx = useContext(AppContext)
289
+ if (!ctx) throw new Error('useApp must be used inside AppProvider')
290
+ return ctx
291
+ }