import { action, type AddIntegrity, computed, Default, handler, NAME, nonPrivateRandom, pattern, type PerSpace, type RequiresIntegrity, safeDateNow, Stream, UI, type VNode, wish, Writable, } from "commonfabric"; import { type AdminManagerCredential, adminManagerCredentialIsActive, adminRegistryEntries, type EmptyAdminRegistryValue, } from "../../cfc/admin/mod.ts"; import { formatVehicle, modelsForMake, normalizeVehicle, normalizeVehicles, US_STATES, type Vehicle, VEHICLE_COLORS, VEHICLE_MAKES, } from "../../vehicles.ts"; export type { Vehicle }; // ============================================================ // Domain Types // ============================================================ export interface ParkingSpot { spotNumber: string; label: string; notes: string; active: boolean; } export type CommuteMode = "drive" | "transit" | "bike" | "wfh" | "other"; export interface Person { name: string; email: string; commuteMode: CommuteMode; spotPreferences: string[]; defaultSpot: string; priorityRank: number; vehicles?: Vehicle[]; } export type RequestStatus = "pending" | "allocated" | "denied" | "cancelled"; export interface SpotRequest { id: string; personName: string; date: string; status: RequestStatus; assignedSpot: string; autoAllocated: boolean; } export const PARKING_ADMIN_INTEGRITY = "parking-admin" as const; export const PARKING_ADMIN_MANAGER_INTEGRITY = "parking-admin-manager" as const; export interface ParkingAdminSubject { personName: string; } export interface ParkingAdminRoleAssignment { subject: ParkingAdminSubject; displayName: string; } export type ParkingAdminRole = AddIntegrity< ParkingAdminRoleAssignment, readonly [typeof PARKING_ADMIN_INTEGRITY] >; export type ParkingAdminManagerCredential = AdminManagerCredential< typeof PARKING_ADMIN_MANAGER_INTEGRITY >; export type ParkingAdminList = RequiresIntegrity< ParkingAdminRole[], readonly [typeof PARKING_ADMIN_MANAGER_INTEGRITY] >; export interface ParkingAdminRegistryStoredValue { admins?: ParkingAdminList; } export type ParkingAdminRegistryValue = | ParkingAdminRegistryStoredValue | Default; export type ParkingAdminRegistryCell = Writable; export type ParkingAdminManagerCredentialCell = Writable< ParkingAdminManagerCredential | null >; export type ParkingSpotList = RequiresIntegrity< ParkingSpot[], readonly [typeof PARKING_ADMIN_INTEGRITY] >; type SpotsCell = Writable< | ParkingSpotList | Default<[ { spotNumber: "1"; label: "Near entrance"; notes: ""; active: true }, { spotNumber: "5"; label: ""; notes: ""; active: true }, { spotNumber: "12"; label: "Compact only"; notes: "Tight, no large vehicles"; active: true; }, ]> >; type PeopleCell = Writable>; type RequestsCell = Writable>; // ============================================================ // Pattern I/O Types // ============================================================ export interface ParkingCoordinatorInput { spots?: PerSpace; people?: PerSpace; requests?: PerSpace; adminRegistry?: PerSpace; } export interface ParkingCoordinatorOutput { [NAME]: string; [UI]: VNode; spots: ParkingSpot[]; people: Person[]; requests: SpotRequest[]; adminMode: boolean; selectedPersonName: string; requestDate: string; requestResult: string; adminRegistry: PerSpace; currentPersonIsAdmin: boolean; currentUserCanManageAdmins: boolean; enableAdminManager: Stream; togglePersonAdmin: Stream<{ name: string }>; toggleAdminMode: Stream; submitRequest: Stream<{ personName: string; date: string }>; cancelRequest: Stream<{ requestId: string }>; addPerson: Stream<{ name: string; email: string; commuteMode: CommuteMode; priorityRank: number; defaultSpot: string; preferences: string; vehicles?: Vehicle[]; }>; editPerson: Stream<{ originalName: string; name: string; email: string; commuteMode: CommuteMode; priorityRank: number; defaultSpot: string; preferences: string; vehicles?: Vehicle[]; }>; removePerson: Stream<{ name: string }>; movePersonUp: Stream<{ name: string }>; movePersonDown: Stream<{ name: string }>; addSpot: Stream<{ spotNumber: string; label: string; notes: string }>; editSpot: Stream< { originalNumber: string; spotNumber: string; label: string; notes: string; active: boolean; } >; removeSpot: Stream<{ spotNumber: string }>; adminOverride: Stream< { spotNumber: string; date: string; personName: string } >; } // ============================================================ // Utilities // ============================================================ const toLocalDateStr = (ts: number): string => { const d = new Date(ts); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${ String(d.getDate()).padStart(2, "0") }`; }; const getWeekDates = (todayStr: string): string[] => { const dates: string[] = []; for (let i = 0; i < 7; i++) { const d = new Date(todayStr + "T00:00:00"); d.setDate(d.getDate() + i); dates.push(toLocalDateStr(d.getTime())); } return dates; }; const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const MONTH_NAMES = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; const formatDateShort = ( dateStr: string, ): { shortName: string; dayNum: string; monthLabel: string } => { const d = new Date(dateStr + "T00:00:00"); return { shortName: DAY_NAMES[d.getDay()], dayNum: String(d.getDate()), monthLabel: MONTH_NAMES[d.getMonth()], }; }; const formatDateDisplay = (dateStr: string): string => { const { shortName, dayNum, monthLabel } = formatDateShort(dateStr); return `${shortName} ${monthLabel} ${dayNum}`; }; const genId = (): string => `req-${safeDateNow()}-${nonPrivateRandom().toString(36).slice(2, 10)}`; const parsePreferences = (s: string | null | undefined): string[] => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean); // Makeβ†’model cascade: when a make select changes, clear the dependent model so a // stale value (e.g. make=Honda, model=Camry) can't linger. `$value` keeps the // make cell in sync; this handler only resets the model. cf-select event props // bind to a handler() object, which must be defined at module scope. const resetModelOnMakeChange = handler< { detail?: { value?: string } }, { model: Writable } >((_event, { model }) => { model.set(""); }); const parkingAdminSubject = (personName: string): ParkingAdminSubject => ({ personName, }); const parkingAdminRolesValue = ( registry: ParkingAdminRegistryCell, ): ParkingAdminRole[] => adminRegistryEntries(registry); const parkingAdminRoleForPerson = ( registry: ParkingAdminRegistryCell, personName: string | undefined, ): ParkingAdminRole | undefined => { const trimmedName = (personName ?? "").trim(); return trimmedName === "" ? undefined : parkingAdminRolesValue(registry).find((role) => role.subject.personName === trimmedName ); }; const personIsParkingAdmin = ( registry: ParkingAdminRegistryCell, personName: string | undefined, ): boolean => parkingAdminRoleForPerson(registry, personName) !== undefined; // Demo-only identity model: the selected person name stands in for the actor. // Do not copy this for production authorization; use a stable user/profile cell. const currentActorName = ( selectedPersonName: Writable, people: PeopleCell, ): string => selectedPersonName.get() || (people.get() ?? [])[0]?.name || ""; const currentParkingAdminRole = ( registry: ParkingAdminRegistryCell, selectedPersonName: Writable, people: PeopleCell, ): ParkingAdminRole | undefined => parkingAdminRoleForPerson( registry, currentActorName(selectedPersonName, people), ); const currentUserCanManageParkingAdmins = ( credential: ParkingAdminManagerCredentialCell, ): boolean => adminManagerCredentialIsActive(credential.get()); const prepareParkingAdminToggle = ( credential: ParkingAdminManagerCredential | null | undefined, registry: ParkingAdminRegistryCell, rawName: string, ): ParkingAdminRole[] | null => { const personName = rawName.trim(); if (!adminManagerCredentialIsActive(credential) || personName === "") { return null; } const adminRoles = parkingAdminRolesValue(registry); const nextRoles = adminRoles.filter((role) => role.subject.personName !== personName ); if (nextRoles.length !== adminRoles.length) { return nextRoles; } return [ ...nextRoles, { subject: parkingAdminSubject(personName), displayName: personName, } as ParkingAdminRole, ]; }; const commuteIcon = (mode: CommuteMode): string => { const icons: Record = { drive: "πŸš—", transit: "🚌", bike: "🚲", wfh: "🏠", other: "β€’", }; return icons[mode] ?? "β€’"; }; // ============================================================ // Allocation Algorithm // ============================================================ function runAutoAllocation( personName: string, date: string, allPeople: Person[], activeSpots: ParkingSpot[], existingRequests: SpotRequest[], ): string { const person = allPeople.find((p) => p.name === personName); if (!person) return ""; const takenSpots = new Set(); for (const req of existingRequests) { if (req.date === date && req.status === "allocated") { takenSpots.add(req.assignedSpot); } } const activeSpotNumbers = activeSpots.filter((s) => s.active).map((s) => s.spotNumber ); const availableSpots = activeSpotNumbers.filter((n) => !takenSpots.has(n)); if (availableSpots.length === 0) return ""; if (person.defaultSpot && availableSpots.includes(person.defaultSpot)) { return person.defaultSpot; } for (const pref of person.spotPreferences) { if (availableSpots.includes(pref)) { return pref; } } return availableSpots[0]; } // ============================================================ // Default Seed Data // ============================================================ export const DEFAULT_SPOTS: ParkingSpot[] = [ { spotNumber: "1", label: "Near entrance", notes: "", active: true }, { spotNumber: "5", label: "", notes: "", active: true }, { spotNumber: "12", label: "Compact only", notes: "Tight, no large vehicles", active: true, }, ]; // ============================================================ // Pattern // ============================================================ export default pattern( ( { spots: inputSpots, people: inputPeople, requests: inputRequests, adminRegistry: inputAdminRegistry, }, ) => { const spots = inputSpots ?? Writable.perSpace.of(DEFAULT_SPOTS); const people = inputPeople ?? Writable.perSpace.of([]); const requests = inputRequests ?? Writable.perSpace.of([]); const defaultAdminRegistry = new Writable.perSpace< ParkingAdminRegistryValue >( {} as ParkingAdminRegistryValue, ); const adminRegistry = (inputAdminRegistry ?? defaultAdminRegistry) as ParkingAdminRegistryCell; const adminManagerCredential = new Writable.perUser< ParkingAdminManagerCredential | null >(null); const nowTimestamp = wish({ query: "#now" }); const todayStr = computed(() => toLocalDateStr(nowTimestamp.result || safeDateNow()) ); const weekDatesArr = computed(() => getWeekDates(todayStr)); // User/session UI state const selectedPersonName = new Writable.perUser(""); const adminMode = new Writable.perSession(false); const requestDate = new Writable.perSession(toLocalDateStr(safeDateNow())); const requestResult = new Writable.perSession(""); // Admin form state const addPersonFormOpen = new Writable.perSession(false); const addSpotFormOpen = new Writable.perSession(false); const editingPersonName = new Writable.perSession(null); const editingSpotNumber = new Writable.perSession(null); const removePersonConfirmTarget = new Writable.perSession( null, ); const removeSpotConfirmTarget = new Writable.perSession( null, ); // Add person form fields const newPersonName = new Writable.perSession(""); const newPersonEmail = new Writable.perSession(""); const newPersonCommuteMode = new Writable.perSession("drive"); const newPersonPriority = new Writable.perSession("1"); const newPersonDefaultSpot = new Writable.perSession(""); const newPersonPreferences = new Writable.perSession(""); const addPersonError = new Writable.perSession(""); // Add spot form fields const newSpotNumber = new Writable.perSession(""); const newSpotLabel = new Writable.perSession(""); const newSpotNotes = new Writable.perSession(""); const addSpotError = new Writable.perSession(""); // Edit person form fields const editName = new Writable.perSession(""); const editEmail = new Writable.perSession(""); const editCommuteMode = new Writable.perSession("drive"); const editPriorityRank = new Writable.perSession("1"); const editDefaultSpot = new Writable.perSession(""); const editPreferences = new Writable.perSession(""); // Edit spot form fields const editSpotNum = new Writable.perSession(""); const editSpotLabel = new Writable.perSession(""); const editSpotNotes = new Writable.perSession(""); const editSpotActive = new Writable.perSession(true); // Add person β€” vehicle draft state const pendingVehicles = new Writable.perSession([]); const draftPlateId = new Writable.perSession(""); const draftPlateState = new Writable.perSession("CA"); const draftColor = new Writable.perSession(""); const draftMake = new Writable.perSession(""); const draftModel = new Writable.perSession(""); // Edit person β€” vehicle draft state const editVehicles = new Writable.perSession([]); const editDraftPlateId = new Writable.perSession(""); const editDraftPlateState = new Writable.perSession("CA"); const editDraftColor = new Writable.perSession(""); const editDraftMake = new Writable.perSession(""); const editDraftModel = new Writable.perSession(""); // Vehicle draft error cells const draftVehicleError = new Writable.perSession(""); const editDraftVehicleError = new Writable.perSession(""); // Override state const gridOverrideSpot = new Writable.perSession(""); const gridOverrideDate = new Writable.perSession(""); const overridePersonName = new Writable.perSession(""); const activeRequestDate = computed(() => requestDate.get() || todayStr); // -------------------------------------------------------- // Actions // -------------------------------------------------------- const enableAdminManager = action(() => { adminManagerCredential.set({ canManageAdmins: true, } as ParkingAdminManagerCredential); }); const togglePersonAdmin = action<{ name: string }>(({ name }) => { const nextAdmins = prepareParkingAdminToggle( adminManagerCredential.get(), adminRegistry, name, ); if (nextAdmins === null) { return; } adminRegistry.set({ admins: nextAdmins as ParkingAdminList }); }); const toggleAdminMode = action(() => { if ( !currentParkingAdminRole(adminRegistry, selectedPersonName, people) ) { adminMode.set(false); return; } adminMode.set(!adminMode.get()); }); const submitRequest = action<{ personName: string; date: string }>( ({ personName: pNameArg, date: dateArg }) => { // Use provided args, or fall back to form state const pName = pNameArg || selectedPersonName.get() || ""; const date = dateArg || activeRequestDate || todayStr; if (!pName || !date || date < todayStr) { requestResult.set("Please select a person and a valid date."); return; } const allPeople = people.get(); const person = allPeople.find((p) => p.name === pName); if (!person) { requestResult.set("Selected person not found."); return; } const existingReqs = requests.get(); const duplicate = existingReqs.find( (r) => r.personName === pName && r.date === date && r.status !== "cancelled", ); if (duplicate) { const spotInfo = duplicate.assignedSpot ? ` (Spot #${duplicate.assignedSpot})` : ""; requestResult.set( `You already have an active request for ${ formatDateDisplay(date) }${spotInfo}.`, ); return; } const allSpotsArr = [...spots.get()]; const assignedSpot = runAutoAllocation( pName, date, [...allPeople], allSpotsArr, [...existingReqs], ); const newReq: SpotRequest = { id: genId(), personName: pName, date, status: assignedSpot ? "allocated" : "denied", assignedSpot, autoAllocated: true, }; requests.set([...existingReqs, newReq]); if (assignedSpot) { requestResult.set( `Spot #${assignedSpot} allocated to ${pName} for ${ formatDateDisplay(date) }.`, ); } else { const activeCount = allSpotsArr.filter((s) => s.active).length; requestResult.set( `No spots available for ${ formatDateDisplay(date) } β€” all ${activeCount} spots are taken.`, ); } }, ); const cancelRequest = action<{ requestId: string }>(({ requestId }) => { requests.set( requests.get().map((r) => r.id === requestId ? { ...r, status: "cancelled" as RequestStatus } : r ), ); }); const addPerson = action< { name: string; email: string; commuteMode: CommuteMode; priorityRank: number; defaultSpot: string; preferences: string; vehicles?: Vehicle[]; } >((event) => { const { name = newPersonName.get() ?? "", email = newPersonEmail.get() ?? "", commuteMode = newPersonCommuteMode.get() ?? "drive", priorityRank = parseInt(newPersonPriority.get() ?? "") || 1, defaultSpot = newPersonDefaultSpot.get() ?? "", preferences = newPersonPreferences.get() ?? "", vehicles: vehiclesArg, } = event ?? {}; const trimName = name.trim(); const trimEmail = email.trim(); if (!trimName || !trimEmail) return; const current = people.get(); if (current.some((p) => p.name === trimName)) { addPersonError.set(`A person named "${trimName}" already exists.`); return; } addPersonError.set(""); // The staged vehicles array does not survive the intra-pattern // actionβ†’action stream send (it arrives `undefined` at the handler), // so fall back to reading the staged perSession cell directly β€” the // same way the other fields default to their form cells above. const normalizedVehicles = normalizeVehicles( vehiclesArg ?? pendingVehicles.get(), ); const newPerson: Person = { name: trimName, email: trimEmail, commuteMode, priorityRank: priorityRank || 1, defaultSpot: defaultSpot || "", spotPreferences: parsePreferences(preferences), vehicles: normalizedVehicles, }; people.set([...current, newPerson]); if (!selectedPersonName.get()) { selectedPersonName.set(trimName); } newPersonName.set(""); newPersonEmail.set(""); newPersonCommuteMode.set("drive"); newPersonPriority.set("1"); newPersonDefaultSpot.set(""); newPersonPreferences.set(""); pendingVehicles.set([]); addPersonFormOpen.set(false); }); const editPerson = action< { originalName: string; name: string; email: string; commuteMode: CommuteMode; priorityRank: number; defaultSpot: string; preferences: string; vehicles?: Vehicle[]; } >((event) => { const { originalName = editingPersonName.get() ?? "", name: editPersonNameArg = editName.get() ?? "", email: editPersonEmailArg = editEmail.get() ?? "", commuteMode: editPersonCommuteModeArg = editCommuteMode.get() ?? "drive", priorityRank: editPersonPriorityArg = parseInt(editPriorityRank.get() ?? "") || 1, defaultSpot: editPersonDefaultSpotArg = editDefaultSpot.get() ?? "", preferences: editPersonPreferencesArg = editPreferences.get() ?? "", vehicles: vehiclesArg, } = event ?? {}; const trimName = editPersonNameArg.trim(); const trimEmail = editPersonEmailArg.trim(); if (!trimName || !trimEmail) return; const current = people.get(); if ( trimName !== originalName && current.some((p) => p.name === trimName) ) return; people.set(current.map((p) => { if (p.name !== originalName) return p; // When vehicles omitted, preserve existing; when provided, normalize const nextVehicles: Vehicle[] = vehiclesArg === undefined ? (p.vehicles ?? []) : normalizeVehicles(vehiclesArg); return { ...p, name: trimName, email: trimEmail, commuteMode: editPersonCommuteModeArg, priorityRank: editPersonPriorityArg || p.priorityRank, defaultSpot: editPersonDefaultSpotArg || "", spotPreferences: parsePreferences(editPersonPreferencesArg), vehicles: nextVehicles, }; })); if (selectedPersonName.get() === originalName) { selectedPersonName.set(trimName); } if (trimName !== originalName) { requests.set( requests.get().map((r) => r.personName === originalName ? { ...r, personName: trimName } : r ), ); if (adminManagerCredentialIsActive(adminManagerCredential.get())) { adminRegistry.set({ admins: parkingAdminRolesValue(adminRegistry).map((role) => role.subject.personName === originalName ? { subject: parkingAdminSubject(trimName), displayName: trimName, } as ParkingAdminRole : role ) as ParkingAdminList, }); } } editingPersonName.set(null); }); const removePerson = action<{ name: string }>(({ name }) => { people.set(people.get().filter((p) => p.name !== name)); if (adminManagerCredentialIsActive(adminManagerCredential.get())) { adminRegistry.set({ admins: parkingAdminRolesValue(adminRegistry).filter((role) => role.subject.personName !== name ) as ParkingAdminList, }); } if (selectedPersonName.get() === name) { const remaining = people.get(); selectedPersonName.set(remaining[0]?.name ?? ""); } removePersonConfirmTarget.set(null); }); const movePersonUp = action<{ name: string }>(({ name }) => { const sorted = [...(people.get() ?? [])].sort((a, b) => a.priorityRank - b.priorityRank ); const idx = sorted.findIndex((p) => p.name === name); if (idx <= 0) return; const above = sorted[idx - 1]; const current = sorted[idx]; const aboveRank = above.priorityRank; const currentRank = current.priorityRank; people.set( people.get().map((p) => { if (p.name === above.name) return { ...p, priorityRank: currentRank }; if (p.name === current.name) return { ...p, priorityRank: aboveRank }; return p; }), ); }); const movePersonDown = action<{ name: string }>(({ name }) => { const sorted = [...(people.get() ?? [])].sort((a, b) => a.priorityRank - b.priorityRank ); const idx = sorted.findIndex((p) => p.name === name); if (idx < 0 || idx >= sorted.length - 1) return; const below = sorted[idx + 1]; const current = sorted[idx]; const belowRank = below.priorityRank; const currentRank = current.priorityRank; people.set( people.get().map((p) => { if (p.name === below.name) return { ...p, priorityRank: currentRank }; if (p.name === current.name) return { ...p, priorityRank: belowRank }; return p; }), ); }); const addSpot = action< { spotNumber: string; label: string; notes: string } >((event) => { if ( !currentParkingAdminRole(adminRegistry, selectedPersonName, people) ) { return; } const { spotNumber: spotNumArg = newSpotNumber.get() ?? "", label = newSpotLabel.get() ?? "", notes = newSpotNotes.get() ?? "", } = event ?? {}; const trimNum = spotNumArg.trim(); if (!trimNum) return; const current = spots.get(); if (current.some((s) => s.spotNumber === trimNum)) { addSpotError.set(`Spot #${trimNum} already exists.`); return; } addSpotError.set(""); spots.set([ ...current, { spotNumber: trimNum, label: label.trim(), notes: notes.trim(), active: true, }, ] as ParkingSpotList); newSpotNumber.set(""); newSpotLabel.set(""); newSpotNotes.set(""); addSpotFormOpen.set(false); }); const editSpot = action< { originalNumber: string; spotNumber: string; label: string; notes: string; active: boolean; } >((event) => { if ( !currentParkingAdminRole(adminRegistry, selectedPersonName, people) ) { return; } const { originalNumber = editingSpotNumber.get() ?? "", spotNumber: spotNumArg2 = editSpotNum.get() ?? "", label: editSpotLabelArg = editSpotLabel.get() ?? "", notes: editSpotNotesArg = editSpotNotes.get() ?? "", active: editSpotActiveArg = editSpotActive.get() ?? true, } = event ?? {}; const trimNum = spotNumArg2.trim(); if (!trimNum) return; const current = spots.get(); if ( trimNum !== originalNumber && current.some((s) => s.spotNumber === trimNum) ) return; spots.set( current.map((s) => s.spotNumber === originalNumber ? { ...s, spotNumber: trimNum, label: editSpotLabelArg.trim(), notes: editSpotNotesArg.trim(), active: editSpotActiveArg, } : s ) as ParkingSpotList, ); if (trimNum !== originalNumber) { requests.set( requests.get().map((r) => r.assignedSpot === originalNumber ? { ...r, assignedSpot: trimNum } : r ), ); } editingSpotNumber.set(null); }); const removeSpot = action<{ spotNumber: string }>( ({ spotNumber: spotNumArg3 }) => { if ( !currentParkingAdminRole(adminRegistry, selectedPersonName, people) ) { return; } spots.set( spots.get().filter((s) => s.spotNumber !== spotNumArg3 ) as ParkingSpotList, ); removeSpotConfirmTarget.set(null); }, ); const adminOverride = action< { spotNumber: string; date: string; personName: string } >(({ spotNumber, date, personName }) => { if ( !currentParkingAdminRole(adminRegistry, selectedPersonName, people) ) { return; } if (!personName || !spotNumber || !date) return; const existingReqs = requests.get(); const spotExisting = existingReqs.find( (r) => r.assignedSpot === spotNumber && r.date === date && r.status === "allocated", ); if (spotExisting && spotExisting.personName !== personName) { requests.set( existingReqs.map((r) => r.id === spotExisting.id ? { ...r, status: "cancelled" as RequestStatus } : r ), ); } const personExisting = requests.get().find( (r) => r.personName === personName && r.date === date && r.status !== "cancelled", ); if (personExisting) { requests.set( requests.get().map((r) => r.id === personExisting.id ? { ...r, assignedSpot: spotNumber, status: "allocated" as RequestStatus, autoAllocated: false, } : r ), ); } else { requests.set([ ...requests.get(), { id: genId(), personName, date, status: "allocated" as RequestStatus, assignedSpot: spotNumber, autoAllocated: false, }, ]); } gridOverrideSpot.set(""); gridOverrideDate.set(""); overridePersonName.set(""); }); // Internal UI actions const startEditPerson = action<{ name: string }>(({ name }) => { const p = people.get().find((x) => x.name === name); if (!p) return; editingPersonName.set(name); editName.set(p.name); editEmail.set(p.email); editCommuteMode.set(p.commuteMode); editPriorityRank.set(String(p.priorityRank)); editDefaultSpot.set(p.defaultSpot); editPreferences.set(p.spotPreferences.join(", ")); editVehicles.set([...(p.vehicles ?? [])]); editDraftPlateId.set(""); editDraftPlateState.set("CA"); editDraftColor.set(""); editDraftMake.set(""); editDraftModel.set(""); editDraftVehicleError.set(""); }); const cancelEditPerson = action(() => editingPersonName.set(null)); const saveEditPerson = action<{ originalName: string }>( ({ originalName }) => { editPerson.send({ originalName, name: editName.get(), email: editEmail.get(), commuteMode: editCommuteMode.get(), priorityRank: parseInt(editPriorityRank.get()) || 1, defaultSpot: editDefaultSpot.get(), preferences: editPreferences.get(), // Materialize plain objects: spreading the cell's array yields // query-result proxies whose fields read empty across the send() // boundary, so vehicles would silently drop. Rebuild them here. vehicles: editVehicles.get().map((v) => ({ plateId: v.plateId, plateState: v.plateState, color: v.color, make: v.make, model: v.model, })), }); }, ); const initiateRemovePerson = action<{ name: string }>(({ name }) => { removePersonConfirmTarget.set(name); }); const cancelRemovePerson = action(() => removePersonConfirmTarget.set(null) ); const startEditSpot = action<{ spotNumber: string }>( ({ spotNumber: spotNumArg4 }) => { const s = spots.get().find((x) => x.spotNumber === spotNumArg4); if (!s) return; editingSpotNumber.set(spotNumArg4); editSpotNum.set(s.spotNumber); editSpotLabel.set(s.label); editSpotNotes.set(s.notes); editSpotActive.set(s.active); }, ); const cancelEditSpot = action(() => editingSpotNumber.set(null)); const saveEditSpot = action<{ originalNumber: string }>( ({ originalNumber }) => { editSpot.send({ originalNumber, spotNumber: editSpotNum.get(), label: editSpotLabel.get(), notes: editSpotNotes.get(), active: editSpotActive.get(), }); }, ); const initiateRemoveSpot = action<{ spotNumber: string }>( ({ spotNumber: spotNumArg5 }) => { removeSpotConfirmTarget.set(spotNumArg5); }, ); const cancelRemoveSpot = action(() => removeSpotConfirmTarget.set(null)); const openGridOverride = action<{ spotNumber: string; date: string }>( ({ spotNumber, date }) => { gridOverrideSpot.set(spotNumber); gridOverrideDate.set(date); overridePersonName.set((people.get() ?? [])[0]?.name ?? ""); }, ); const cancelOverride = action(() => { gridOverrideSpot.set(""); gridOverrideDate.set(""); }); const submitAddPerson = action(() => { addPerson.send({ name: newPersonName.get(), email: newPersonEmail.get(), commuteMode: newPersonCommuteMode.get(), priorityRank: parseInt(newPersonPriority.get()) || 1, defaultSpot: newPersonDefaultSpot.get(), preferences: newPersonPreferences.get(), // Materialize plain objects: spreading the cell's array yields // query-result proxies whose fields read empty across the send() // boundary, so vehicles would silently drop. Rebuild them here. vehicles: pendingVehicles.get().map((v) => ({ plateId: v.plateId, plateState: v.plateState, color: v.color, make: v.make, model: v.model, })), }); }); const submitAddSpot = action(() => { addSpot.send({ spotNumber: newSpotNumber.get(), label: newSpotLabel.get(), notes: newSpotNotes.get(), }); }); // Vehicle actions β€” add/remove for pending (add-person form) const addPendingVehicle = action(() => { const candidate = normalizeVehicle({ plateId: draftPlateId.get(), plateState: draftPlateState.get(), color: draftColor.get(), make: draftMake.get(), model: draftModel.get(), }); if (!candidate.plateId) { draftVehicleError.set("Plate ID is required."); return; } const existing = pendingVehicles.get(); const key = `${candidate.plateId}|${candidate.plateState}`; if (existing.some((v) => `${v.plateId}|${v.plateState}` === key)) { draftVehicleError.set("That plate is already listed."); return; } draftVehicleError.set(""); pendingVehicles.set([...existing, candidate]); draftPlateId.set(""); draftPlateState.set("CA"); draftColor.set(""); draftMake.set(""); draftModel.set(""); }); const removePendingVehicle = action<{ index: number }>(({ index }) => { const current = [...pendingVehicles.get()]; current.splice(index, 1); pendingVehicles.set(current); }); // Vehicle actions β€” add/remove for editVehicles (edit-person form) const addEditVehicle = action(() => { const candidate = normalizeVehicle({ plateId: editDraftPlateId.get(), plateState: editDraftPlateState.get(), color: editDraftColor.get(), make: editDraftMake.get(), model: editDraftModel.get(), }); if (!candidate.plateId) { editDraftVehicleError.set("Plate ID is required."); return; } const existing = editVehicles.get(); const key = `${candidate.plateId}|${candidate.plateState}`; if (existing.some((v) => `${v.plateId}|${v.plateState}` === key)) { editDraftVehicleError.set("That plate is already listed."); return; } editDraftVehicleError.set(""); editVehicles.set([...existing, candidate]); editDraftPlateId.set(""); editDraftPlateState.set("CA"); editDraftColor.set(""); editDraftMake.set(""); editDraftModel.set(""); }); const removeEditVehicle = action<{ index: number }>(({ index }) => { const current = [...editVehicles.get()]; current.splice(index, 1); editVehicles.set(current); }); const toggleAddPersonForm = action(() => { addPersonFormOpen.set(!addPersonFormOpen.get()); draftVehicleError.set(""); }); const toggleAddSpotForm = action(() => addSpotFormOpen.set(!addSpotFormOpen.get()) ); // -------------------------------------------------------- // Helper computeds for UI (keep action closures using .get()) // -------------------------------------------------------- const _isDateInPast = computed(() => activeRequestDate < todayStr); const spotDeactivateWarning = computed(() => { const editNum = editingSpotNumber.get(); if (!editNum || (editSpotActive.get() ?? true)) return false; return (requests.get() ?? []).some( (r) => r.assignedSpot === editNum && r.status === "allocated" && r.date >= todayStr, ); }); const noPeople = computed(() => (people.get() ?? []).length === 0); const personSelectItems = computed(() => (people.get() ?? []).map((p) => ({ label: p.name, value: p.name })) ); const requestDisabled = computed(() => !selectedPersonName.get() || activeRequestDate < todayStr || (people.get() ?? []).length === 0 ); const currentPersonIsAdmin = computed(() => currentParkingAdminRole(adminRegistry, selectedPersonName, people) !== undefined ); const adminModeEnabled = computed(() => adminMode.get() ? currentPersonIsAdmin : false ); const currentUserCanManageAdmins = computed(() => currentUserCanManageParkingAdmins(adminManagerCredential) ); const canBootstrapPeople = computed(() => (people.get() ?? []).length === 0 && currentUserCanManageParkingAdmins(adminManagerCredential) ); const showAdminPeopleSection = computed(() => adminModeEnabled === true || canBootstrapPeople === true ); const adminAccessRows = computed(() => (people.get() ?? []).map((person) => ({ name: person.name, email: person.email, isAdmin: personIsParkingAdmin(adminRegistry, person.name), canManageAdmins: currentUserCanManageAdmins === true, })) ); const commuteModeOptions = [ { label: "πŸš— Drive", value: "drive" }, { label: "🚌 Transit", value: "transit" }, { label: "🚲 Bike", value: "bike" }, { label: "🏠 WFH", value: "wfh" }, { label: "β€’ Other", value: "other" }, ]; const todayFormatted = computed(() => formatDateDisplay(todayStr)); // Compute week month boundary info once (string or null) const weekMonthInfo = computed(() => { const firstMonth = new Date(weekDatesArr[0] + "T00:00:00").getMonth(); const lastMonth = new Date(weekDatesArr[6] + "T00:00:00").getMonth(); if (firstMonth !== lastMonth) { return `${MONTH_NAMES[firstMonth]} / ${MONTH_NAMES[lastMonth]}`; } return null; }); // Pre-compute week grid cell data to avoid OpaqueCell closure issues. // Returns: Array of spot rows, each with an array of cell data per date. // Accessing OpaqueCell values (spot.number, req.id etc.) inside this // single computed() is safe β€” the closure is at top-level, not nested. const weekGridData = computed(() => { const allSpots = (spots.get() ?? []).filter((s) => s != null && s.active); const allRequests = requests.get() ?? []; const currentPerson = selectedPersonName.get(); const overrideSpot = gridOverrideSpot.get(); const overrideDate = gridOverrideDate.get(); const overridePerson = overridePersonName.get(); const weekGridShowAdmin = adminModeEnabled === true; return allSpots.map((spot) => { const spotNum = spot.spotNumber; const cells = weekDatesArr.map((dateStr) => { const req = allRequests.find( (r) => r.date === dateStr && r.status === "allocated" && r.assignedSpot === spotNum, ) ?? null; const isAllocated = req !== null; const isOwn = req !== null && req.personName === currentPerson; const isManual = req !== null && req.autoAllocated === false; const isOverride = overrideSpot === spotNum && overrideDate === dateStr; const conflictName = req && req.personName !== overridePerson ? req.personName : null; const isToday = dateStr === todayStr; const bgColor = isAllocated ? (isToday ? "#dbeafe" : "#eff6ff") : (isToday ? "#fef9c3" : "#f0fdf4"); return { spotNumber: spotNum, dateStr, isToday, req: req ? { ...req } : null, isAllocated, isOwn, isManual, isOverride, conflictName, showAdmin: weekGridShowAdmin, bgColor, }; }); return { spotNumber: spotNum, spotLabel: spot.label, cells }; }); }); // Pre-compute today strip cell data for each active spot const todayStripData = computed(() => { const allSpots = (spots.get() ?? []).filter((s) => s != null && s.active); const allRequests = requests.get() ?? []; const currentPerson = selectedPersonName.get(); const todayStripShowAdmin = adminModeEnabled === true; return allSpots.map((spot) => { const req = allRequests.find( (r) => r.date === todayStr && r.status === "allocated" && r.assignedSpot === spot.spotNumber, ) ?? null; return { spotNumber: spot.spotNumber, spotLabel: spot.label, req: req ? { ...req } : null, isAvailable: req === null, isOwn: req !== null && req.personName === currentPerson, showAdmin: todayStripShowAdmin, }; }); }); // Pre-compute sorted people list for admin panel const adminPeopleData = computed(() => { // Read the perSession edit/remove-confirm targets HERE, at the top of // this computed, and emit `isEditing`/`isRemoveConfirm` per person. // Reading these perSession cells from a `computed()` nested inside the // `.map()` render below silently returns nothing (a narrower perSession // cell can't be followed from that space-scoped render context), so the // inline edit form / remove-confirm prompt never opened. const editingName = editingPersonName.get(); const removeConfirmName = removePersonConfirmTarget.get(); const sorted = [...(people.get() ?? [])].sort((a, b) => a.priorityRank - b.priorityRank ); return sorted.map((p, idx) => ({ name: p.name, email: p.email, commuteMode: p.commuteMode, priorityRank: p.priorityRank, defaultSpot: p.defaultSpot, spotPreferences: [...p.spotPreferences], vehicles: [...(p.vehicles ?? [])].map((v) => ({ formatted: formatVehicle(v), })), isFirst: idx === 0, isLast: idx === sorted.length - 1, isEditing: editingName === p.name, isRemoveConfirm: removeConfirmName === p.name, })); }); // Pre-compute vehicle select options for draft forms (cascade makeβ†’model) const draftModelItems = computed(() => { const make = draftMake.get(); const models = modelsForMake(make); return [ { label: "β€”", value: "" }, ...models.map((m) => ({ label: m, value: m })), ]; }); const editDraftModelItems = computed(() => { const make = editDraftMake.get(); const models = modelsForMake(make); return [ { label: "β€”", value: "" }, ...models.map((m) => ({ label: m, value: m })), ]; }); const colorSelectItems = [ { label: "β€”", value: "" }, ...VEHICLE_COLORS.map((c) => ({ label: c, value: c })), ]; const makeSelectItems = [ { label: "β€”", value: "" }, ...VEHICLE_MAKES.map((m) => ({ label: m, value: m })), ]; const stateSelectItems = US_STATES.map((s) => ({ label: s, value: s })); // Pre-compute vehicle row display data to avoid OpaqueCell closure issues. // Accessing Vehicle fields inside these single top-level computeds is safe. const pendingVehicleRows = computed(() => (pendingVehicles.get() ?? []).map((v, idx) => ({ idx, formatted: formatVehicle(v), })) ); const editVehicleRows = computed(() => (editVehicles.get() ?? []).map((v, idx) => ({ idx, formatted: formatVehicle(v), })) ); // Pre-compute disabled flags for model selects (avoids .get() in JSX prop) const draftMakeSelected = computed(() => !!draftMake.get()); const editDraftMakeSelected = computed(() => !!editDraftMake.get()); // Pre-compute sorted spots list for admin panel const adminSpotsData = computed(() => { // Read the perSession edit/remove-confirm targets HERE (see the matching // note on `adminPeopleData`): a `computed()` nested in the `.map()` render // can't follow these narrower perSession cells from its space-scoped // context, so the inline spot edit/remove prompts never opened. const editingNum = editingSpotNumber.get(); const removeConfirmNum = removeSpotConfirmTarget.get(); return [...(spots.get() ?? [])].map((s) => ({ spotNumber: s.spotNumber, label: s.label, notes: s.notes, active: s.active, isEditingSpot: editingNum === s.spotNumber, isRemoveSpotConfirm: removeConfirmNum === s.spotNumber, })); }); // -------------------------------------------------------- // UI // -------------------------------------------------------- return { [NAME]: "Parking Coordinator", [UI]: ( {/* Header */}
Parking
{todayFormatted} toggleAdminMode.send()} > {adminModeEnabled ? "Admin: ON" : "Admin: OFF"}
{/* === Section A: Today Strip === */} Today β€” {todayFormatted} {noPeople ? ( No team members yet β€” ask your admin to add people. ) : null} {todayStripData.map((stripCell) => { const stripSpotNumber = stripCell.spotNumber; const stripSpotLabel = stripCell.spotLabel; const stripReq = stripCell.req; const stripIsAvailable = stripCell.isAvailable; const stripIsOwn = stripCell.isOwn; const stripShowAdmin = stripCell.showAdmin; return ( #{stripSpotNumber} {stripSpotLabel ? ( {stripSpotLabel} ) : null} {stripReq ? stripReq.personName : "Available"} {stripReq && stripIsOwn ? ( cancelRequest.send({ requestId: stripReq.id, })} > Cancel ) : null} {stripShowAdmin ? ( openGridOverride.send({ spotNumber: stripSpotNumber, date: todayStr, })} > Assign ) : null} ); })} {/* === Section B: Request Form === */} Request a Spot Person {noPeople ? ( No team members yet β€” ask your admin to add people. ) : ( )} Date submitRequest.send({ personName: selectedPersonName.get(), date: activeRequestDate, })} > Request Spot {computed(() => { if (!(activeRequestDate < todayStr)) return null; return ( Please select today or a future date. ); })} {computed(() => { const result = requestResult.get() ?? ""; if (!result) return null; const isSuccess = result.startsWith("Spot #"); const color = isSuccess ? "#166534" : "#991b1b"; const bg = isSuccess ? "#dcfce7" : "#fee2e2"; return ( {result} ); })} {/* === Section C: Admin Access === */} Admin Access Demo manager access lets any user change who can manage parking spots. enableAdminManager.send()} > Enable manager demo {noPeople ? ( Add people before assigning admins. ) : null} {adminAccessRows.map((row) => { const rowName = row.name; const rowEmail = row.email; const rowIsAdmin = row.isAdmin; const rowCanManageAdmins = row.canManageAdmins; return ( {rowName} {rowEmail} togglePersonAdmin.send({ name: rowName, })} > {rowIsAdmin ? "Remove admin" : "Make admin"} ); })} {/* === Section D: Week-Ahead Grid === */} This Week {weekMonthInfo ? ( {weekMonthInfo} ) : null}
{/* Corner spacer */}
{/* Date headers */} {weekDatesArr.map((dateStr) => { const shortName = computed(() => formatDateShort(dateStr).shortName ); const dayNum = computed(() => formatDateShort(dateStr).dayNum ); const isToday = computed(() => dateStr === todayStr); return (
{shortName} {dayNum}
); })} {/* Spot rows β€” uses pre-computed weekGridData to avoid OpaqueCell closure issues */} {weekGridData.map((spotRow) => ( <>
#{spotRow.spotNumber}
{spotRow.cells.map((gridCell) => { const gridReq = gridCell.req; const gridIsAllocated = gridCell.isAllocated; const gridIsOwn = gridCell.isOwn; const gridIsManual = gridCell.isManual; const gridIsOverride = gridCell.isOverride; const gridConflictName = gridCell.conflictName; const gridShowAdmin = gridCell.showAdmin; const gridBgColor = gridCell.bgColor; const gridIsToday = gridCell.isToday; const gridSpotNumber = gridCell.spotNumber; const gridDateStr = gridCell.dateStr; return (
{gridIsOverride ? ( (people.get() ?? []).map((p) => ({ label: p.name, value: p.name, })) )} style="font-size: 0.6875rem;" /> {gridConflictName ? ( Already assigned to{" "} {gridConflictName}. Overwrite? ) : null} adminOverride.send({ spotNumber: gridSpotNumber, date: gridDateStr, personName: overridePersonName.get(), })} > OK cancelOverride.send()} > Γ— ) : gridIsAllocated && gridReq ? ( <> {gridReq.personName} {gridIsOwn ? ( cancelRequest.send({ requestId: gridReq.id, })} > Γ— ) : null} {gridIsManual ? ( M ) : null} ) : ( <> Free {gridShowAdmin ? ( openGridOverride.send({ spotNumber: gridSpotNumber, date: gridDateStr, })} > + ) : null} )}
); })} ))}
{/* === Section E: Admin / bootstrap people management === */} {showAdminPeopleSection ? ( <> {/* People */} People toggleAddPersonForm.send()} > + Add Person {(people.get() ?? []).length === 0 ? ( πŸ‘₯ No team members yet. Add the first person below. ) : null} {adminPeopleData.map((person) => { const { name: personName, email, commuteMode, priorityRank, defaultSpot, spotPreferences, vehicles: personVehicles, isFirst, isLast, } = person; // Derived in `adminPeopleData` (see note there) β€” a // `computed()` nested here that reads the perSession // target cells does not re-render. const isEditing = person.isEditing; const isRemoveConfirm = person.isRemoveConfirm; const activeSpotOpts = computed(() => (spots.get() ?? []) .filter((s) => s.active) .map((s) => ({ label: `#${s.spotNumber}${ s.label ? " β€” " + s.label : "" }`, value: s.spotNumber, })) ); const editSpotItems = computed( () => [ { label: "None", value: "" }, ...activeSpotOpts, ], ); return ( isEditing ? "border: 2px solid var(--cf-colors-blue-500);" : "" )} > {isEditing ? ( Name * Email * Commute Priority * Default Spot Preferences (comma-separated spot numbers) {/* Edit form β€” Vehicles subsection */} Vehicles {editVehicleRows.map((evRow) => ( {evRow.formatted} removeEditVehicle.send({ index: evRow.idx, })} > Γ— ))} Plate * State Color Make Model addEditVehicle.send()} > + Add vehicle {computed(() => { const err = editDraftVehicleError.get(); if (!err) return null; return ( {err} ); })} saveEditPerson.send({ originalName: personName, })} > Save cancelEditPerson.send()} > Cancel ) : ( {personName} #{priorityRank} {defaultSpot ? ( Spot #{defaultSpot} ) : null} {email} {commuteIcon(commuteMode)} {commuteMode} movePersonUp.send({ name: personName, })} > ↑ movePersonDown.send({ name: personName, })} > ↓ startEditPerson.send({ name: personName, })} > Edit initiateRemovePerson.send({ name: personName, })} > Remove {spotPreferences.length > 0 ? ( Prefers: {spotPreferences.map((n) => "#" + n ) .join(", ")} ) : null} {personVehicles.length > 0 ? ( {personVehicles.map((pv) => ( {pv.formatted} ))} ) : null} {isRemoveConfirm ? ( This person has upcoming requests. They will be preserved. Remove anyway? removePerson.send({ name: personName, })} > Remove cancelRemovePerson.send()} > Cancel ) : null} )} ); })} {addPersonFormOpen.get() ? ( Add Person Name * Email * Commute Priority * Default Spot [ { label: "None", value: "" }, ...(spots.get() ?? []) .filter((s) => s.active) .map((s) => ({ label: `#${s.spotNumber}`, value: s.spotNumber, })), ])} style="width: 100%;" /> Preferences (comma-separated) {/* Add form β€” Vehicles subsection */} Vehicles {pendingVehicleRows.map((pvRow) => ( {pvRow.formatted} removePendingVehicle.send({ index: pvRow.idx, })} > Γ— ))} Plate * State Color Make Model addPendingVehicle.send()} > + Add vehicle {computed(() => { const err = draftVehicleError.get(); if (!err) return null; return ( {err} ); })} {computed(() => { const err = addPersonError.get(); if (!err) return null; return ( {err} ); })} submitAddPerson.send()} > Add Person toggleAddPersonForm.send()} > Cancel ) : null} {/* Parking Spots */} {adminModeEnabled ? ( Parking Spots toggleAddSpotForm.send()} > + Add Spot {adminSpotsData.map((spot) => { const spotNum2 = spot.spotNumber; const spotLabel2 = spot.label; const spotNotes2 = spot.notes; const spotActive2 = spot.active; // Derived in `adminSpotsData` (see note there) β€” a // `computed()` nested here reading the perSession // target cells does not re-render. const isEditingSpot = spot.isEditingSpot; const isRemoveSpotConfirm = spot.isRemoveSpotConfirm; return ( {isEditingSpot ? ( Number * Label Notes Active {spotDeactivateWarning ? ( Has upcoming allocations β€” they will remain. ) : null} saveEditSpot.send({ originalNumber: spotNum2, })} > Save cancelEditSpot.send()} > Cancel ) : ( <> #{spotNum2} {spotLabel2 || "(no label)"} {spotNotes2 ? ( {spotNotes2} ) : null} {!spotActive2 ? ( Inactive ) : null} startEditSpot.send({ spotNumber: spotNum2, })} > Edit initiateRemoveSpot.send({ spotNumber: spotNum2, })} > Remove {isRemoveSpotConfirm ? ( Spot #{spotNum2}{" "} has upcoming allocations. They will be preserved. Remove anyway? removeSpot.send({ spotNumber: spotNum2, })} > Remove cancelRemoveSpot.send()} > Cancel ) : null} )} ); })} {addSpotFormOpen.get() ? ( Add Spot Number * Label Notes {computed(() => { const err = addSpotError.get(); if (!err) return null; return ( {err} ); })} submitAddSpot.send()} > Add Spot toggleAddSpotForm.send()} > Cancel ) : null} ) : null} ) : null} ), // Exposed state (Writables auto-unwrap to their T type) spots, people, requests, adminRegistry: adminRegistry as PerSpace, adminMode: adminModeEnabled, currentPersonIsAdmin, currentUserCanManageAdmins, selectedPersonName: computed(() => selectedPersonName.get() ?? ""), requestDate: activeRequestDate, requestResult: computed(() => requestResult.get() ?? ""), // Exposed actions enableAdminManager, togglePersonAdmin, toggleAdminMode, submitRequest, cancelRequest, addPerson, editPerson, removePerson, movePersonUp, movePersonDown, addSpot, editSpot, removeSpot, adminOverride, }; }, );