Spaces:
Build error
Build error
Upload src/lib/app-context.tsx
Browse files- 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 |
+
}
|