240 lines
10 KiB
TypeScript
240 lines
10 KiB
TypeScript
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
|
import { json } from "@remix-run/node";
|
|
import { useLoaderData } from "@remix-run/react";
|
|
import { requireAuthLevel } from "~/utils/auth.server";
|
|
import DashboardLayout from "~/components/DashboardLayout";
|
|
import ReportSheetViewModal from "~/components/ReportSheetViewModal";
|
|
import { useState } from "react";
|
|
import { prisma } from "~/utils/db.server";
|
|
|
|
export const meta: MetaFunction = () => [{ title: "Report Sheets - Phosphat Report" }];
|
|
|
|
interface ReportSheet {
|
|
id: string;
|
|
date: string;
|
|
area: string;
|
|
dredgerLocation: string;
|
|
reclamationLocation: string;
|
|
status: string;
|
|
dayReport?: any;
|
|
nightReport?: any;
|
|
}
|
|
|
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
const user = await requireAuthLevel(request, 1);
|
|
|
|
// Get sheets with related data
|
|
let sheets = await prisma.sheet.findMany({
|
|
orderBy: { createdAt: 'desc' },
|
|
include: {
|
|
area: { select: { name: true } },
|
|
dredgerLocation: { select: { name: true, class: true } },
|
|
reclamationLocation: { select: { name: true } },
|
|
dayShift: {
|
|
include: {
|
|
employee: { select: { name: true } },
|
|
area: { select: { name: true } },
|
|
dredgerLocation: { select: { name: true, class: true } },
|
|
reclamationLocation: { select: { name: true } }
|
|
}
|
|
},
|
|
nightShift: {
|
|
include: {
|
|
employee: { select: { name: true } },
|
|
area: { select: { name: true } },
|
|
dredgerLocation: { select: { name: true, class: true } },
|
|
reclamationLocation: { select: { name: true } }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Filter sheets for level 1 users (only show sheets where user has at least one shift)
|
|
if (user.authLevel === 1) {
|
|
sheets = sheets.filter((sheet: any) =>
|
|
(sheet.dayShift && sheet.dayShift.employeeId === user.id) ||
|
|
(sheet.nightShift && sheet.nightShift.employeeId === user.id)
|
|
);
|
|
}
|
|
|
|
// Transform sheets to match the expected interface
|
|
const transformedSheets = sheets.map((sheet: any) => ({
|
|
id: sheet.id.toString(),
|
|
date: sheet.date,
|
|
area: sheet.area.name,
|
|
dredgerLocation: sheet.dredgerLocation.name,
|
|
reclamationLocation: sheet.reclamationLocation.name,
|
|
status: sheet.status,
|
|
dayReport: sheet.dayShift,
|
|
nightReport: sheet.nightShift
|
|
}));
|
|
|
|
return json({ user, sheets: transformedSheets });
|
|
};
|
|
|
|
export default function ReportSheet() {
|
|
const { user, sheets } = useLoaderData<typeof loader>();
|
|
const [viewingSheet, setViewingSheet] = useState<ReportSheet | null>(null);
|
|
const [showViewModal, setShowViewModal] = useState(false);
|
|
|
|
const handleView = (sheet: ReportSheet) => {
|
|
setViewingSheet(sheet);
|
|
setShowViewModal(true);
|
|
};
|
|
|
|
const handleCloseViewModal = () => {
|
|
setShowViewModal(false);
|
|
setViewingSheet(null);
|
|
};
|
|
|
|
const getShiftBadge = (shift: string) => {
|
|
return shift === "day"
|
|
? "bg-yellow-100 text-yellow-800"
|
|
: "bg-blue-100 text-blue-800";
|
|
};
|
|
|
|
const getShiftIcon = (shift: string) => {
|
|
return shift === "day" ? (
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
</svg>
|
|
) : (
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<DashboardLayout user={user}>
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Report Sheets</h1>
|
|
<p className="mt-1 text-sm text-gray-600">View grouped reports by location and date</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Report Sheets Table */}
|
|
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Date
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Area
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Locations
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Available Shifts
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Employees
|
|
</th>
|
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{sheets.map((sheet) => (
|
|
<tr key={sheet.id} className="hover:bg-gray-50 transition-colors duration-150">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{new Date(sheet.date).toLocaleDateString('en-GB')}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">{sheet.area}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">
|
|
<div>Dredger: {sheet.dredgerLocation}</div>
|
|
<div className="text-gray-500">Reclamation: {sheet.reclamationLocation}</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex space-x-2">
|
|
{sheet.dayReport && (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getShiftBadge('day')}`}>
|
|
{getShiftIcon('day')}
|
|
<span className="ml-1">Day</span>
|
|
</span>
|
|
)}
|
|
{sheet.nightReport && (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getShiftBadge('night')}`}>
|
|
{getShiftIcon('night')}
|
|
<span className="ml-1">Night</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${sheet.status === 'completed'
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-yellow-100 text-yellow-800'
|
|
}`}>
|
|
{sheet.status === 'completed' ? (
|
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
)}
|
|
{sheet.status.charAt(0).toUpperCase() + sheet.status.slice(1)}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">
|
|
{sheet.dayReport && (
|
|
<div>Day: {sheet.dayReport.employee.name}</div>
|
|
)}
|
|
{sheet.nightReport && (
|
|
<div>Night: {sheet.nightReport.employee.name}</div>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<button
|
|
onClick={() => handleView(sheet)}
|
|
className="text-blue-600 hover:text-blue-900 transition-colors duration-150"
|
|
>
|
|
View Sheet
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{sheets.length === 0 && (
|
|
<div className="text-center py-12">
|
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No report sheets</h3>
|
|
<p className="mt-1 text-sm text-gray-500">Report sheets will appear here when reports are created.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* View Modal */}
|
|
<ReportSheetViewModal
|
|
isOpen={showViewModal}
|
|
onClose={handleCloseViewModal}
|
|
sheet={viewingSheet}
|
|
/>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
} |