This commit is contained in:
yznahmad 2025-07-29 15:22:36 +03:00
parent e8c83df854
commit b43819aa7b
22 changed files with 1295 additions and 95 deletions

View File

@ -1,4 +1,4 @@
import { Form, Link, useLoaderData } from "@remix-run/react"; import { Form, Link } from "@remix-run/react";
import type { Employee } from "@prisma/client"; import type { Employee } from "@prisma/client";
interface DashboardLayoutProps { interface DashboardLayoutProps {
@ -84,6 +84,18 @@ export default function DashboardLayout({ children, user }: DashboardLayoutProps
<svg className="mr-4 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="mr-4 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" /> <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> </svg>
Shifts
</Link>
</li>
<li>
<Link
to="/report-sheet"
className="group flex items-center px-2 py-2 text-base font-medium rounded-md text-gray-900 hover:bg-gray-50"
>
<svg className="mr-4 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
Reports Reports
</Link> </Link>
</li> </li>

View File

@ -0,0 +1,524 @@
import React from 'react';
import { exportReportToExcel } from '~/utils/excelExport';
interface ReportSheet {
id: string;
date: string;
area: string;
dredgerLocation: string;
reclamationLocation: string;
dayReport?: any;
nightReport?: any;
}
interface ReportSheetViewModalProps {
isOpen: boolean;
onClose: () => void;
sheet: ReportSheet | null;
}
export default function ReportSheetViewModal({ isOpen, onClose, sheet }: ReportSheetViewModalProps) {
if (!isOpen || !sheet) return null;
const handleExportExcel = async () => {
if (sheet.dayReport) {
await exportReportToExcel(sheet.dayReport);
}
if (sheet.nightReport) {
await exportReportToExcel(sheet.nightReport);
}
};
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose}></div>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-7xl sm:w-full">
<div className="bg-white px-6 pt-6 pb-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold text-gray-900">Report Sheet - {new Date(sheet.date).toLocaleDateString('en-GB')}</h3>
<div className="flex space-x-2">
<button
type="button"
onClick={handleExportExcel}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
Export Excel
</button>
<button
type="button"
onClick={() => {
const printContent = document.getElementById('reportframe');
if (printContent) {
const printWindow = window.open('', '_blank');
if (printWindow) {
printWindow.document.write(`
<html>
<head>
<title>Report Sheet Print</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
table { border-collapse: collapse; }
.border-2 { border: 2px solid black; }
.border { border: 1px solid black; }
.border-t { border-top: 1px solid black; }
.border-r { border-right: 1px solid black; }
.border-black { border-color: black; }
.bg-green-500 {
background-color: #10b981 !important;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.text-white { color: white !important; }
.text-center { text-align: center; }
.font-bold { font-weight: bold; }
.font-semibold { font-weight: 600; }
.p-2 { padding: 8px; }
.p-4 { padding: 16px; }
.mb-2 { margin-bottom: 8px; }
.mb-4 { margin-bottom: 16px; }
.mt-1 { margin-top: 4px; }
.mt-4 { margin-top: 16px; }
.mt-6 { margin-top: 24px; }
.pt-1 { padding-top: 4px; }
.pt-2 { padding-top: 8px; }
.w-full { width: 100%; }
.h-16 { height: 64px; }
.mx-auto { margin-left: auto; margin-right: auto; }
.underline { text-decoration: underline; }
.text-sm { font-size: 14px; }
.text-lg { font-size: 18px; }
.min-h-[100px] { min-height: 100px; }
@media print {
body {
margin: 0;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.no-print { display: none; }
.bg-green-500 {
background-color: #10b981 !important;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.text-white { color: white !important; }
}
</style>
</head>
<body>
${printContent.innerHTML}
</body>
</html>
`);
printWindow.document.close();
printWindow.print();
printWindow.close();
}
}
}}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>
Print
</button>
<button
type="button"
onClick={onClose}
className="bg-white rounded-md text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Combined Report Sheet Layout */}
<div id="reportframe" className="max-h-[80vh] overflow-y-auto bg-white p-6 border border-gray-300" style={{ fontFamily: 'Arial, sans-serif' }}>
<ReportSheetHeader sheet={sheet} />
<ReportSheetInfo sheet={sheet} />
<ReportSheetDredgerSection sheet={sheet} />
<ReportSheetLocationData sheet={sheet} />
<ReportSheetPipelineLength sheet={sheet} />
{/* Day Shift Section */}
{sheet.dayReport && (
<>
<ReportSheetShiftHeader shift="day" />
<ReportSheetEquipmentStats report={sheet.dayReport} />
<ReportSheetTimeSheet report={sheet.dayReport} />
<ReportSheetStoppages report={sheet.dayReport} />
<ReportSheetNotes report={sheet.dayReport} />
</>
)}
{/* Night Shift Section */}
{sheet.nightReport && (
<>
<ReportSheetShiftHeader shift="night" />
<ReportSheetEquipmentStats report={sheet.nightReport} />
<ReportSheetTimeSheet report={sheet.nightReport} />
<ReportSheetStoppages report={sheet.nightReport} />
<ReportSheetNotes report={sheet.nightReport} />
</>
)}
<ReportSheetFooter />
</div>
</div>
</div>
</div>
</div>
);
}
// Header Section Component
function ReportSheetHeader({ sheet }: { sheet: ReportSheet }) {
return (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse">
<tbody>
<tr>
<td className="border-r-2 border-black p-2 text-center font-bold text-lg" style={{ width: '70%' }}>
<div>Reclamation Work Diary - Daily Sheet</div>
<div className="border-t border-black mt-1 pt-1">QF-3.6.1-08</div>
<div className="border-t border-black mt-1 pt-1">Rev. 1.0</div>
</td>
<td className="p-2 text-center" style={{ width: '30%' }}>
<img
src="/logo-light.png"
alt="Arab Potash Logo"
className="h-16 mx-auto"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
</td>
</tr>
</tbody>
</table>
</div>
);
}
// Report Info Component
function ReportSheetInfo({ sheet }: { sheet: ReportSheet }) {
return (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse">
<tbody>
<tr>
<td className="border-r border-black p-2 font-semibold" style={{ width: '15%' }}>Date:</td>
<td className="border-r border-black p-2" style={{ width: '35%' }}>
{new Date(sheet.date).toLocaleDateString('en-GB')}
</td>
<td className="border-r border-black p-2 font-semibold" style={{ width: '20%' }}> Report No. </td>
<td className="p-2" style={{ width: '30%' }}>{sheet.id}</td>
</tr>
</tbody>
</table>
</div>
);
}
// Dredger Section Component
function ReportSheetDredgerSection({ sheet }: { sheet: ReportSheet }) {
return (
<div className="text-center font-bold text-lg mb-2 underline">
{sheet.area} Dredger
</div>
);
}
// Location Data Component (using data from either available report)
function ReportSheetLocationData({ sheet }: { sheet: ReportSheet }) {
const report = sheet.dayReport || sheet.nightReport;
if (!report) return null;
return (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse text-sm">
<tbody>
<tr>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black" style={{ width: '25%' }}>
Dredger Location
</td>
<td className="p-2 border-r border-black text-center" style={{ width: '25%' }}>
{report.dredgerLocation.name}
</td>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black" style={{ width: '25%' }}>
Dredger Line Length
</td>
<td className="p-2 text-center" style={{ width: '25%' }}>
{report.dredgerLineLength}
</td>
</tr>
<tr>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black border-t border-black">
Reclamation Location
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.reclamationLocation.name}
</td>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black border-t border-black">
Shore Connection
</td>
<td className="p-2 border-t border-black text-center">
{report.shoreConnection}
</td>
</tr>
<tr>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black border-t border-black">
Reclamation Height
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.reclamationHeight?.base || 0}m - {(report.reclamationHeight?.extra + report.reclamationHeight?.base || 0 || 0)}m
</td>
<td className="p-2 border-r border-black border-t border-black" colSpan={2}></td>
</tr>
</tbody>
</table>
</div>
);
}
// Pipeline Length Component (using data from either available report)
function ReportSheetPipelineLength({ sheet }: { sheet: ReportSheet }) {
const report = sheet.dayReport || sheet.nightReport;
if (!report) return null;
return (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse text-sm">
<tbody>
<tr>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black" rowSpan={2}>
Pipeline Length "from Shore Connection"
</td>
<td className="p-2 border-r border-black text-center font-semibold">Main</td>
<td className="p-2 border-r border-black text-center font-semibold">extension</td>
<td className="p-2 border-r border-black text-center font-semibold">total</td>
<td className="p-2 border-r border-black text-center font-semibold">Reserve</td>
<td className="p-2 border-r border-black text-center font-semibold">extension</td>
<td className="p-2 text-center font-semibold">total</td>
</tr>
<tr>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.pipelineLength?.main || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.pipelineLength?.ext1 || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{(report.pipelineLength?.main || 0) + (report.pipelineLength?.ext1 || 0)}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.pipelineLength?.reserve || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.pipelineLength?.ext2 || 0}
</td>
<td className="p-2 border-t border-black text-center">
{(report.pipelineLength?.reserve || 0) + (report.pipelineLength?.ext2 || 0)}
</td>
</tr>
</tbody>
</table>
</div>
);
}
// Shift Header Component
function ReportSheetShiftHeader({ shift }: { shift: string }) {
return (
<div className="bg-green-500 text-white p-2 text-center font-bold mb-2 mt-6">
{shift.charAt(0).toUpperCase() + shift.slice(1)} Shift
</div>
);
}
// Equipment Statistics Component
function ReportSheetEquipmentStats({ report }: { report: any }) {
return (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse text-sm">
<tbody>
<tr>
<td className="p-2 border-r border-black text-center font-semibold">Dozers</td>
<td className="p-2 border-r border-black text-center font-semibold">Exc.</td>
<td className="p-2 border-r border-black text-center font-semibold">Loader</td>
<td className="p-2 border-r border-black text-center font-semibold">Foreman</td>
<td className="p-2 text-center font-semibold">Laborer</td>
</tr>
<tr>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.stats?.Dozers || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.stats?.Exc || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.stats?.Loaders || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.stats?.Foreman || ''}
</td>
<td className="p-2 border-t border-black text-center">
{report.stats?.Laborer || 0}
</td>
</tr>
</tbody>
</table>
</div>
);
}
// Time Sheet Component
function ReportSheetTimeSheet({ report }: { report: any }) {
return (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse text-sm">
<thead>
<tr>
<td className="p-2 border-r border-black text-center font-semibold" rowSpan={2}>Time Sheet</td>
<td className="p-2 border-r border-black text-center font-semibold">From</td>
<td className="p-2 border-r border-black text-center font-semibold">To</td>
<td className="p-2 border-r border-black text-center font-semibold">From</td>
<td className="p-2 border-r border-black text-center font-semibold">To</td>
<td className="p-2 border-r border-black text-center font-semibold">Total</td>
<td className="p-2 text-center font-semibold">Reason</td>
</tr>
</thead>
<tbody>
{Array.isArray(report.timeSheet) && report.timeSheet.length > 0 ? (
report.timeSheet.map((entry: any, index: number) => (
<tr key={index}>
<td className="p-2 border-r border-black border-t border-black font-semibold">
{entry.machine}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.from1}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.to1}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.from2}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.to2}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.total}
</td>
<td className="p-2 border-t border-black">
{entry.reason}
</td>
</tr>
))
) : (
<tr>
<td className="p-2 border-r border-black border-t border-black text-center" colSpan={7}>
No time sheet entries
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
// Stoppages Component
function ReportSheetStoppages({ report }: { report: any }) {
return (
<>
<div className="bg-green-500 text-white p-2 text-center font-bold mb-2">
Dredger Stoppages
</div>
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse text-sm">
<thead>
<tr>
<td className="p-2 border-r border-black text-center font-semibold">From</td>
<td className="p-2 border-r border-black text-center font-semibold">To</td>
<td className="p-2 border-r border-black text-center font-semibold">Total</td>
<td className="p-2 border-r border-black text-center font-semibold">Reason</td>
<td className="p-2 border-r border-black text-center font-semibold">Responsible</td>
<td className="p-2 text-center font-semibold">Notes</td>
</tr>
</thead>
<tbody>
{Array.isArray(report.stoppages) && report.stoppages.length > 0 ? (
report.stoppages.map((entry: any, index: number) => (
<tr key={index}>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.from}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.to}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.total}
</td>
<td className="p-2 border-r border-black border-t border-black">
{entry.reason}
</td>
<td className="p-2 border-r border-black border-t border-black">
{entry.responsible}
</td>
<td className="p-2 border-t border-black">
{entry.note}
</td>
</tr>
))
) : (
<tr>
<td className="p-2 border-r border-black border-t border-black text-center" colSpan={6}>
No stoppages recorded
</td>
</tr>
)}
</tbody>
</table>
</div>
</>
);
}
// Notes Component
function ReportSheetNotes({ report }: { report: any }) {
return (
<>
<div className="bg-green-500 text-white p-2 text-center font-bold mb-2">
Notes & Comments
</div>
<div className="border-2 border-black mb-4 min-h-[100px]">
<div className="p-4 text-center">
{report.notes || 'No additional notes'}
</div>
</div>
</>
);
}
// Footer Component
function ReportSheetFooter() {
return (
<div className="text-center text-sm mt-4 border-t border-black pt-2">
{/* موقعة لأعمال الصيانة */}
</div>
);
}

