"use client" import { useState, useMemo, useRef } from "react" import Link from "next/link" import { cn } from "@/lib/utils" import { useIsMobile } from "@/hooks/use-mobile" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Label } from "@/components/ui/label" import { Checkbox } from "@/components/ui/checkbox" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@/components/ui/sheet" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { IconSearch, IconFilter, IconX, IconPlus, IconCalendar, IconList, IconTimeline, IconChevronRight, IconDots, IconDownload, IconUpload, IconPrinter, IconHistory, IconCalendarOff, IconLoader2, } from "@tabler/icons-react" import { ScheduleListView } from "./schedule-list-view" import { ScheduleGanttView } from "./schedule-gantt-view" import { ScheduleCalendarView } from "./schedule-calendar-view" import { ScheduleMobileView } from "./schedule-mobile-view" import { WorkdayExceptionsView } from "./workday-exceptions-view" import { ScheduleBaselineView } from "./schedule-baseline-view" import { TaskFormDialog } from "./task-form-dialog" import type { ScheduleData, ScheduleBaselineData, TaskFilters, TaskStatus, ConstructionPhase, } from "@/lib/schedule/types" import { EMPTY_FILTERS, STATUS_OPTIONS, PHASE_OPTIONS, } from "@/lib/schedule/types" type View = "calendar" | "list" | "gantt" const VIEW_OPTIONS = [ { value: "calendar" as const, icon: IconCalendar, label: "Calendar" }, { value: "list" as const, icon: IconList, label: "List" }, { value: "gantt" as const, icon: IconTimeline, label: "Gantt" }, ] interface ScheduleViewProps { readonly projectId: string readonly projectName: string readonly initialData: ScheduleData readonly baselines: ScheduleBaselineData[] readonly allProjects?: readonly { id: string; name: string }[] } export function ScheduleView({ projectId, projectName, initialData, baselines, }: ScheduleViewProps) { const isMobile = useIsMobile() const [view, setView] = useState("calendar") const [taskFormOpen, setTaskFormOpen] = useState(false) const [filters, setFilters] = useState(EMPTY_FILTERS) const [baselinesOpen, setBaselinesOpen] = useState(false) const [exceptionsOpen, setExceptionsOpen] = useState(false) const [importDialogOpen, setImportDialogOpen] = useState(false) const [isImporting, setIsImporting] = useState(false) const fileInputRef = useRef(null) const filteredTasks = useMemo(() => { let tasks = initialData.tasks if (filters.status.length > 0) { tasks = tasks.filter((t) => filters.status.includes(t.status)) } if (filters.phase.length > 0) { tasks = tasks.filter((t) => filters.phase.includes(t.phase as ConstructionPhase) ) } if (filters.assignedTo) { const search = filters.assignedTo.toLowerCase() tasks = tasks.filter((t) => t.assignedTo?.toLowerCase().includes(search) ) } if (filters.search) { const search = filters.search.toLowerCase() tasks = tasks.filter((t) => t.title.toLowerCase().includes(search) ) } return tasks }, [initialData.tasks, filters]) const activeFilterCount = filters.status.length + filters.phase.length + (filters.assignedTo ? 1 : 0) const toggleStatus = (status: TaskStatus) => { const current = filters.status const next = current.includes(status) ? current.filter((s) => s !== status) : [...current, status] setFilters({ ...filters, status: next }) } const togglePhase = (phase: ConstructionPhase) => { const current = filters.phase const next = current.includes(phase) ? current.filter((p) => p !== phase) : [...current, phase] setFilters({ ...filters, phase: next }) } const removeStatusChip = (status: TaskStatus) => { setFilters({ ...filters, status: filters.status.filter((s) => s !== status), }) } const removePhaseChip = (phase: ConstructionPhase) => { setFilters({ ...filters, phase: filters.phase.filter((p) => p !== phase), }) } const clearFilters = () => setFilters(EMPTY_FILTERS) // CSV export const handleExportCSV = () => { const headers = [ "Title", "Phase", "Status", "Start Date", "End Date", "Duration (days)", "% Complete", "Assigned To", "Critical Path", "Milestone", ] const rows = filteredTasks.map((task) => [ task.title, task.phase, task.status, task.startDate, task.endDateCalculated, task.workdays.toString(), task.percentComplete.toString(), task.assignedTo ?? "", task.isCriticalPath ? "Yes" : "No", task.isMilestone ? "Yes" : "No", ]) const escapeCSV = (value: string) => { if (value.includes(",") || value.includes('"') || value.includes("\n")) { return `"${value.replace(/"/g, '""')}"` } return value } const csvContent = [ headers.join(","), ...rows.map((row) => row.map(escapeCSV).join(",")), ].join("\n") const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }) const link = document.createElement("a") link.download = `${projectName.replace(/\s+/g, "-")}-schedule-${new Date().toISOString().split("T")[0]}.csv` link.href = URL.createObjectURL(blob) link.click() URL.revokeObjectURL(link.href) } // CSV import const handleFileSelect = async ( e: React.ChangeEvent ) => { const file = e.target.files?.[0] if (!file) return setIsImporting(true) try { const text = await file.text() const lines = text.split("\n") const headers = lines[0].split(",").map((h) => h.trim().toLowerCase()) const titleIdx = headers.findIndex( (h) => h.includes("title") || h.includes("task") ) const startIdx = headers.findIndex((h) => h.includes("start")) const durationIdx = headers.findIndex( (h) => h.includes("duration") || h.includes("days") ) const phaseIdx = headers.findIndex((h) => h.includes("phase")) const assignedIdx = headers.findIndex( (h) => h.includes("assigned") || h.includes("owner") ) const parsed: Record[] = [] for (let i = 1; i < lines.length; i++) { const line = lines[i].trim() if (!line) continue const values = line.split(",").map((v) => v.trim()) const task: Record = {} if (titleIdx >= 0 && values[titleIdx]) task.title = values[titleIdx] if (startIdx >= 0 && values[startIdx]) task.startDate = values[startIdx] if (durationIdx >= 0 && values[durationIdx]) { task.workdays = parseInt(values[durationIdx]) || 1 } if (phaseIdx >= 0 && values[phaseIdx]) task.phase = values[phaseIdx] if (assignedIdx >= 0 && values[assignedIdx]) { task.assignedTo = values[assignedIdx] } if (task.title) { task.status = "PENDING" task.percentComplete = 0 parsed.push(task) } } if (parsed.length > 0) { const blob = new Blob( [JSON.stringify(parsed, null, 2)], { type: "application/json" } ) const url = URL.createObjectURL(blob) const link = document.createElement("a") link.href = url link.download = `imported-tasks-${Date.now()}.json` link.click() URL.revokeObjectURL(url) alert(`Parsed ${parsed.length} tasks from CSV. Downloaded as JSON for review.`) } else { alert("No valid tasks found in the CSV file.") } } catch (error) { console.error("Import failed:", error) alert("Failed to parse CSV file. Please check the format.") } finally { setIsImporting(false) setImportDialogOpen(false) if (fileInputRef.current) { fileInputRef.current.value = "" } } } return (
{/* Header: breadcrumb + view toggle + new task */}
{/* View switcher */}
{VIEW_OPTIONS.map(({ value, icon: Icon, label }) => ( ))}
{/* Action bar: search, filters, overflow */}
{/* Search */}
setFilters({ ...filters, search: e.target.value })} className="h-8 pl-8 text-sm" />
{/* Filter popover */}
{STATUS_OPTIONS.map((opt) => ( ))}
{PHASE_OPTIONS.map((opt) => ( ))}
setFilters({ ...filters, assignedTo: e.target.value }) } className="mt-1.5 h-8 text-sm" />
{activeFilterCount > 0 && ( )}
{/* Active filter chips */}
{filters.status.map((s) => ( removeStatusChip(s)} > {STATUS_OPTIONS.find((o) => o.value === s)?.label ?? s} ))} {filters.phase.map((p) => ( removePhaseChip(p)} > {p} ))} {filters.assignedTo && ( setFilters({ ...filters, assignedTo: "" })} > {filters.assignedTo} )}
{filteredTasks.length} task{filteredTasks.length !== 1 ? "s" : ""} {/* Overflow menu */} Export CSV setImportDialogOpen(true)}> Import CSV window.print()}> Print setBaselinesOpen(true)}> Baselines setExceptionsOpen(true)}> Workday Exceptions
{/* View content */}
{view === "calendar" && ( isMobile ? ( ) : ( ) )} {view === "list" && ( )} {view === "gantt" && ( )}
{/* New task dialog */} {/* Import dialog */} Import Schedule Upload a CSV file with columns for title, start date, duration, phase, and assigned to.

Supported format: CSV with headers

{/* Baselines sheet */} Baselines Save and compare schedule snapshots.
{/* Exceptions sheet */} Workday Exceptions Holidays, vacation days, and other non-working days.
) }