/** * Directory Page * * Interactive company directory for facility tenants and visitors. * Features smart filtering with dynamically generated alphabet buttons, * presence-based status filtering, and auto-clearing unavailable selections. * Companies are sorted with present companies first for easy sign-out access. */ /* React hooks */ import { useState, useEffect, useMemo, useCallback } from "react"; /* UI Components */ import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; /* Kiosk Components */ import { CompanyCard } from "@/components/kiosk/company-card"; /* Types & Constants */ import { type Company } from "@/types/company"; import type { Room, FloorPlanWithRooms } from "@/types/floor-plan"; /* Context */ import { useKioskData } from "@/contexts/kiosk-data-context"; /* Services */ import { updateCompanyStatus } from "@/services/companies"; /* Constants */ import { LOADING_MESSAGES, ERROR_MESSAGES, EMPTY_MESSAGES, SECTION_HEADERS, FILTER_LABELS } from "@/constants/ui-text"; /* Filter state options: all companies, present only, or absent only */ type StatusFilter = "all" | "present" | "absent"; /** * Directory Component * * Manages filtering state and renders company directory with smart features: * - Dynamic alphabet generation (only shows letters that exist in data) * - Smart letter availability (disabled when no matches for current filter) * - Auto-clearing selections when they become invalid * - Sorted sections (present first for easy sign-out, then absent) * - Real-time updates via PocketBase subscriptions */ export function Directory() { const { companies, floorPlans, loading, error } = useKioskData(); const [selectedLetter, setSelectedLetter] = useState(null); const [statusFilter, setStatusFilter] = useState("all"); const [updatingCompanyId, setUpdatingCompanyId] = useState(null); /** * Checks if a company matches the current status filter * Extracted to avoid duplication - used in both availableLetters and filteredCompanies */ const matchesStatusFilter = useCallback( (company: Company) => { return ( statusFilter === "all" || (statusFilter === "present" && company.status === "present") || (statusFilter === "absent" && company.status === "absent") ); }, [statusFilter], ); /** * Handles toggling company status between present and absent * Updates via PocketBase API and real-time subscription auto-updates UI */ const handleStatusToggle = useCallback(async (company: Company): Promise => { const newStatus = company.status === "present" ? "absent" : "present"; setUpdatingCompanyId(company.id); try { await updateCompanyStatus(company.id, newStatus); /* Real-time subscription will automatically update the UI */ } catch (error) { console.error("Failed to update company status:", error); /* TODO: Could add error toast notification here */ } finally { setUpdatingCompanyId(null); } }, []); /** * Creates lookup map for company locations * Prevents O(n*m) lookups when rendering company cards */ const companyLocationMap = useMemo(() => { const map = new Map(); for (const floorPlan of floorPlans) { for (const room of floorPlan.rooms) { if (room.company) { map.set(room.company, { room, floorPlan }); } } } return map; }, [floorPlans]); /** * Generates dynamic alphabet from actual company data * Only displays letters that exist in the company list */ const alphabet = useMemo(() => { const letters = new Set(); companies.forEach((company) => { const firstLetter = company.short_name.charAt(0).toUpperCase(); letters.add(firstLetter); }); return Array.from(letters).sort(); }, [companies]); /** * Calculates which alphabet letters have companies for the current status filter * Returns Set for O(1) lookup when rendering alphabet buttons */ const availableLetters = useMemo(() => { const letters = new Set(); companies.forEach((company) => { if (matchesStatusFilter(company)) { const firstLetter = company.short_name.charAt(0).toUpperCase(); letters.add(firstLetter); } }); return letters; }, [companies, matchesStatusFilter]); /** * Auto-clear selected letter when it becomes unavailable * Example: User has "M" selected, switches to "Prisutni" but no present * companies start with "M" - auto-clears to show all present companies */ useEffect(() => { if (selectedLetter && !availableLetters.has(selectedLetter)) { setSelectedLetter(null); } }, [statusFilter, selectedLetter, availableLetters]); /* Filter companies by both letter selection and status filter */ const filteredCompanies = useMemo(() => { return companies.filter((company) => { const matchesLetter = !selectedLetter || company.short_name.toUpperCase().startsWith(selectedLetter); return matchesLetter && matchesStatusFilter(company); }); }, [companies, selectedLetter, matchesStatusFilter]); /* Separate and sort companies: present first (for easy sign-out), then absent */ const presentCompanies = filteredCompanies .filter((c) => c.status === "present") .sort((a, b) => a.short_name.localeCompare(b.short_name)); const absentCompanies = filteredCompanies .filter((c) => c.status === "absent") .sort((a, b) => a.short_name.localeCompare(b.short_name)); /* Loading state */ if (loading) { return (

{LOADING_MESSAGES.companies}

); } /* Error state */ if (error) { return (

{ERROR_MESSAGES.generic}

); } return (
{/* Filter controls: Status filter and dynamic alphabet navigation */}
{/* Status filter: All, Present, or Absent companies */}
{/* Dynamic alphabet: Only shows letters that exist in company data */}
{alphabet.map((letter) => { const isAvailable = availableLetters.has(letter); const isSelected = selectedLetter === letter; return ( ); })}
{/* Present companies grid (shown first for easy tenant sign-out) */}
{presentCompanies.map((company) => ( ))}
{/* Visual separator between present and absent sections */} {absentCompanies.length > 0 && presentCompanies.length > 0 && (
{SECTION_HEADERS.absentCompanies}
)} {/* Absent companies grid (shown below separator) */} {absentCompanies.length > 0 && (
{absentCompanies.map((company) => ( ))}
)} {/* Empty state: Shown when no companies match the filter criteria */} {filteredCompanies.length === 0 && (

{EMPTY_MESSAGES.noCompanies}

)}
); }