View File

@ -32,7 +32,81 @@ export default function ReportViewModal({ isOpen, onClose, report }: ReportViewM
</button> </button>
<button <button
type="button" type="button"
onClick={() => window.print()} onClick={() => {
const printContent = document.getElementById('reportframe');
if (printContent) {
const printWindow = window.open('', '_blank');
if (printWindow) {
printWindow.document.write(`
<html>
<head>
<title>Report Print</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
table { border-collapse: collapse; }
.border-2 { border: 2px solid black; }
.border { border: 1px solid black; }
.border-t { border-top: 1px solid black; }
.border-r { border-right: 1px solid black; }
.border-black { border-color: black; }
.bg-green-500 {
background-color: #10b981 !important;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.text-white { color: white !important; }
.text-center { text-align: center; }
.font-bold { font-weight: bold; }
.font-semibold { font-weight: 600; }
.p-2 { padding: 8px; }
.p-4 { padding: 16px; }
.mb-2 { margin-bottom: 8px; }
.mb-4 { margin-bottom: 16px; }
.mt-1 { margin-top: 4px; }
.mt-4 { margin-top: 16px; }
.mt-6 { margin-top: 24px; }
.pt-1 { padding-top: 4px; }
.pt-2 { padding-top: 8px; }
.w-full { width: 100%; }
.h-16 { height: 64px; }
.mx-auto { margin-left: auto; margin-right: auto; }
.underline { text-decoration: underline; }
.text-sm { font-size: 14px; }
.text-lg { font-size: 18px; }
.min-h-[100px] { min-height: 100px; }
@media print {
body {
margin: 0;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.no-print { display: none; }
.bg-green-500 {
background-color: #10b981 !important;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.text-white { color: white !important; }
}
</style>
</head>
<body>
${printContent.innerHTML}
</body>
</html>
`);
printWindow.document.close();
printWindow.print();
printWindow.close();
}
}
}}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
> >
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -53,7 +127,7 @@ export default function ReportViewModal({ isOpen, onClose, report }: ReportViewM
</div> </div>
{/* ISO Standard Report Layout */} {/* ISO Standard Report Layout */}
<div className="max-h-[80vh] overflow-y-auto bg-white p-6 border border-gray-300" style={{ fontFamily: 'Arial, sans-serif' }}> <div id="reportframe" className="max-h-[80vh] overflow-y-auto bg-white p-6 border border-gray-300" style={{ fontFamily: 'Arial, sans-serif' }}>
<ReportHeader /> <ReportHeader />
<ReportInfo report={report} /> <ReportInfo report={report} />
<ReportDredgerSection report={report} /> <ReportDredgerSection report={report} />
@ -113,7 +187,7 @@ function ReportInfo({ report }: { report: any }) {
<td className="border-r border-black p-2" style={{ width: '35%' }}> <td className="border-r border-black p-2" style={{ width: '35%' }}>
{new Date(report.createdDate).toLocaleDateString('en-GB')} {new Date(report.createdDate).toLocaleDateString('en-GB')}
</td> </td>
<td className="border-r border-black p-2 font-semibold" style={{ width: '20%' }}>Report No.</td> <td className="border-r border-black p-2 font-semibold" style={{ width: '20%' }}>Shift No.</td>
<td className="p-2" style={{ width: '30%' }}>{report.id}</td> <td className="p-2" style={{ width: '30%' }}>{report.id}</td>
</tr> </tr>
</tbody> </tbody>

View File

@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout"; import DashboardLayout from "~/components/DashboardLayout";
import FormModal from "~/components/FormModal"; import FormModal from "~/components/FormModal";
import Toast from "~/components/Toast"; import Toast from "~/components/Toast";
import { PrismaClient } from "@prisma/client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
const prisma = new PrismaClient();
export const meta: MetaFunction = () => [{ title: "Areas Management - Phosphat Report" }]; export const meta: MetaFunction = () => [{ title: "Areas Management - Phosphat Report" }];

View File

@ -3,9 +3,7 @@ import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react"; import { useLoaderData } from "@remix-run/react";
import { requireAuthLevel } from "~/utils/auth.server"; import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout"; import DashboardLayout from "~/components/DashboardLayout";
import { PrismaClient } from "@prisma/client"; import { prisma } from "~/utils/db.server";
const prisma = new PrismaClient();
export const meta: MetaFunction = () => [{ title: "Dashboard - Phosphat Report" }]; export const meta: MetaFunction = () => [{ title: "Dashboard - Phosphat Report" }];

View File

@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout"; import DashboardLayout from "~/components/DashboardLayout";
import FormModal from "~/components/FormModal"; import FormModal from "~/components/FormModal";
import Toast from "~/components/Toast"; import Toast from "~/components/Toast";
import { PrismaClient } from "@prisma/client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
const prisma = new PrismaClient();
export const meta: MetaFunction = () => [{ title: "Dredger Locations Management - Phosphat Report" }]; export const meta: MetaFunction = () => [{ title: "Dredger Locations Management - Phosphat Report" }];

View File

@ -5,11 +5,9 @@ import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout"; import DashboardLayout from "~/components/DashboardLayout";
import FormModal from "~/components/FormModal"; import FormModal from "~/components/FormModal";
import Toast from "~/components/Toast"; import Toast from "~/components/Toast";
import { PrismaClient } from "@prisma/client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { prisma } from "~/utils/db.server";
const prisma = new PrismaClient();
export const meta: MetaFunction = () => [{ title: "Employee Management - Phosphat Report" }]; export const meta: MetaFunction = () => [{ title: "Employee Management - Phosphat Report" }];
@ -46,6 +44,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const email = formData.get("email"); const email = formData.get("email");
const password = formData.get("password"); const password = formData.get("password");
const authLevel = formData.get("authLevel"); const authLevel = formData.get("authLevel");
const status = formData.get("status");
if (intent === "create") { if (intent === "create") {
if (typeof name !== "string" || name.length === 0) { if (typeof name !== "string" || name.length === 0) {
@ -82,7 +81,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {
username, username,
email, email,
password: hashedPassword, password: hashedPassword,
authLevel: parseInt(authLevel) authLevel: parseInt(authLevel),
status: 'active'
} }
}); });
return json({ success: "Employee created successfully!" }); return json({ success: "Employee created successfully!" });
@ -141,6 +141,13 @@ export const action = async ({ request }: ActionFunctionArgs) => {
authLevel: parseInt(authLevel) authLevel: parseInt(authLevel)
}; };
// Add status if provided (but prevent users from changing their own status)
if (typeof status === "string" && ["active", "inactive"].includes(status)) {
if (parseInt(id) !== user.id) {
updateData.status = status;
}
}
// Only update password if provided // Only update password if provided
if (typeof password === "string" && password.length >= 6) { if (typeof password === "string" && password.length >= 6) {
updateData.password = bcrypt.hashSync(password, 10); updateData.password = bcrypt.hashSync(password, 10);
@ -158,6 +165,45 @@ export const action = async ({ request }: ActionFunctionArgs) => {
} }
} }
if (intent === "toggleStatus") {
if (typeof id !== "string") {
return json({ errors: { form: "Invalid employee ID" } }, { status: 400 });
}
const employeeId = parseInt(id);
// Prevent users from changing their own status
if (employeeId === user.id) {
return json({ errors: { form: "You cannot change your own status" } }, { status: 403 });
}
// Check if the employee exists and if current user can edit them
const existingEmployee = await prisma.employee.findUnique({
where: { id: employeeId },
select: { authLevel: true, status: true }
});
if (!existingEmployee) {
return json({ errors: { form: "Employee not found" } }, { status: 404 });
}
// Level 2 users cannot edit Level 3 employees
if (user.authLevel === 2 && existingEmployee.authLevel === 3) {
return json({ errors: { form: "You don't have permission to edit Super Admin users" } }, { status: 403 });
}
try {
const newStatus = existingEmployee.status === 'active' ? 'inactive' : 'active';
await prisma.employee.update({
where: { id: employeeId },
data: { status: newStatus }
});
return json({ success: `Employee status changed to ${newStatus}!` });
} catch (error) {
return json({ errors: { form: "Failed to update employee status" } }, { status: 400 });
}
}
if (intent === "delete") { if (intent === "delete") {
if (typeof id !== "string") { if (typeof id !== "string") {
return json({ errors: { form: "Invalid employee ID" } }, { status: 400 }); return json({ errors: { form: "Invalid employee ID" } }, { status: 400 });
@ -195,7 +241,7 @@ export default function Employees() {
const { user, employees } = useLoaderData<typeof loader>(); const { user, employees } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>(); const actionData = useActionData<typeof action>();
const navigation = useNavigation(); const navigation = useNavigation();
const [editingEmployee, setEditingEmployee] = useState<{ id: number; name: string; username: string; email: string; authLevel: number } | null>(null); const [editingEmployee, setEditingEmployee] = useState<{ id: number; name: string; username: string; email: string; authLevel: number; status: string } | null>(null);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
@ -213,7 +259,7 @@ export default function Employees() {
} }
}, [actionData]); }, [actionData]);
const handleEdit = (employee: { id: number; name: string; username: string; email: string; authLevel: number }) => { const handleEdit = (employee: { id: number; name: string; username: string; email: string; authLevel: number; status: string }) => {
setEditingEmployee(employee); setEditingEmployee(employee);
setShowModal(true); setShowModal(true);
}; };
@ -275,6 +321,24 @@ export default function Employees() {
} }
}; };
const getStatusBadge = (status: string) => {
return status === 'active'
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800";
};
const getStatusIcon = (status: string) => {
return status === 'active' ? (
<svg className="h-4 w-4" 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="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
};
return ( return (
<DashboardLayout user={user}> <DashboardLayout user={user}>
<div className="space-y-6"> <div className="space-y-6">
@ -312,6 +376,9 @@ export default function Employees() {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Access Level Access Level
</th> </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"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Reports Count Reports Count
</th> </th>
@ -347,6 +414,12 @@ export default function Employees() {
Level {employee.authLevel} - {getAuthLevelText(employee.authLevel)} Level {employee.authLevel} - {getAuthLevelText(employee.authLevel)}
</span> </span>
</td> </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 ${getStatusBadge(employee.status)}`}>
{getStatusIcon(employee.status)}
<span className="ml-1 capitalize">{employee.status}</span>
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap"> <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 bg-blue-100 text-blue-800"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{employee._count.reports} reports {employee._count.reports} reports
@ -361,6 +434,21 @@ export default function Employees() {
Edit Edit
</button> </button>
{employee.id !== user.id && ( {employee.id !== user.id && (
<>
<Form method="post" className="inline">
<input type="hidden" name="intent" value="toggleStatus" />
<input type="hidden" name="id" value={employee.id} />
<button
type="submit"
className={`transition-colors duration-150 ${
employee.status === 'active'
? 'text-orange-600 hover:text-orange-900'
: 'text-green-600 hover:text-green-900'
}`}
>
{employee.status === 'active' ? 'Deactivate' : 'Activate'}
</button>
</Form>
<Form method="post" className="inline"> <Form method="post" className="inline">
<input type="hidden" name="intent" value="delete" /> <input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={employee.id} /> <input type="hidden" name="id" value={employee.id} />
@ -376,6 +464,7 @@ export default function Employees() {
Delete Delete
</button> </button>
</Form> </Form>
</>
)} )}
</div> </div>
</td> </td>
@ -516,6 +605,23 @@ export default function Employees() {
<p className="mt-1 text-sm text-red-600">{actionData.errors.authLevel}</p> <p className="mt-1 text-sm text-red-600">{actionData.errors.authLevel}</p>
)} )}
</div> </div>
{isEditing && editingEmployee?.id !== user.id && (
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 mb-2">
Status
</label>
<select
name="status"
id="status"
defaultValue={editingEmployee?.status || "active"}
className="block w-full border-gray-300 h-9 p-2 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
)}
</div> </div>
{isEditing && ( {isEditing && (

View File

@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout"; import DashboardLayout from "~/components/DashboardLayout";
import FormModal from "~/components/FormModal"; import FormModal from "~/components/FormModal";
import Toast from "~/components/Toast"; import Toast from "~/components/Toast";
import { PrismaClient } from "@prisma/client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
const prisma = new PrismaClient();
export const meta: MetaFunction = () => [{ title: "Equipment Management - Phosphat Report" }]; export const meta: MetaFunction = () => [{ title: "Equipment Management - Phosphat Report" }];

View File

@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout"; import DashboardLayout from "~/components/DashboardLayout";
import FormModal from "~/components/FormModal"; import FormModal from "~/components/FormModal";
import Toast from "~/components/Toast"; import Toast from "~/components/Toast";
import { PrismaClient } from "@prisma/client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
const prisma = new PrismaClient();
export const meta: MetaFunction = () => [{ title: "Foreman Management - Phosphat Report" }]; export const meta: MetaFunction = () => [{ title: "Foreman Management - Phosphat Report" }];

View File

@ -1,12 +1,10 @@
import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { Form, useActionData, useLoaderData } from "@remix-run/react"; import { Form, useActionData, useLoaderData } from "@remix-run/react";
import { PrismaClient } from "@prisma/client";
import { requireAuthLevel } from "~/utils/auth.server"; import { requireAuthLevel } from "~/utils/auth.server";
import { testEmailConnection } from "~/utils/mail.server"; import { testEmailConnection } from "~/utils/mail.server";
import DashboardLayout from "~/components/DashboardLayout"; import DashboardLayout from "~/components/DashboardLayout";
import { useState } from "react"; import { useState } from "react";
import { prisma } from "~/utils/db.server";
const prisma = new PrismaClient();
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
// Require auth level 3 to access mail settings // Require auth level 3 to access mail settings

View File

@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout"; import DashboardLayout from "~/components/DashboardLayout";
import FormModal from "~/components/FormModal"; import FormModal from "~/components/FormModal";
import Toast from "~/components/Toast"; import Toast from "~/components/Toast";
import { PrismaClient } from "@prisma/client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
const prisma = new PrismaClient();
export const meta: MetaFunction = () => [{ title: "Reclamation Locations Management - Phosphat Report" }]; export const meta: MetaFunction = () => [{ title: "Reclamation Locations Management - Phosphat Report" }];

240
app/routes/report-sheet.tsx Normal file
View File

@ -0,0 +1,240 @@
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>
);
}

View File

@ -6,10 +6,9 @@ import DashboardLayout from "~/components/DashboardLayout";
import ReportViewModal from "~/components/ReportViewModal"; import ReportViewModal from "~/components/ReportViewModal";
import ReportFormModal from "~/components/ReportFormModal"; import ReportFormModal from "~/components/ReportFormModal";
import Toast from "~/components/Toast"; import Toast from "~/components/Toast";
import { PrismaClient } from "@prisma/client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { manageSheet, removeFromSheet } from "~/utils/sheet.server";
const prisma = new PrismaClient(); import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Reports Management - Phosphat Report" }]; export const meta: MetaFunction = () => [{ title: "Reports Management - Phosphat Report" }];
@ -84,7 +83,14 @@ export const action = async ({ request }: ActionFunctionArgs) => {
// Check if user owns this report or has admin privileges // Check if user owns this report or has admin privileges
const existingReport = await prisma.report.findUnique({ const existingReport = await prisma.report.findUnique({
where: { id: parseInt(id) }, where: { id: parseInt(id) },
select: { employeeId: true, createdDate: true } select: {
employeeId: true,
createdDate: true,
shift: true,
areaId: true,
dredgerLocationId: true,
reclamationLocationId: true
}
}); });
if (!existingReport) { if (!existingReport) {
@ -173,7 +179,21 @@ export const action = async ({ request }: ActionFunctionArgs) => {
} }
} }
await prisma.report.update({ // 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) }, where: { id: parseInt(id) },
data: { data: {
shift, shift,
@ -204,6 +224,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {
notes: notes || null 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!" }); return json({ success: "Report updated successfully!" });
} catch (error) { } catch (error) {
return json({ errors: { form: "Failed to update report" } }, { status: 400 }); return json({ errors: { form: "Failed to update report" } }, { status: 400 });
@ -218,7 +248,14 @@ export const action = async ({ request }: ActionFunctionArgs) => {
// Check if user owns this report or has admin privileges // Check if user owns this report or has admin privileges
const existingReport = await prisma.report.findUnique({ const existingReport = await prisma.report.findUnique({
where: { id: parseInt(id) }, where: { id: parseInt(id) },
select: { employeeId: true, createdDate: true } select: {
employeeId: true,
createdDate: true,
shift: true,
areaId: true,
dredgerLocationId: true,
reclamationLocationId: true
}
}); });
if (!existingReport) { if (!existingReport) {
@ -244,6 +281,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {
} }
try { 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({ await prisma.report.delete({
where: { id: parseInt(id) } where: { id: parseInt(id) }
}); });
@ -495,8 +542,8 @@ export default function Reports() {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-2xl font-bold text-gray-900">Reports Management</h1> <h1 className="text-2xl font-bold text-gray-900">Shifts Management</h1>
<p className="mt-1 text-sm text-gray-600">Create and manage operational reports</p> <p className="mt-1 text-sm text-gray-600">Create and manage operational shifts</p>
</div> </div>
<Link <Link
to="/reports/new" to="/reports/new"
@ -505,7 +552,7 @@ export default function Reports() {
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg> </svg>
Create New Report Create New Shift
</Link> </Link>
</div> </div>
@ -516,7 +563,7 @@ export default function Reports() {
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Report Details Shift Details
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Shift & Area Shift & Area
@ -545,7 +592,7 @@ export default function Reports() {
</div> </div>
</div> </div>
<div className="ml-4"> <div className="ml-4">
<div className="text-sm font-medium text-gray-900">Report #{report.id}</div> <div className="text-sm font-medium text-gray-900">Shift #{report.id}</div>
<div className="text-sm text-gray-500">by {report.employee.name}</div> <div className="text-sm text-gray-500">by {report.employee.name}</div>
</div> </div>
</div> </div>
@ -623,7 +670,7 @@ export default function Reports() {
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg> </svg>
Create Report Create Shifs
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -3,10 +3,9 @@ import { json, redirect } from "@remix-run/node";
import { Form, useActionData, useLoaderData, useNavigation, Link } from "@remix-run/react"; import { Form, useActionData, useLoaderData, useNavigation, Link } from "@remix-run/react";
import { requireAuthLevel } from "~/utils/auth.server"; import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout"; import DashboardLayout from "~/components/DashboardLayout";
import { PrismaClient } from "@prisma/client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { manageSheet } from "~/utils/sheet.server";
const prisma = new PrismaClient(); import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "New Report - Phosphat Report" }]; export const meta: MetaFunction = () => [{ title: "New Report - Phosphat Report" }];
@ -107,7 +106,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
} }
} }
await prisma.report.create({ const report = await prisma.report.create({
data: { data: {
employeeId: user.id, employeeId: user.id,
shift, shift,
@ -139,6 +138,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {
} }
}); });
// Manage sheet creation/update
await manageSheet(
report.id,
shift,
parseInt(areaId),
parseInt(dredgerLocationId),
parseInt(reclamationLocationId),
report.createdDate
);
// Redirect to reports page with success message // Redirect to reports page with success message
return redirect("/reports?success=Report created successfully!"); return redirect("/reports?success=Report created successfully!");
} catch (error) { } catch (error) {
@ -378,8 +387,8 @@ export default function NewReport() {
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Create New Report</h1> <h1 className="text-3xl font-bold text-gray-900">Create New Shifts</h1>
<p className="mt-2 text-gray-600">Fill out the operational report details step by step</p> <p className="mt-2 text-gray-600">Fill out the operational shift details step by step</p>
</div> </div>
<Link <Link
to="/reports" to="/reports"
@ -388,7 +397,7 @@ export default function NewReport() {
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg> </svg>
Back to Reports Back to Shifts
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -2,9 +2,7 @@ import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remi
import { json, redirect } from "@remix-run/node"; import { json, redirect } from "@remix-run/node";
import { Form, Link, useActionData } from "@remix-run/react"; import { Form, Link, useActionData } from "@remix-run/react";
import { createUser, createUserSession, getUserId } from "~/utils/auth.server"; import { createUser, createUserSession, getUserId } from "~/utils/auth.server";
import { PrismaClient } from "@prisma/client"; import { prisma } from "~/utils/db.server";
const prisma = new PrismaClient();
export const meta: MetaFunction = () => [{ title: "Sign Up - Phosphat Report" }]; export const meta: MetaFunction = () => [{ title: "Sign Up - Phosphat Report" }];

View File

@ -1,8 +1,6 @@
import { createCookieSessionStorage, redirect } from "@remix-run/node"; import { createCookieSessionStorage, redirect } from "@remix-run/node";
import { PrismaClient } from "@prisma/client";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { prisma } from "~/utils/db.server";
const prisma = new PrismaClient();
// Session storage // Session storage
const sessionSecret = process.env.SESSION_SECRET || "default-secret"; const sessionSecret = process.env.SESSION_SECRET || "default-secret";
@ -58,8 +56,14 @@ export async function getUser(request: Request) {
try { try {
const user = await prisma.employee.findUnique({ const user = await prisma.employee.findUnique({
where: { id: userId }, where: { id: userId },
select: { id: true, name: true, username: true, email: true, authLevel: true }, select: { id: true, name: true, username: true, email: true, authLevel: true, status: true },
}); });
// If user is inactive, logout and return null
if (user && user.status === 'inactive') {
throw logout(request);
}
return user; return user;
} catch { } catch {
throw logout(request); throw logout(request);
@ -109,7 +113,12 @@ export async function verifyLogin(usernameOrEmail: string, password: string) {
return null; return null;
} }
return { id: user.id, username: user.username, email: user.email, name: user.name, authLevel: user.authLevel }; // Check if user is inactive
if (user.status === 'inactive') {
return null;
}
return { id: user.id, username: user.username, email: user.email, name: user.name, authLevel: user.authLevel, status: user.status };
} }
export async function createUser(name: string, username: string, email: string, password: string, authLevel: number = 1) { export async function createUser(name: string, username: string, email: string, password: string, authLevel: number = 1) {
@ -122,8 +131,9 @@ export async function createUser(name: string, username: string, email: string,
email, email,
password: hashedPassword, password: hashedPassword,
authLevel, authLevel,
status: 'active',
}, },
}); });
return { id: user.id, username: user.username, email: user.email, name: user.name, authLevel: user.authLevel }; return { id: user.id, username: user.username, email: user.email, name: user.name, authLevel: user.authLevel, status: user.status };
} }

121
app/utils/sheet.server.ts Normal file
View File

@ -0,0 +1,121 @@
import { prisma } from "~/utils/db.server";
export async function manageSheet(reportId: number, shift: string, areaId: number, dredgerLocationId: number, reclamationLocationId: number, createdDate: Date) {
// Format date as YYYY-MM-DD
const dateString = createdDate.toISOString().split('T')[0];
try {
// Check if a sheet already exists for this combination
const existingSheet = await prisma.sheet.findUnique({
where: {
areaId_dredgerLocationId_reclamationLocationId_date: {
areaId,
dredgerLocationId,
reclamationLocationId,
date: dateString
}
}
});
if (existingSheet) {
// Sheet exists, update it with the new shift
if (shift === 'day' && !existingSheet.dayShiftId) {
await prisma.sheet.update({
where: { id: existingSheet.id },
data: {
dayShiftId: reportId,
status: existingSheet.nightShiftId ? 'completed' : 'pending'
}
});
} else if (shift === 'night' && !existingSheet.nightShiftId) {
await prisma.sheet.update({
where: { id: existingSheet.id },
data: {
nightShiftId: reportId,
status: existingSheet.dayShiftId ? 'completed' : 'pending'
}
});
}
} else {
// No sheet exists, create a new one
const sheetData: any = {
areaId,
dredgerLocationId,
reclamationLocationId,
date: dateString,
status: 'pending'
};
if (shift === 'day') {
sheetData.dayShiftId = reportId;
} else if (shift === 'night') {
sheetData.nightShiftId = reportId;
}
await prisma.sheet.create({
data: sheetData
});
}
} catch (error) {
console.error('Error managing sheet:', error);
throw error;
}
}
export async function removeFromSheet(reportId: number, shift: string, areaId: number, dredgerLocationId: number, reclamationLocationId: number, createdDate: Date) {
// Format date as YYYY-MM-DD
const dateString = createdDate.toISOString().split('T')[0];
try {
// Find the sheet for this combination
const existingSheet = await prisma.sheet.findUnique({
where: {
areaId_dredgerLocationId_reclamationLocationId_date: {
areaId,
dredgerLocationId,
reclamationLocationId,
date: dateString
}
}
});
if (existingSheet) {
if (shift === 'day' && existingSheet.dayShiftId === reportId) {
if (existingSheet.nightShiftId) {
// Keep sheet but remove day shift
await prisma.sheet.update({
where: { id: existingSheet.id },
data: {
dayShiftId: null,
status: 'pending'
}
});
} else {
// Delete sheet if no other shift
await prisma.sheet.delete({
where: { id: existingSheet.id }
});
}
} else if (shift === 'night' && existingSheet.nightShiftId === reportId) {
if (existingSheet.dayShiftId) {
// Keep sheet but remove night shift
await prisma.sheet.update({
where: { id: existingSheet.id },
data: {
nightShiftId: null,
status: 'pending'
}
});
} else {
// Delete sheet if no other shift
await prisma.sheet.delete({
where: { id: existingSheet.id }
});
}
}
}
} catch (error) {
console.error('Error removing from sheet:', error);
throw error;
}
}

Binary file not shown.

1
prisma/dev.sqbpro Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><sqlb_project><db path="dev.db" readonly="0" foreign_keys="1" case_sensitive_like="0" temp_store="0" wal_autocheckpoint="1000" synchronous="2"/><attached/><window><main_tabs open="structure browser pragmas query" current="1"/></window><tab_structure><column_width id="0" width="300"/><column_width id="1" width="0"/><column_width id="2" width="100"/><column_width id="3" width="7352"/><column_width id="4" width="0"/><expanded_item id="0" parent="1"/><expanded_item id="1" parent="1"/><expanded_item id="2" parent="1"/><expanded_item id="3" parent="1"/></tab_structure><tab_browse><table title="Report" custom_title="0" dock_id="3" table="4,6:mainReport"/><table title="Report" custom_title="0" dock_id="2" table="4,6:mainReport"/><table title="Area" custom_title="0" dock_id="1" table="4,4:mainArea"/><table title="Area" custom_title="0" dock_id="4" table="4,4:mainArea"/><dock_state state="000000ff00000000fd00000001000000020000043b000002aefc0100000001fc000000000000043b0000011800fffffffa000000030100000004fb000000160064006f0063006b00420072006f00770073006500310100000000ffffffff0000011800fffffffb000000160064006f0063006b00420072006f00770073006500320100000000ffffffff0000011800fffffffb000000160064006f0063006b00420072006f00770073006500330100000000ffffffff0000011800fffffffb000000160064006f0063006b00420072006f00770073006500340100000000ffffffff0000011800ffffff000002580000000000000004000000040000000800000008fc00000000"/><default_encoding codec=""/><browse_table_settings><table schema="main" name="Area" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk="_rowid_" freeze_columns="0"><sort/><column_widths><column index="1" value="29"/><column index="2" value="54"/></column_widths><filter_values/><conditional_formats/><row_id_formats/><display_formats/><hidden_columns/><plot_y_axes/><global_filter/></table><table schema="main" name="Report" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk="_rowid_" freeze_columns="0"><sort/><column_widths><column index="1" value="29"/><column index="2" value="76"/><column index="3" value="109"/><column index="4" value="109"/><column index="5" value="46"/><column index="6" value="47"/><column index="7" value="113"/><column index="8" value="113"/><column index="9" value="136"/><column index="10" value="101"/><column index="11" value="171"/><column index="12" value="300"/><column index="13" value="300"/><column index="14" value="300"/><column index="15" value="300"/><column index="16" value="93"/></column_widths><filter_values/><conditional_formats/><row_id_formats/><display_formats/><hidden_columns/><plot_y_axes/><global_filter/></table></browse_table_settings></tab_browse><tab_sql><sql name="SQL 1"></sql><current_tab id="0"/></tab_sql></sqlb_project>

View File

@ -0,0 +1,41 @@
-- CreateTable
CREATE TABLE "Sheet" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"dayShiftId" INTEGER,
"nightShiftId" INTEGER,
"status" TEXT NOT NULL DEFAULT 'pending',
"areaId" INTEGER NOT NULL,
"dredgerLocationId" INTEGER NOT NULL,
"reclamationLocationId" INTEGER NOT NULL,
"date" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Sheet_areaId_fkey" FOREIGN KEY ("areaId") REFERENCES "Area" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Sheet_dredgerLocationId_fkey" FOREIGN KEY ("dredgerLocationId") REFERENCES "DredgerLocation" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Sheet_reclamationLocationId_fkey" FOREIGN KEY ("reclamationLocationId") REFERENCES "ReclamationLocation" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Sheet_dayShiftId_fkey" FOREIGN KEY ("dayShiftId") REFERENCES "Report" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Sheet_nightShiftId_fkey" FOREIGN KEY ("nightShiftId") REFERENCES "Report" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Employee" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"authLevel" INTEGER NOT NULL,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'active'
);
INSERT INTO "new_Employee" ("authLevel", "email", "id", "name", "password", "username") SELECT "authLevel", "email", "id", "name", "password", "username" FROM "Employee";
DROP TABLE "Employee";
ALTER TABLE "new_Employee" RENAME TO "Employee";
CREATE UNIQUE INDEX "Employee_username_key" ON "Employee"("username");
CREATE UNIQUE INDEX "Employee_email_key" ON "Employee"("email");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
-- CreateIndex
CREATE UNIQUE INDEX "Sheet_areaId_dredgerLocationId_reclamationLocationId_date_key" ON "Sheet"("areaId", "dredgerLocationId", "reclamationLocationId", "date");

View File

@ -31,12 +31,17 @@ model Report {
timeSheet Json // JSON: Array of timesheet objects timeSheet Json // JSON: Array of timesheet objects
stoppages Json // JSON: Array of stoppage records stoppages Json // JSON: Array of stoppage records
notes String? notes String?
// Sheet relations
daySheetFor Sheet[] @relation("DayShift")
nightSheetFor Sheet[] @relation("NightShift")
} }
model Area { model Area {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @unique name String @unique
reports Report[] reports Report[]
sheets Sheet[]
} }
model DredgerLocation { model DredgerLocation {
@ -44,12 +49,14 @@ model DredgerLocation {
name String @unique name String @unique
class String // 's', 'd', 'sp' class String // 's', 'd', 'sp'
reports Report[] reports Report[]
sheets Sheet[]
} }
model ReclamationLocation { model ReclamationLocation {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @unique name String @unique
reports Report[] reports Report[]
sheets Sheet[]
} }
model Employee { model Employee {
@ -59,6 +66,7 @@ model Employee {
username String @unique username String @unique
email String @unique email String @unique
password String password String
status String @default("active") // 'active' or 'inactive'
reports Report[] reports Report[]
passwordResetTokens PasswordResetToken[] passwordResetTokens PasswordResetToken[]
} }
@ -85,6 +93,28 @@ model Equipment {
number Int number Int
} }
model Sheet {
id Int @id @default(autoincrement())
dayShiftId Int?
nightShiftId Int?
status String @default("pending") // 'pending', 'completed'
areaId Int
dredgerLocationId Int
reclamationLocationId Int
date String // Store as string in YYYY-MM-DD format
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
area Area @relation(fields: [areaId], references: [id])
dredgerLocation DredgerLocation @relation(fields: [dredgerLocationId], references: [id])
reclamationLocation ReclamationLocation @relation(fields: [reclamationLocationId], references: [id])
dayShift Report? @relation("DayShift", fields: [dayShiftId], references: [id])
nightShift Report? @relation("NightShift", fields: [nightShiftId], references: [id])
@@unique([areaId, dredgerLocationId, reclamationLocationId, date])
}
model MailSettings { model MailSettings {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
host String host String

View File

@ -86,7 +86,8 @@ async function main() {
authLevel: 3, authLevel: 3,
username: process.env.SUPER_ADMIN, username: process.env.SUPER_ADMIN,
email: process.env.SUPER_ADMIN_EMAIL, email: process.env.SUPER_ADMIN_EMAIL,
password: bcrypt.hashSync(process.env.SUPER_ADMIN_PASSWORD, 10) password: bcrypt.hashSync(process.env.SUPER_ADMIN_PASSWORD, 10),
status: 'active'
} }
}) })