import React, { useMemo, useState, useEffect } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Checkbox } from "@/components/ui/checkbox"; import { Trash2, Plus, Save, AlertTriangle, Clock, Search, Filter, Calendar, Users, Activity as ActivityIcon, Wrench } from "lucide-react"; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend } from "recharts"; // --- Types --- type Entry = { id: string; date: string; // YYYY-MM-DD start: string; // HH:MM (24h) end: string; // HH:MM (24h) activity: string; apparatus: string; details: string; personnel: string[]; }; type Equipment = { name: string; status: "green" | "yellow" | "red"; note?: string; }; // --- Helpers --- const pad = (n: number) => (n < 10 ? `0${n}` : `${n}`); const todayStr = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; }; const minutesBetween = (start: string, end: string) => { // supports over-midnight by assuming same day unless end < start (then +24h) const [sh, sm] = start.split(":").map(Number); const [eh, em] = end.split(":").map(Number); let startMin = sh * 60 + sm; let endMin = eh * 60 + em; if (endMin < startMin) endMin += 24 * 60; return Math.max(0, endMin - startMin); }; const defaultActivities = [ "Admin – Reports", "Admin – Notes/Updates", "Patrol – Routine", "911 – Emergency Response", "Rescue – Waterborne", "Training", "Maintenance", "Community Outreach", ]; const defaultApparatus = [ "HAB1N", "HAB2N", "RHIB-21", "Rescue PWC-1", "Truck-Unit 2", "Foot Patrol", ]; const defaultPersonnel = [ "Ayers, Ian", "Kabris, George", "Higgins, John", "Warne, Edward", "Pickett, Jack", "Doe, Jane", ]; const DEFAULT_EQUIPMENT: Equipment[] = [ { name: "HAB1N", status: "green" }, { name: "HAB2N", status: "green" }, { name: "RHIB-21", status: "yellow", note: "Awaiting bilge pump" }, { name: "Rescue PWC-1", status: "green" }, { name: "Truck-Unit 2", status: "green" }, { name: "AIS System", status: "green" }, { name: "TAK Server", status: "green" }, ]; // Storage keys const LS_ENTRIES = "msdl_entries_v1"; const LS_EQUIP = "msdl_equipment_v1"; const LS_HEADER = "msdl_header_v1"; export default function MarineSafetyDailyLog() { // Header fields const [departmentName, setDepartmentName] = useState("Ventura Port District - Harbor Patrol"); const [station, setStation] = useState("ST1 - Harbor Patrol Headquarters"); const [shift, setShift] = useState("Harbor Patrol 24 Hour Roster"); const [specialNotes, setSpecialNotes] = useState(""); // Equipment & critical alerts const [equipment, setEquipment] = useState(DEFAULT_EQUIPMENT); const [critical, setCritical] = useState(""); // Date & entries const [selectedDate, setSelectedDate] = useState(todayStr()); const [entries, setEntries] = useState([]); // Filters/search const [searchText, setSearchText] = useState(""); const [filterActivity, setFilterActivity] = useState("all"); const [filterApparatus, setFilterApparatus] = useState("all"); const [filterPersonnel, setFilterPersonnel] = useState("all"); // Reporting const [reportRange, setReportRange] = useState("30"); // days // Persist useEffect(() => { const e = localStorage.getItem(LS_ENTRIES); const eq = localStorage.getItem(LS_EQUIP); const hd = localStorage.getItem(LS_HEADER); if (e) setEntries(JSON.parse(e)); if (eq) setEquipment(JSON.parse(eq)); if (hd) { const h = JSON.parse(hd); setDepartmentName(h.departmentName ?? departmentName); setStation(h.station ?? station); setShift(h.shift ?? shift); setSpecialNotes(h.specialNotes ?? ""); setCritical(h.critical ?? ""); } }, []); useEffect(() => { localStorage.setItem(LS_ENTRIES, JSON.stringify(entries)); }, [entries]); useEffect(() => { localStorage.setItem(LS_EQUIP, JSON.stringify(equipment)); }, [equipment]); useEffect(() => { localStorage.setItem( LS_HEADER, JSON.stringify({ departmentName, station, shift, specialNotes, critical }) ); }, [departmentName, station, shift, specialNotes, critical]); // Derived const dayEntries = useMemo(() => { return entries.filter((e) => e.date === selectedDate); }, [entries, selectedDate]); const filteredEntries = useMemo(() => { return dayEntries.filter((e) => { const s = searchText.toLowerCase(); const textMatch = !s || e.details.toLowerCase().includes(s) || e.activity.toLowerCase().includes(s) || e.apparatus.toLowerCase().includes(s) || e.personnel.join(", ").toLowerCase().includes(s); const actMatch = filterActivity === "all" || e.activity === filterActivity; const appMatch = filterApparatus === "all" || e.apparatus === filterApparatus; const perMatch = filterPersonnel === "all" || e.personnel.includes(filterPersonnel); return textMatch && actMatch && appMatch && perMatch; }); }, [dayEntries, searchText, filterActivity, filterApparatus, filterPersonnel]); // Add/edit form state const [form, setForm] = useState({ id: "", date: todayStr(), start: "08:00", end: "09:00", activity: defaultActivities[0], apparatus: defaultApparatus[0], details: "", personnel: [], }); useEffect(() => { setForm((f) => ({ ...f, date: selectedDate })); }, [selectedDate]); const resetForm = () => setForm({ id: "", date: selectedDate, start: "08:00", end: "09:00", activity: defaultActivities[0], apparatus: defaultApparatus[0], details: "", personnel: [], }); const saveEntry = () => { if (!form.start || !form.end) return; const isEdit = Boolean(form.id); if (isEdit) { setEntries((prev) => prev.map((e) => (e.id === form.id ? form : e))); } else { setEntries((prev) => [{ ...form, id: crypto.randomUUID() }, ...prev]); } resetForm(); }; const deleteEntry = (id: string) => setEntries((prev) => prev.filter((e) => e.id !== id)); const editEntry = (e: Entry) => setForm(e); // Reporting calculations const reportData = useMemo(() => { const days = parseInt(reportRange, 10); const now = new Date(); const start = new Date(now); start.setDate(now.getDate() - days + 1); const inRange = entries.filter((e) => { const [y, m, d] = e.date.split("-").map(Number); const dt = new Date(y, m - 1, d); return dt >= start && dt <= now; }); const byActivity: Record = {}; const byPersonnel: Record = {}; let totalMinutes = 0; for (const e of inRange) { const mins = minutesBetween(e.start, e.end); totalMinutes += mins; byActivity[e.activity] = (byActivity[e.activity] ?? 0) + mins; for (const p of e.personnel) byPersonnel[p] = (byPersonnel[p] ?? 0) + mins; } const actChart = Object.entries(byActivity).map(([k, v]) => ({ name: k, Hours: +(v / 60).toFixed(2) })); const perChart = Object.entries(byPersonnel).map(([k, v]) => ({ name: k, Hours: +(v / 60).toFixed(2) })); // sort for nicer charts actChart.sort((a, b) => b.Hours - a.Hours); perChart.sort((a, b) => b.Hours - a.Hours); return { totalHours: +(totalMinutes / 60).toFixed(2), actChart, perChart, }; }, [entries, reportRange]); const addEquipment = () => setEquipment((prev) => [...prev, { name: `Equipment ${prev.length + 1}`, status: "green" }]); const updateEquipment = (idx: number, patch: Partial) => setEquipment((prev) => prev.map((e, i) => (i === idx ? { ...e, ...patch } : e))); const removeEquipment = (idx: number) => setEquipment((prev) => prev.filter((_, i) => i !== idx)); const dateBackOne = () => { const d = new Date(selectedDate); if (!isNaN(d.valueOf())) { d.setDate(d.getDate() - 1); setSelectedDate(`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`); } }; const dateForwardOne = () => { const d = new Date(selectedDate); if (!isNaN(d.valueOf())) { d.setDate(d.getDate() + 1); setSelectedDate(`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`); } }; return (
{/* Header / Banner */} Marine Safety Daily Log
setDepartmentName(e.target.value)} />
setStation(e.target.value)} />
setShift(e.target.value)} />
setSelectedDate(e.target.value)} />