import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData, useSearchParams } from "@remix-run/react"; import { requireAuthLevel } from "~/utils/auth.server"; import DashboardLayout from "~/components/DashboardLayout"; import { useState } from "react"; import { prisma } from "~/utils/db.server"; export const meta: MetaFunction = () => [{ title: "Stoppages Analysis - Phosphat Report" }]; interface StoppageEntry { id: string; from: string; to: string; total: string; reason: string; responsible: string; note: string; } interface StoppageData { sheetId: number; date: string; area: string; dredgerLocation: string; reclamationLocation: string; shift: string; employee: string; stoppages: StoppageEntry[]; countedStoppages: StoppageEntry[]; uncountedStoppages: StoppageEntry[]; totalStoppageTime: number; // in minutes (all stoppages) countedStoppageTime: number; // in minutes (counted only) uncountedStoppageTime: number; // in minutes (uncounted only) } export const loader = async ({ request }: LoaderFunctionArgs) => { const user = await requireAuthLevel(request, 2); // 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 areaId = url.searchParams.get('areaId'); const employeeId = url.searchParams.get('employeeId'); const dredgerLocationId = url.searchParams.get('dredgerLocationId'); // Build where clause for sheets const whereClause: any = {}; // Date range filter if (dateFrom || dateTo) { if (dateFrom) { whereClause.date = { gte: dateFrom }; } if (dateTo) { whereClause.date = { ...whereClause.date, lte: dateTo }; } } // Area filter if (areaId && areaId !== 'all') { whereClause.areaId = parseInt(areaId); } // Dredger location filter if (dredgerLocationId && dredgerLocationId !== 'all') { whereClause.dredgerLocationId = parseInt(dredgerLocationId); } // Get sheets with their reports let sheets = await prisma.sheet.findMany({ where: whereClause, orderBy: { date: 'desc' }, include: { area: { select: { name: true } }, dredgerLocation: { select: { name: true, class: true } }, reclamationLocation: { select: { name: true } }, dayShift: { include: { employee: { select: { name: true } } } }, nightShift: { include: { employee: { select: { name: true } } } } } }); // Get dropdown data for filters const [areas, dredgerLocations, employees] = await Promise.all([ prisma.area.findMany({ orderBy: { name: 'asc' } }), prisma.dredgerLocation.findMany({ orderBy: { name: 'asc' } }), prisma.employee.findMany({ where: { status: 'active' }, select: { id: true, name: true }, orderBy: { name: 'asc' } }) ]); // Process stoppages data const stoppagesData: StoppageData[] = []; // Helper function to convert time string to minutes const timeToMinutes = (timeStr: string): number => { if (!timeStr || timeStr === '00:00') return 0; const [hours, minutes] = timeStr.split(':').map(Number); return (hours * 60) + minutes; }; // Helper function to check if a stoppage should be counted const isStoppageCounted = (stoppage: StoppageEntry): boolean => { const reason = stoppage.reason?.toLowerCase() || ''; const note = stoppage.note?.toLowerCase() || ''; // Exclude stoppages with "Brine" or "Change Shift" in reason or notes return !( reason.includes('brine') || reason.includes('change shift') || note.includes('brine') || note.includes('change shift') || reason.includes('shift change') || note.includes('shift change') ); }; sheets.forEach(sheet => { // Process day shift stoppages if (sheet.dayShift && Array.isArray(sheet.dayShift.stoppages)) { const stoppages = sheet.dayShift.stoppages as StoppageEntry[]; if (stoppages.length > 0) { const countedStoppages = stoppages.filter(isStoppageCounted); const uncountedStoppages = stoppages.filter(s => !isStoppageCounted(s)); const totalTime = stoppages.reduce((sum, stoppage) => sum + timeToMinutes(stoppage.total), 0); const countedTime = countedStoppages.reduce((sum, stoppage) => sum + timeToMinutes(stoppage.total), 0); const uncountedTime = uncountedStoppages.reduce((sum, stoppage) => sum + timeToMinutes(stoppage.total), 0); stoppagesData.push({ sheetId: sheet.id, date: sheet.date, area: sheet.area.name, dredgerLocation: sheet.dredgerLocation.name, reclamationLocation: sheet.reclamationLocation.name, shift: 'Day', employee: sheet.dayShift.employee.name, stoppages, countedStoppages, uncountedStoppages, totalStoppageTime: totalTime, countedStoppageTime: countedTime, uncountedStoppageTime: uncountedTime }); } } // Process night shift stoppages if (sheet.nightShift && Array.isArray(sheet.nightShift.stoppages)) { const stoppages = sheet.nightShift.stoppages as StoppageEntry[]; if (stoppages.length > 0) { const countedStoppages = stoppages.filter(isStoppageCounted); const uncountedStoppages = stoppages.filter(s => !isStoppageCounted(s)); const totalTime = stoppages.reduce((sum, stoppage) => sum + timeToMinutes(stoppage.total), 0); const countedTime = countedStoppages.reduce((sum, stoppage) => sum + timeToMinutes(stoppage.total), 0); const uncountedTime = uncountedStoppages.reduce((sum, stoppage) => sum + timeToMinutes(stoppage.total), 0); stoppagesData.push({ sheetId: sheet.id, date: sheet.date, area: sheet.area.name, dredgerLocation: sheet.dredgerLocation.name, reclamationLocation: sheet.reclamationLocation.name, shift: 'Night', employee: sheet.nightShift.employee.name, stoppages, countedStoppages, uncountedStoppages, totalStoppageTime: totalTime, countedStoppageTime: countedTime, uncountedStoppageTime: uncountedTime }); } } }); // Apply employee filter if specified (post-processing) let filteredStoppagesData = stoppagesData; if (employeeId && employeeId !== 'all') { const selectedEmployee = employees.find(emp => emp.id === parseInt(employeeId)); if (selectedEmployee) { filteredStoppagesData = stoppagesData.filter(data => data.employee === selectedEmployee.name); } } // Calculate summary statistics const totalStoppages = filteredStoppagesData.reduce((sum, data) => sum + data.stoppages.length, 0); const countedStoppages = filteredStoppagesData.reduce((sum, data) => sum + data.countedStoppages.length, 0); const uncountedStoppages = filteredStoppagesData.reduce((sum, data) => sum + data.uncountedStoppages.length, 0); const totalStoppageTime = filteredStoppagesData.reduce((sum, data) => sum + data.totalStoppageTime, 0); const countedStoppageTime = filteredStoppagesData.reduce((sum, data) => sum + data.countedStoppageTime, 0); const uncountedStoppageTime = filteredStoppagesData.reduce((sum, data) => sum + data.uncountedStoppageTime, 0); const totalSheets = filteredStoppagesData.length; const averageStoppagesPerSheet = totalSheets > 0 ? Math.round((totalStoppages / totalSheets) * 10) / 10 : 0; const averageCountedStoppagesPerSheet = totalSheets > 0 ? Math.round((countedStoppages / totalSheets) * 10) / 10 : 0; const averageStoppageTimePerSheet = totalSheets > 0 ? Math.round((totalStoppageTime / totalSheets) * 10) / 10 : 0; const averageCountedTimePerSheet = totalSheets > 0 ? Math.round((countedStoppageTime / totalSheets) * 10) / 10 : 0; // Convert total time back to hours:minutes format const formatMinutesToTime = (minutes: number): string => { const hours = Math.floor(minutes / 60); const mins = minutes % 60; return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`; }; return json({ user, stoppagesData: filteredStoppagesData, areas, dredgerLocations, employees, filters: { dateFrom, dateTo, areaId, employeeId, dredgerLocationId }, summary: { totalStoppages, countedStoppages, uncountedStoppages, totalStoppageTime: formatMinutesToTime(totalStoppageTime), countedStoppageTime: formatMinutesToTime(countedStoppageTime), uncountedStoppageTime: formatMinutesToTime(uncountedStoppageTime), totalStoppageTimeMinutes: totalStoppageTime, countedStoppageTimeMinutes: countedStoppageTime, uncountedStoppageTimeMinutes: uncountedStoppageTime, totalSheets, averageStoppagesPerSheet, averageCountedStoppagesPerSheet, averageStoppageTimePerSheet: formatMinutesToTime(Math.round(averageStoppageTimePerSheet)), averageCountedTimePerSheet: formatMinutesToTime(Math.round(averageCountedTimePerSheet)) } }); }; export default function Stoppages() { const { user, stoppagesData, areas, dredgerLocations, employees, filters, summary } = useLoaderData(); const [searchParams, setSearchParams] = useSearchParams(); const [showFilters, setShowFilters] = useState(false); // 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.areaId || filters.employeeId || filters.dredgerLocationId; }; // Get today's date for date input max values const today = new Date().toISOString().split('T')[0]; const getShiftBadge = (shift: string) => { return shift === "Day" ? "bg-yellow-100 text-yellow-800" : "bg-blue-100 text-blue-800"; }; return (

Stoppages Analysis

Analyze operational stoppages across all report sheets

{/* Filter Section */}
{hasActiveFilters() && ( {Object.values(filters).filter(Boolean).length} active )}
{hasActiveFilters() && ( )}
{showFilters && (
{/* 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" />
{/* Area */}
{/* Employee */}
{/* Dredger Location */}
{/* Results Summary */}

Showing {stoppagesData.length} sheet{stoppagesData.length !== 1 ? 's' : ''} with stoppages {hasActiveFilters() && ' matching your filters'}

)}
{/* Legend */}
Counted Stoppages
Uncounted Stoppages
Uncounted: Brine operations & shift changes (planned activities)
{/* Summary Statistics */}

Stoppages Summary

Overall statistics for the selected period

{/* Total Stoppages */}
{summary.totalStoppages}
Total Stoppages
{/* Counted Stoppages */}
{summary.countedStoppages}
Counted
{/* Uncounted Stoppages */}
{summary.uncountedStoppages}
Uncounted
{/* Total Time */}
{summary.totalStoppageTime}
Total Time
{/* Counted Time */}
{summary.countedStoppageTime}
Counted Time
{/* Uncounted Time */}
{summary.uncountedStoppageTime}
Uncounted Time
{/* Sheets with Stoppages */}
{summary.totalSheets}
Sheets w/ Stops
{/* Average Counted per Sheet */}
{summary.averageCountedStoppagesPerSheet}
Avg Counted/Sheet
{/* Additional Insights */}
Counted vs Total: {summary.totalStoppages > 0 ? Math.round((summary.countedStoppages / summary.totalStoppages) * 100) : 0}% counted
Avg Total/Sheet: {summary.averageStoppagesPerSheet}
Avg Total Time/Sheet: {summary.averageStoppageTimePerSheet}
Avg Counted Time/Sheet: {summary.averageCountedTimePerSheet}
{/* Stoppages Table - Desktop */}
{stoppagesData.map((data, index) => ( ))}
Date & Shift Location Employee Stoppages Total Time
{new Date(data.date).toLocaleDateString('en-GB')}
{data.shift} Shift
{data.area}
{data.dredgerLocation} - {data.reclamationLocation}
{data.employee}
{data.stoppages.map((stoppage, idx) => { const isCounted = data.countedStoppages.some(cs => cs.id === stoppage.id); return (
{stoppage.reason} {isCounted ? 'Counted' : 'Uncounted'}
{stoppage.total}
{stoppage.from} - {stoppage.to}
{stoppage.responsible && (
Responsible: {stoppage.responsible}
)} {stoppage.note && (
{stoppage.note}
)}
); })}
Total: {Math.floor(data.totalStoppageTime / 60).toString().padStart(2, '0')}: {(data.totalStoppageTime % 60).toString().padStart(2, '0')}
Counted: {Math.floor(data.countedStoppageTime / 60).toString().padStart(2, '0')}: {(data.countedStoppageTime % 60).toString().padStart(2, '0')}
{data.countedStoppages.length} counted, {data.uncountedStoppages.length} uncounted
{/* Stoppages Cards - Mobile */}
{stoppagesData.map((data, index) => (
{new Date(data.date).toLocaleDateString('en-GB')}
{data.area}
{data.shift}
Employee: {data.employee}
Location: {data.dredgerLocation} - {data.reclamationLocation}
Total Time: {Math.floor(data.totalStoppageTime / 60).toString().padStart(2, '0')}: {(data.totalStoppageTime % 60).toString().padStart(2, '0')}
Counted Time: {Math.floor(data.countedStoppageTime / 60).toString().padStart(2, '0')}: {(data.countedStoppageTime % 60).toString().padStart(2, '0')}
Stoppages ({data.countedStoppages.length} counted, {data.uncountedStoppages.length} uncounted):
{data.stoppages.map((stoppage) => { const isCounted = data.countedStoppages.some(cs => cs.id === stoppage.id); return (
{stoppage.reason} {isCounted ? 'C' : 'U'}
{stoppage.total}
{stoppage.from} - {stoppage.to}
{stoppage.responsible && (
Responsible: {stoppage.responsible}
)} {stoppage.note && (
{stoppage.note}
)}
); })}
))}
{stoppagesData.length === 0 && (

No stoppages found

{hasActiveFilters() ? "No stoppages match your current filters. Try adjusting the filters." : "No stoppages have been recorded yet." }

)}
); }