import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form, useActionData, useLoaderData, useNavigation, Link, useSearchParams } from "@remix-run/react"; import { requireAuthLevel } from "~/utils/auth.server"; import DashboardLayout from "~/components/DashboardLayout"; import ReportViewModal from "~/components/ReportViewModal"; import ReportFormModal from "~/components/ReportFormModal"; import Toast from "~/components/Toast"; import { useState, useEffect } from "react"; import { manageSheet, removeFromSheet } from "~/utils/sheet.server"; import { prisma } from "~/utils/db.server"; export const meta: MetaFunction = () => [{ title: "Reports Management - Alhaffer Report System" }]; export const loader = async ({ request }: LoaderFunctionArgs) => { const user = await requireAuthLevel(request, 1); // All employees can access reports // Parse URL search parameters for filters const url = new URL(request.url); const dateFrom = url.searchParams.get('dateFrom'); const dateTo = url.searchParams.get('dateTo'); const shift = url.searchParams.get('shift'); const areaId = url.searchParams.get('areaId'); const employeeId = url.searchParams.get('employeeId'); const dredgerLocationId = url.searchParams.get('dredgerLocationId'); const search = url.searchParams.get('search'); // Build where clause based on filters const whereClause: any = {}; // Date range filter if (dateFrom || dateTo) { whereClause.createdDate = {}; if (dateFrom) { whereClause.createdDate.gte = new Date(dateFrom + 'T00:00:00.000Z'); } if (dateTo) { whereClause.createdDate.lte = new Date(dateTo + 'T23:59:59.999Z'); } } // Shift filter if (shift && shift !== 'all') { whereClause.shift = shift; } // Area filter if (areaId && areaId !== 'all') { whereClause.areaId = parseInt(areaId); } // Employee filter if (employeeId && employeeId !== 'all') { whereClause.employeeId = parseInt(employeeId); } // Dredger location filter if (dredgerLocationId && dredgerLocationId !== 'all') { whereClause.dredgerLocationId = parseInt(dredgerLocationId); } // Search filter (search within notes) if (search && search.trim() !== '') { whereClause.notes = { contains: search.trim(), mode: 'insensitive' // Case-insensitive search }; } // Get filtered reports with related data let reports = await prisma.report.findMany({ where: whereClause, orderBy: { createdDate: 'desc' }, include: { employee: { select: { name: true } }, area: { select: { name: true } }, dredgerLocation: { select: { name: true, class: true } }, reclamationLocation: { select: { name: true } } } }); // if (user.authLevel === 1){ // // filter report by user id // reports = reports.filter((report: any) => report.employeeId === user.id); // } // if (user.authLevel === 1) { // reports = await prisma.report.findMany({ // where: { employeeId: user.id }, // orderBy: { createdDate: 'desc' }, // include: { // employee: { select: { name: true } }, // area: { select: { name: true } }, // dredgerLocation: { select: { name: true, class: true } }, // reclamationLocation: { select: { name: true } } // } // }); // } // Calculate statistics for the stats section const today = new Date(); today.setHours(0, 0, 0, 0); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const sevenDaysAgo = new Date(today); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); // Get dropdown data and statistics const [areas, dredgerLocations, reclamationLocations, foremen, equipment, employees, stats] = await Promise.all([ prisma.area.findMany({ orderBy: { name: 'asc' } }), prisma.dredgerLocation.findMany({ orderBy: { name: 'asc' } }), prisma.reclamationLocation.findMany({ orderBy: { name: 'asc' } }), prisma.foreman.findMany({ orderBy: { name: 'asc' } }), prisma.equipment.findMany({ orderBy: [{ category: 'asc' }, { model: 'asc' }, { number: 'asc' }] }), prisma.employee.findMany({ where: { status: 'active' }, select: { id: true, name: true }, orderBy: { name: 'asc' } }), // Calculate statistics Promise.all([ // Total reports count prisma.report.count(), // Day shift count prisma.report.count({ where: { shift: 'day' } }), // Night shift count prisma.report.count({ where: { shift: 'night' } }), // Reports from today prisma.report.count({ where: { createdDate: { gte: today, lt: new Date(today.getTime() + 24 * 60 * 60 * 1000) } } }), // Reports from yesterday prisma.report.count({ where: { createdDate: { gte: yesterday, lt: today } } }), // Reports from last 7 days for average calculation prisma.report.count({ where: { createdDate: { gte: sevenDaysAgo } } }) ]) ]); const [totalReports, dayShiftCount, nightShiftCount, todayCount, yesterdayCount, last7DaysCount] = stats; const averagePerDay = Math.round((last7DaysCount / 7) * 10) / 10; // Round to 1 decimal place return json({ user, reports, areas, dredgerLocations, reclamationLocations, foremen, equipment, employees, stats: { totalReports, dayShiftCount, nightShiftCount, todayCount, yesterdayCount, averagePerDay }, filters: { dateFrom, dateTo, shift, areaId, employeeId, dredgerLocationId, search } }); }; export const action = async ({ request }: ActionFunctionArgs) => { const user = await requireAuthLevel(request, 1); const formData = await request.formData(); const intent = formData.get("intent"); const id = formData.get("id"); if (intent === "update") { if (typeof id !== "string") { return json({ errors: { form: "Invalid report ID" } }, { status: 400 }); } // Check if user owns this report or has admin privileges const existingReport = await prisma.report.findUnique({ where: { id: parseInt(id) }, select: { employeeId: true, createdDate: true, shift: true, areaId: true, dredgerLocationId: true, reclamationLocationId: true } }); if (!existingReport) { return json({ errors: { form: "Report not found" } }, { status: 404 }); } if (user.authLevel < 2) { // Regular users can only edit their own reports if (existingReport.employeeId !== user.id) { return json({ errors: { form: "You can only edit your own reports" } }, { status: 403 }); } // Regular users can only edit their latest report const latestUserReport = await prisma.report.findFirst({ where: { employeeId: user.id }, orderBy: { createdDate: 'desc' }, select: { id: true } }); if (!latestUserReport || latestUserReport.id !== parseInt(id)) { return json({ errors: { form: "You can only edit your latest report" } }, { status: 403 }); } } const shift = formData.get("shift"); const areaId = formData.get("areaId"); const dredgerLocationId = formData.get("dredgerLocationId"); const dredgerLineLength = formData.get("dredgerLineLength"); const reclamationLocationId = formData.get("reclamationLocationId"); const shoreConnection = formData.get("shoreConnection"); const notes = formData.get("notes"); // Complex JSON fields const reclamationHeightBase = formData.get("reclamationHeightBase"); const reclamationHeightExtra = formData.get("reclamationHeightExtra"); const pipelineMain = formData.get("pipelineMain"); const pipelineExt1 = formData.get("pipelineExt1"); const pipelineReserve = formData.get("pipelineReserve"); const pipelineExt2 = formData.get("pipelineExt2"); const statsDozers = formData.get("statsDozers"); const statsExc = formData.get("statsExc"); const statsLoaders = formData.get("statsLoaders"); const statsForeman = formData.get("statsForeman"); const statsLaborer = formData.get("statsLaborer"); const timeSheetData = formData.get("timeSheetData"); const stoppagesData = formData.get("stoppagesData"); // Validation (same as create) if (typeof shift !== "string" || !["day", "night"].includes(shift)) { return json({ errors: { shift: "Valid shift is required" } }, { status: 400 }); } if (typeof areaId !== "string" || !areaId) { return json({ errors: { areaId: "Area is required" } }, { status: 400 }); } if (typeof dredgerLocationId !== "string" || !dredgerLocationId) { return json({ errors: { dredgerLocationId: "Dredger location is required" } }, { status: 400 }); } if (typeof dredgerLineLength !== "string" || isNaN(parseInt(dredgerLineLength))) { return json({ errors: { dredgerLineLength: "Valid dredger line length is required" } }, { status: 400 }); } if (typeof reclamationLocationId !== "string" || !reclamationLocationId) { return json({ errors: { reclamationLocationId: "Reclamation location is required" } }, { status: 400 }); } if (typeof shoreConnection !== "string" || isNaN(parseInt(shoreConnection))) { return json({ errors: { shoreConnection: "Valid shore connection is required" } }, { status: 400 }); } try { // Parse JSON arrays let timeSheet = []; let stoppages = []; if (timeSheetData && typeof timeSheetData === "string") { try { timeSheet = JSON.parse(timeSheetData); } catch (e) { timeSheet = []; } } if (stoppagesData && typeof stoppagesData === "string") { try { stoppages = JSON.parse(stoppagesData); } catch (e) { stoppages = []; } } // First, remove from old sheet if location/date changed if (existingReport.areaId !== parseInt(areaId) || existingReport.dredgerLocationId !== parseInt(dredgerLocationId) || existingReport.reclamationLocationId !== parseInt(reclamationLocationId)) { await removeFromSheet( parseInt(id), existingReport.shift, existingReport.areaId, existingReport.dredgerLocationId, existingReport.reclamationLocationId, existingReport.createdDate ); } const updatedReport = await prisma.report.update({ where: { id: parseInt(id) }, data: { shift, areaId: parseInt(areaId), dredgerLocationId: parseInt(dredgerLocationId), dredgerLineLength: parseInt(dredgerLineLength), reclamationLocationId: parseInt(reclamationLocationId), shoreConnection: parseInt(shoreConnection), reclamationHeight: { base: parseInt(reclamationHeightBase as string) || 0, extra: parseInt(reclamationHeightExtra as string) || 0 }, pipelineLength: { main: parseInt(pipelineMain as string) || 0, ext1: parseInt(pipelineExt1 as string) || 0, reserve: parseInt(pipelineReserve as string) || 0, ext2: parseInt(pipelineExt2 as string) || 0 }, stats: { Dozers: parseInt(statsDozers as string) || 0, Exc: parseInt(statsExc as string) || 0, Loaders: parseInt(statsLoaders as string) || 0, Foreman: statsForeman as string || "", Laborer: parseInt(statsLaborer as string) || 0 }, timeSheet, stoppages, notes: notes || null } }); // Manage sheet for new location/date await manageSheet( parseInt(id), shift, parseInt(areaId), parseInt(dredgerLocationId), parseInt(reclamationLocationId), updatedReport.createdDate // Use original creation date, not update date ); return json({ success: "Report updated successfully!" }); } catch (error) { return json({ errors: { form: "Failed to update report" } }, { status: 400 }); } } if (intent === "duplicate") { if (typeof id !== "string") { return json({ errors: { form: "Invalid report ID" } }, { status: 400 }); } // Get the original report with all its data const originalReport = await prisma.report.findUnique({ where: { id: parseInt(id) }, include: { area: true, dredgerLocation: true, reclamationLocation: true } }); if (!originalReport) { return json({ errors: { form: "Report not found" } }, { status: 404 }); } // Check if report is too old (before the day before current date) const reportDate = new Date(originalReport.createdDate); const dayBeforeToday = new Date(); dayBeforeToday.setDate(dayBeforeToday.getDate() - 1); dayBeforeToday.setHours(0, 0, 0, 0); // Set to start of day if (reportDate < dayBeforeToday) { return json({ errors: { form: "Cannot duplicate reports older than yesterday" } }, { status: 400 }); } // Check if the report is part of a complete sheet (both day and night shifts exist) const dateString = originalReport.createdDate.toISOString().split('T')[0]; const existingSheet = await prisma.sheet.findUnique({ where: { areaId_dredgerLocationId_reclamationLocationId_date: { areaId: originalReport.areaId, dredgerLocationId: originalReport.dredgerLocationId, reclamationLocationId: originalReport.reclamationLocationId, date: dateString } } }); // If sheet exists and has both shifts, don't allow duplication if (existingSheet && existingSheet.dayShiftId && existingSheet.nightShiftId) { return json({ errors: { form: "Cannot duplicate report - sheet is already complete with both day and night shifts" } }, { status: 400 }); } // Determine the new shift (opposite of original) const newShift = originalReport.shift === 'day' ? 'night' : 'day'; // Check if the opposite shift already exists if (existingSheet) { if ((newShift === 'day' && existingSheet.dayShiftId) || (newShift === 'night' && existingSheet.nightShiftId)) { return json({ errors: { form: `Cannot duplicate report - ${newShift} shift already exists for this date and location` } }, { status: 400 }); } } try { // Create the duplicate report with opposite shift and no stoppages const duplicateReport = await prisma.report.create({ data: { employeeId: user.id, // Assign to current user shift: newShift, areaId: originalReport.areaId, dredgerLocationId: originalReport.dredgerLocationId, dredgerLineLength: originalReport.dredgerLineLength, reclamationLocationId: originalReport.reclamationLocationId, shoreConnection: originalReport.shoreConnection, reclamationHeight: originalReport.reclamationHeight, pipelineLength: originalReport.pipelineLength, stats: originalReport.stats, timeSheet: originalReport.timeSheet, stoppages: [], // Empty stoppages array notes: originalReport.notes } }); // Manage sheet for the new report await manageSheet( duplicateReport.id, newShift, originalReport.areaId, originalReport.dredgerLocationId, originalReport.reclamationLocationId, duplicateReport.createdDate ); return json({ success: `Report duplicated successfully as ${newShift} shift!` }); } catch (error) { console.error('Duplicate error:', error); return json({ errors: { form: "Failed to duplicate report" } }, { status: 400 }); } } if (intent === "carryTo") { if (typeof id !== "string") { return json({ errors: { form: "Invalid report ID" } }, { status: 400 }); } // Get the original report with all its data const originalReport = await prisma.report.findUnique({ where: { id: parseInt(id) }, include: { area: true, dredgerLocation: true, reclamationLocation: true } }); if (!originalReport) { return json({ errors: { form: "Report not found" } }, { status: 404 }); } // Check if report is from today or future (cannot carry forward) const reportDate = new Date(originalReport.createdDate); const today = new Date(); today.setHours(0, 0, 0, 0); reportDate.setHours(0, 0, 0, 0); if (reportDate >= today) { return json({ errors: { form: "Cannot carry forward reports from today or future dates" } }, { status: 400 }); } // Check if a report with same shift, area, and locations already exists for today const todayString = today.toISOString().split('T')[0]; const existingTodayReport = await prisma.report.findFirst({ where: { createdDate: { gte: today, lt: new Date(today.getTime() + 24 * 60 * 60 * 1000) }, shift: originalReport.shift, areaId: originalReport.areaId, dredgerLocationId: originalReport.dredgerLocationId, reclamationLocationId: originalReport.reclamationLocationId } }); if (existingTodayReport) { return json({ errors: { form: `A ${originalReport.shift} shift report already exists for today with the same location configuration` } }, { status: 400 }); } try { // Create the carried forward report with today's date const carriedReport = await prisma.report.create({ data: { employeeId: user.id, // Assign to current user shift: originalReport.shift, areaId: originalReport.areaId, dredgerLocationId: originalReport.dredgerLocationId, dredgerLineLength: originalReport.dredgerLineLength, reclamationLocationId: originalReport.reclamationLocationId, shoreConnection: originalReport.shoreConnection, reclamationHeight: originalReport.reclamationHeight, pipelineLength: originalReport.pipelineLength, stats: originalReport.stats, timeSheet: originalReport.timeSheet, stoppages: [], // Empty stoppages array for new day notes: originalReport.notes, createdDate: new Date() // Set to current date/time } }); // Manage sheet for the new report await manageSheet( carriedReport.id, originalReport.shift, originalReport.areaId, originalReport.dredgerLocationId, originalReport.reclamationLocationId, carriedReport.createdDate ); // Check if the sheet should be marked as completed const todaySheet = await prisma.sheet.findUnique({ where: { areaId_dredgerLocationId_reclamationLocationId_date: { areaId: originalReport.areaId, dredgerLocationId: originalReport.dredgerLocationId, reclamationLocationId: originalReport.reclamationLocationId, date: todayString } } }); // If sheet has both day and night shifts, mark as completed if (todaySheet && todaySheet.dayShiftId && todaySheet.nightShiftId) { await prisma.sheet.update({ where: { id: todaySheet.id }, data: { status: 'completed' } }); } return json({ success: `Report carried forward to today as ${originalReport.shift} shift!` }); } catch (error) { console.error('CarryTo error:', error); return json({ errors: { form: "Failed to carry forward report" } }, { status: 400 }); } } if (intent === "delete") { if (typeof id !== "string") { return json({ errors: { form: "Invalid report ID" } }, { status: 400 }); } // Check if user owns this report or has admin privileges const existingReport = await prisma.report.findUnique({ where: { id: parseInt(id) }, select: { employeeId: true, createdDate: true, shift: true, areaId: true, dredgerLocationId: true, reclamationLocationId: true } }); if (!existingReport) { return json({ errors: { form: "Report not found" } }, { status: 404 }); } if (user.authLevel < 2) { // Regular users can only delete their own reports if (existingReport.employeeId !== user.id) { return json({ errors: { form: "You can only delete your own reports" } }, { status: 403 }); } // Regular users can only delete their latest report const latestUserReport = await prisma.report.findFirst({ where: { employeeId: user.id }, orderBy: { createdDate: 'desc' }, select: { id: true } }); if (!latestUserReport || latestUserReport.id !== parseInt(id)) { return json({ errors: { form: "You can only delete your latest report" } }, { status: 403 }); } } try { // Remove from sheet before deleting report await removeFromSheet( parseInt(id), existingReport.shift, existingReport.areaId, existingReport.dredgerLocationId, existingReport.reclamationLocationId, existingReport.createdDate ); await prisma.report.delete({ where: { id: parseInt(id) } }); return json({ success: "Report deleted successfully!" }); } catch (error) { return json({ errors: { form: "Failed to delete report" } }, { status: 400 }); } } return json({ errors: { form: "Invalid action" } }, { status: 400 }); }; export default function Reports() { const { user, reports, areas, dredgerLocations, reclamationLocations, foremen, equipment, employees, stats, filters } = useLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const [searchParams, setSearchParams] = useSearchParams(); const [editingReport, setEditingReport] = useState(null); const [viewingReport, setViewingReport] = useState(null); const [showModal, setShowModal] = useState(false); const [showViewModal, setShowViewModal] = useState(false); const [showFilters, setShowFilters] = useState(false); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); // Dynamic arrays state for editing only const [timeSheetEntries, setTimeSheetEntries] = useState>([]); const [stoppageEntries, setStoppageEntries] = useState>([]); const isSubmitting = navigation.state === "submitting"; const isEditing = editingReport !== null; // Handle success/error messages from URL params and action data useEffect(() => { const successMessage = searchParams.get("success"); const errorMessage = searchParams.get("error"); if (successMessage) { setToast({ message: successMessage, type: "success" }); // Clear the URL parameter window.history.replaceState({}, '', '/reports'); } else if (errorMessage) { setToast({ message: errorMessage, type: "error" }); // Clear the URL parameter window.history.replaceState({}, '', '/reports'); } else if (actionData?.success) { setToast({ message: actionData.success, type: "success" }); setShowModal(false); setEditingReport(null); } else if (actionData?.errors?.form) { setToast({ message: actionData.errors.form, type: "error" }); } }, [actionData, searchParams]); const handleView = (report: any) => { setViewingReport(report); setShowViewModal(true); }; const handleEdit = (report: any) => { setEditingReport(report); // Load existing timesheet and stoppages data setTimeSheetEntries(Array.isArray(report.timeSheet) ? report.timeSheet : []); setStoppageEntries(Array.isArray(report.stoppages) ? report.stoppages : []); setShowModal(true); }; // Remove handleAdd since we're using a separate page const handleCloseModal = () => { setShowModal(false); setEditingReport(null); setTimeSheetEntries([]); setStoppageEntries([]); }; const handleCloseViewModal = () => { setShowViewModal(false); setViewingReport(null); }; // Helper function to calculate time difference in hours:minutes format const calculateTimeDifference = (from1: string, to1: string, from2: string, to2: string) => { if (!from1 || !to1) return "00:00"; const parseTime = (timeStr: string) => { const [hours, minutes] = timeStr.split(':').map(Number); return hours * 60 + minutes; }; const formatTime = (minutes: number) => { const hours = Math.floor(minutes / 60); const mins = minutes % 60; return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`; }; let totalMinutes = 0; // First period if (from1 && to1) { const start1 = parseTime(from1); let end1 = parseTime(to1); if (end1 < start1) end1 += 24 * 60; totalMinutes += end1 - start1; } // Second period if (from2 && to2) { const start2 = parseTime(from2); let end2 = parseTime(to2); if (end2 < start2) end2 += 24 * 60; totalMinutes += end2 - start2; } return formatTime(Math.max(0, totalMinutes)); }; const calculateStoppageTime = (from: string, to: string) => { if (!from || !to) return "00:00"; const parseTime = (timeStr: string) => { const [hours, minutes] = timeStr.split(':').map(Number); return hours * 60 + minutes; }; const formatTime = (minutes: number) => { const hours = Math.floor(minutes / 60); const mins = minutes % 60; return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`; }; const startMinutes = parseTime(from); let endMinutes = parseTime(to); if (endMinutes < startMinutes) endMinutes += 24 * 60; const totalMinutes = Math.max(0, endMinutes - startMinutes); return formatTime(totalMinutes); }; // TimeSheet management functions const addTimeSheetEntry = () => { const newEntry = { id: Date.now().toString(), machine: '', from1: '', to1: '', from2: '', to2: '', total: '00:00', reason: '' }; setTimeSheetEntries([...timeSheetEntries, newEntry]); }; const removeTimeSheetEntry = (id: string) => { setTimeSheetEntries(timeSheetEntries.filter(entry => entry.id !== id)); }; const updateTimeSheetEntry = (id: string, field: string, value: string) => { setTimeSheetEntries(timeSheetEntries.map(entry => { if (entry.id === id) { const updatedEntry = { ...entry, [field]: value }; // Auto-calculate total when time fields change if (['from1', 'to1', 'from2', 'to2'].includes(field)) { updatedEntry.total = calculateTimeDifference( updatedEntry.from1, updatedEntry.to1, updatedEntry.from2, updatedEntry.to2 ); } return updatedEntry; } return entry; })); }; // Stoppage management functions const addStoppageEntry = () => { const newEntry = { id: Date.now().toString(), from: '', to: '', total: '00:00', reason: '', responsible: '', note: '' }; setStoppageEntries([...stoppageEntries, newEntry]); }; const removeStoppageEntry = (id: string) => { setStoppageEntries(stoppageEntries.filter(entry => entry.id !== id)); }; const updateStoppageEntry = (id: string, field: string, value: string) => { setStoppageEntries(stoppageEntries.map(entry => { if (entry.id === id) { const updatedEntry = { ...entry, [field]: value }; // Auto-calculate total when time fields change if (['from', 'to'].includes(field)) { updatedEntry.total = calculateStoppageTime(updatedEntry.from, updatedEntry.to); } return updatedEntry; } return entry; })); }; const getShiftBadge = (shift: string) => { return shift === "day" ? "bg-yellow-100 text-yellow-800" : "bg-blue-100 text-blue-800"; }; const canEditReport = (report: any) => { // Admin users (auth level >= 2) can edit any report if (user.authLevel >= 2) { return true; } // Regular users (auth level 1) can only edit their own latest report if (report.employeeId === user.id) { // Find the latest report for this user const userReports = reports.filter(r => r.employeeId === user.id); const latestReport = userReports.reduce((latest, current) => new Date(current.createdDate) > new Date(latest.createdDate) ? current : latest ); return report.id === latestReport.id; } return false; }; const canDuplicateReport = (report: any) => { // Check if report is too old (before the day before current date) const reportDate = new Date(report.createdDate); const dayBeforeToday = new Date(); dayBeforeToday.setDate(dayBeforeToday.getDate() - 1); dayBeforeToday.setHours(0, 0, 0, 0); // Set to start of day // If report is before the day before today, don't allow duplication if (reportDate < dayBeforeToday) { return false; } // All users (auth level 1+) can duplicate reports, not just their own // The server will handle the final validation for sheet completeness return true; }; const canCarryForwardReport = (report: any) => { // Check if report is from today or future (cannot carry forward) const reportDate = new Date(report.createdDate); const today = new Date(); today.setHours(0, 0, 0, 0); reportDate.setHours(0, 0, 0, 0); // If report is from today or future, don't allow carry forward if (reportDate >= today) { return false; } // All users (auth level 1+) can carry forward reports return true; }; const isReportTooOld = (report: any) => { const reportDate = new Date(report.createdDate); const dayBeforeToday = new Date(); dayBeforeToday.setDate(dayBeforeToday.getDate() - 1); dayBeforeToday.setHours(0, 0, 0, 0); return reportDate < dayBeforeToday; }; // Filter functions const handleFilterChange = (filterName: string, value: string) => { const newSearchParams = new URLSearchParams(searchParams); if (value === '' || value === 'all') { newSearchParams.delete(filterName); } else { newSearchParams.set(filterName, value); } setSearchParams(newSearchParams); }; const clearAllFilters = () => { setSearchParams(new URLSearchParams()); }; const hasActiveFilters = () => { return filters.dateFrom || filters.dateTo || filters.shift || filters.areaId || filters.employeeId || filters.dredgerLocationId || filters.search; }; // Debounced search handler for better performance const [searchTerm, setSearchTerm] = useState(filters.search || ''); const [searchTimeout, setSearchTimeout] = useState(null); const handleSearchChange = (value: string) => { setSearchTerm(value); // Clear existing timeout if (searchTimeout) { clearTimeout(searchTimeout); } // Set new timeout for debounced search const newTimeout = setTimeout(() => { handleFilterChange('search', value); }, 500); // 500ms delay setSearchTimeout(newTimeout); }; // Update search term when filters change (e.g., from URL or clear all) useEffect(() => { setSearchTerm(filters.search || ''); }, [filters.search]); // Cleanup timeout on unmount useEffect(() => { return () => { if (searchTimeout) { clearTimeout(searchTimeout); } }; }, [searchTimeout]); // Get today's date for date input max values const today = new Date().toISOString().split('T')[0]; return (

Shifts Management

Create and manage operational shifts

Create New Shift
{/* Quick Stats Section */}

Quick Statistics

Overview of report activity

{/* Total Reports */}
{stats.totalReports}
Total Shifts
{/* Day Shift Count */}
{stats.dayShiftCount}
Day Shifts
{/* Night Shift Count */}
{stats.nightShiftCount}
Night Shifts
{/* Today's Reports */}
{stats.todayCount}
Today
{/* Yesterday's Reports */}
{stats.yesterdayCount}
Yesterday
{/* Average Per Day */}
{stats.averagePerDay}
Avg/Day (7d)
{/* Additional Insights */}
Shift Distribution: {stats.totalReports > 0 ? Math.round((stats.dayShiftCount / stats.totalReports) * 100) : 0}% Day, {stats.totalReports > 0 ? Math.round((stats.nightShiftCount / stats.totalReports) * 100) : 0}% Night
Today vs Yesterday: = stats.yesterdayCount ? 'text-green-600' : 'text-red-600'}`}> {stats.todayCount >= stats.yesterdayCount ? '+' : ''}{stats.todayCount - stats.yesterdayCount}
Weekly Trend: {stats.averagePerDay} reports/day
{/* Filter Section */}
{hasActiveFilters() && ( {Object.values(filters).filter(Boolean).length} active )}
{hasActiveFilters() && ( )}
{showFilters && (
{/* Search Input - Full Width */} {/*
handleSearchChange(e.target.value)} className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> {filters.search && (
)}
{filters.search && (

Searching for: "{filters.search}"

{searchTerm !== filters.search && ( Searching... )}
)}
*/}
{/* Date From */}
handleFilterChange('dateFrom', e.target.value)} className="block w-full text-sm border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" />
{/* Date To */}
handleFilterChange('dateTo', e.target.value)} className="block w-full text-sm border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" />
{/* Shift Type */}
{/* Area */}
{/* Employee */}
{/* Dredger Location */}
{/* Results Summary */}

Showing {reports.length} report{reports.length !== 1 ? 's' : ''} {hasActiveFilters() && ( {filters.search ? ' matching your search and filters' : ' matching your filters'} )}

{filters.search && reports.length === 0 && (

No reports found containing "{filters.search}" in their notes. Try a different search term.

)}
)}
{/* Reports Table - Desktop */}
{reports.map((report) => ( ))}
Shift Details Shift & Area Locations Created Actions
Shift #{report.id}
by {report.employee.name}
{report.shift.charAt(0).toUpperCase() + report.shift.slice(1)} Shift {report.area.name}
{report.area.name} Dredger
{report.dredgerLocation.name} - {report.reclamationLocation.name}
{new Date(report.createdDate).toLocaleDateString('en-GB')}
{canDuplicateReport(report) ? (
) : isReportTooOld(report) ? ( Duplicate ) : null} {canCarryForwardReport(report) && (
)} {canEditReport(report) && ( <>
)}
{/* Reports Cards - Mobile */}
{reports.map((report) => (
Shift #{report.id}
by {report.employee.name}
{report.shift.charAt(0).toUpperCase() + report.shift.slice(1)}
Area: {report.area.name}
Dredger: {report.dredgerLocation.name}
Reclamation: {report.reclamationLocation.name}
Created: {new Date(report.createdDate).toLocaleDateString('en-GB')}
{canDuplicateReport(report) ? (
) : isReportTooOld(report) ? ( ) : null} {canCarryForwardReport(report) && (
)} {canEditReport(report) && (
)}
))}
{reports.length === 0 && (

No reports

Get started by creating your first report.

Create Shifts
)}
{/* Edit Form Modal - Only for editing existing reports */} {isEditing && ( )} {/* View Modal */} {/* Toast Notifications */} {toast && ( setToast(null)} /> )}
); }