This commit is contained in:
yznahmad 2025-09-07 04:05:44 +03:00
parent 86f3fa7f1d
commit 7580162816
20 changed files with 973 additions and 45 deletions

227
CARRY_FORWARD_FEATURE.md Normal file
View File

@ -0,0 +1,227 @@
# Carry Forward Feature
## Overview
Added a "CarryTo" functionality that allows users to carry forward (clone) existing reports to today's date. This feature is useful for continuing operations with similar configurations from previous days.
## Features
### ✅ **Core Functionality**
1. **Clone Report**: Creates an exact copy of an existing report with today's date
2. **Smart Validation**: Prevents carrying forward reports from today or future dates
3. **Conflict Prevention**: Checks for existing reports with same configuration for today
4. **Sheet Management**: Automatically manages sheet creation and completion status
5. **User Assignment**: Assigns the carried forward report to the current user
### ✅ **Business Logic**
- **Source Reports**: Only reports from previous days can be carried forward
- **Target Date**: Always creates the new report with today's date and current time
- **Data Preservation**: Copies all report data except stoppages (starts fresh for new day)
- **Sheet Completion**: Automatically marks sheets as "completed" when both day and night shifts exist
## User Interface
### ✅ **Desktop Actions**
```
[View] [Duplicate] [CarryTo] [Edit] [Delete]
```
### ✅ **Mobile Actions**
```
[View Details]
[Duplicate as Night Shift]
[Carry Forward to Today]
[Edit] [Delete]
```
### ✅ **Visual Design**
- **Color**: Purple theme to distinguish from duplicate (green)
- **Icon**: Forward arrow or calendar icon
- **Confirmation**: User confirmation dialog before execution
- **Feedback**: Success/error messages via toast notifications
## Technical Implementation
### ✅ **Validation Logic**
```typescript
const canCarryForwardReport = (report: any) => {
const reportDate = new Date(report.createdDate);
const today = new Date();
today.setHours(0, 0, 0, 0);
reportDate.setHours(0, 0, 0, 0);
// Cannot carry forward reports from today or future
if (reportDate >= today) {
return false;
}
return true;
};
```
### ✅ **Server-Side Processing**
```typescript
// 1. Validate source report exists and is from past
// 2. Check for existing report with same configuration today
// 3. Create new report with today's date
// 4. Manage sheet creation/updates
// 5. Check and update sheet completion status
```
### ✅ **Data Cloning**
```typescript
const carriedReport = await prisma.report.create({
data: {
employeeId: user.id, // Current user
shift: originalReport.shift, // Same 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: [], // Fresh start for new day
notes: originalReport.notes,
createdDate: new Date() // Today's date
}
});
```
## Business Rules
### ✅ **Eligibility Criteria**
1. **Date Restriction**: Source report must be from a previous day
2. **User Access**: All authenticated users can carry forward reports
3. **Conflict Prevention**: Cannot create if same configuration exists for today
4. **Data Integrity**: Maintains referential integrity with areas, locations, etc.
### ✅ **What Gets Copied**
- ✅ Shift type (day/night)
- ✅ Area and location assignments
- ✅ Equipment configurations
- ✅ Pipeline lengths and heights
- ✅ Statistics and personnel counts
- ✅ Time sheet entries
- ✅ Notes and comments
### ✅ **What Gets Reset**
- ❌ Stoppages (starts with empty array)
- ❌ Creation date (set to current date/time)
- ❌ Employee assignment (assigned to current user)
## Sheet Management
### ✅ **Automatic Sheet Creation**
- Creates or updates sheet for today's date
- Links the carried forward report to appropriate shift slot
- Maintains proper sheet structure and relationships
### ✅ **Completion Logic**
```typescript
// Check if sheet has both day and night shifts
if (todaySheet && todaySheet.dayShiftId && todaySheet.nightShiftId) {
await prisma.sheet.update({
where: { id: todaySheet.id },
data: { status: 'completed' }
});
}
```
## Use Cases
### **Operational Scenarios**
1. **Continuous Operations**
- Carry forward yesterday's day shift to today
- Maintain same equipment and location setup
- Start fresh with stoppages for new day
2. **Shift Handover**
- Night shift carries forward day shift configuration
- Ensures consistency in operational setup
- Reduces setup time for similar operations
3. **Recurring Operations**
- Weekly operations with similar patterns
- Monthly maintenance cycles
- Seasonal operational configurations
### **Workflow Examples**
1. **Daily Operations**
```
Yesterday Day Shift → [CarryTo] → Today Day Shift
- Same location, equipment, personnel
- Fresh stoppages tracking
- Current user assignment
```
2. **Shift Completion**
```
Day Shift Carried Forward → Night Shift Created → Sheet Completed
- Automatic sheet status update
- Complete operational cycle
- Ready for reporting
```
## Error Handling
### ✅ **Validation Errors**
- **Future Date**: "Cannot carry forward reports from today or future dates"
- **Duplicate Configuration**: "A [shift] shift report already exists for today with the same location configuration"
- **Missing Report**: "Report not found"
- **System Error**: "Failed to carry forward report"
### ✅ **User Feedback**
- **Success**: "Report carried forward to today as [shift] shift!"
- **Confirmation**: User must confirm before execution
- **Visual Indicators**: Button states show availability
- **Toast Notifications**: Success/error messages
## Security & Permissions
### ✅ **Access Control**
- All authenticated users (auth level 1+) can carry forward reports
- Users can carry forward any report, not just their own
- Carried forward report is assigned to current user
- Maintains audit trail with creation timestamps
### ✅ **Data Validation**
- Server-side validation of all business rules
- Prevents duplicate configurations for same day
- Ensures data integrity and consistency
- Proper error handling and user feedback
## Performance Considerations
### ✅ **Database Operations**
- Single transaction for report creation and sheet management
- Efficient queries for validation checks
- Proper indexing on date and location fields
- Minimal data transfer with focused queries
### ✅ **User Experience**
- Fast execution with immediate feedback
- Clear visual indicators for button availability
- Intuitive confirmation dialogs
- Consistent behavior across desktop and mobile
## Future Enhancements
### **Potential Improvements**
1. **Bulk Carry Forward**: Carry forward multiple reports at once
2. **Scheduled Carry Forward**: Automatic carry forward for recurring operations
3. **Template System**: Save common configurations as templates
4. **Selective Data Copy**: Choose which data elements to carry forward
5. **Carry Forward History**: Track which reports were carried forward from where
### **Advanced Features**
1. **Smart Suggestions**: Suggest reports to carry forward based on patterns
2. **Batch Operations**: Carry forward entire sheets or multiple shifts
3. **Custom Date Selection**: Carry forward to specific future dates
4. **Approval Workflow**: Require approval for certain carry forward operations
5. **Integration**: Connect with scheduling systems for automated carry forward
The Carry Forward feature streamlines operational continuity by allowing users to quickly replicate successful operational configurations from previous days, reducing setup time and ensuring consistency in operations."

View File

@ -1,6 +1,6 @@
import { Form, Link, useLocation } from "@remix-run/react";
import type { Employee } from "@prisma/client";
import { useState } from "react";
import { useState, useEffect } from "react";
interface DashboardLayoutProps {
children: React.ReactNode;
@ -9,14 +9,15 @@ interface DashboardLayoutProps {
export default function DashboardLayout({ children, user }: DashboardLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
// Initialize from localStorage if available
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('sidebar-collapsed');
return saved ? JSON.parse(saved) : false;
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
// Initialize sidebar state from localStorage after hydration
useEffect(() => {
const saved = localStorage.getItem('sidebar-collapsed');
if (saved) {
setSidebarCollapsed(JSON.parse(saved));
}
return false;
});
}, []);
const toggleSidebar = () => setSidebarOpen(!sidebarOpen);
@ -24,9 +25,7 @@ export default function DashboardLayout({ children, user }: DashboardLayoutProps
const newCollapsed = !sidebarCollapsed;
setSidebarCollapsed(newCollapsed);
// Persist to localStorage
if (typeof window !== 'undefined') {
localStorage.setItem('sidebar-collapsed', JSON.stringify(newCollapsed));
}
localStorage.setItem('sidebar-collapsed', JSON.stringify(newCollapsed));
};
return (
@ -69,7 +68,7 @@ export default function DashboardLayout({ children, user }: DashboardLayoutProps
<img
className="h-14 w-auto"
src="/logo03.png"
alt="Report System"
alt="Phosphat Report"
/>
</div>
@ -177,8 +176,8 @@ function SidebarContent({
onItemClick();
}}
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md transition-colors duration-150 ${active
? 'bg-indigo-100 text-indigo-900 border-r-2 border-indigo-500'
: 'text-gray-900 hover:bg-gray-50 hover:text-gray-900'
? 'bg-indigo-100 text-indigo-900 border-r-2 border-indigo-500'
: 'text-gray-900 hover:bg-gray-50 hover:text-gray-900'
}`}
title={collapsed ? children?.toString() : undefined}
>

View File

@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Areas Management - Alhaffer Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Areas Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);

View File

@ -5,7 +5,7 @@ import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Dashboard - Alhaffer Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Dashboard - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 1);

View File

@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Dredger Locations Management - Alhaffer Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Dredger Locations Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);

View File

@ -9,7 +9,7 @@ import { useState, useEffect } from "react";
import bcrypt from "bcryptjs";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Employee Management - Alhaffer Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Employee Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);

View File

@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Equipment Management - Alhaffer Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Equipment Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);

View File

@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Foreman Management - Alhaffer Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Foreman Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);

View File

@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Reclamation Locations Management - Alhaffer Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Reclamation Locations Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);

View File

@ -7,7 +7,7 @@ import ReportSheetViewModal from "~/components/ReportSheetViewModal";
import { useState } from "react";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Report Sheets - Alhaffer Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Report Sheets - Phosphat Report" }];
interface ReportSheet {
id: string;

View File

@ -10,7 +10,7 @@ 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 Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Reports Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 1); // All employees can access reports
@ -457,6 +457,112 @@ export const action = async ({ request }: ActionFunctionArgs) => {
}
}
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 });
@ -781,6 +887,22 @@ export default function Reports() {
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();
@ -1244,6 +1366,24 @@ export default function Reports() {
Duplicate
</span>
) : null}
{canCarryForwardReport(report) && (
<Form method="post" className="inline">
<input type="hidden" name="intent" value="carryTo" />
<input type="hidden" name="id" value={report.id} />
<button
type="submit"
onClick={(e) => {
if (!confirm(`Are you sure you want to carry forward this ${report.shift} shift to today? This will create a new report with today's date.`)) {
e.preventDefault();
}
}}
className="text-purple-600 hover:text-purple-900 transition-colors duration-150"
title="Carry forward to today"
>
CarryTo
</button>
</Form>
)}
{canEditReport(report) && (
<>
<button
@ -1354,6 +1494,23 @@ export default function Reports() {
Duplicate as {report.shift === 'day' ? 'Night' : 'Day'} Shift (Too Old)
</button>
) : null}
{canCarryForwardReport(report) && (
<Form method="post" className="w-full">
<input type="hidden" name="intent" value="carryTo" />
<input type="hidden" name="id" value={report.id} />
<button
type="submit"
onClick={(e) => {
if (!confirm(`Are you sure you want to carry forward this ${report.shift} shift to today? This will create a new report with today's date.`)) {
e.preventDefault();
}
}}
className="w-full text-center px-3 py-2 text-sm text-purple-600 bg-purple-50 rounded-md hover:bg-purple-100 transition-colors duration-150"
>
Carry Forward to Today
</button>
</Form>
)}
{canEditReport(report) && (
<div className="flex space-x-2">
<button

View File

@ -7,7 +7,7 @@ import { useState, useEffect } from "react";
import { manageSheet } from "~/utils/sheet.server";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "New Report - Alhaffer Report System" }];
export const meta: MetaFunction = () => [{ title: "New Report - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 1); // All employees can create reports
@ -106,6 +106,34 @@ export const action = async ({ request }: ActionFunctionArgs) => {
}
}
// Build automatic notes for pipeline extensions
const ext1Value = parseInt(pipelineExt1 as string) || 0;
const ext2Value = parseInt(pipelineExt2 as string) || 0;
const shiftText = shift === 'day' ? 'Day' : 'Night';
let automaticNotes = [];
// Add Extension 1 note if value > 0
if (ext1Value > 0) {
automaticNotes.push(`Main Extension ${ext1Value}m ${shiftText}`);
}
// Add Extension 2 note if value > 0
if (ext2Value > 0) {
automaticNotes.push(`Reserve Extension ${ext2Value}m ${shiftText}`);
}
// Combine automatic notes with user notes
let finalNotes = notes || '';
if (automaticNotes.length > 0) {
const automaticNotesText = automaticNotes.join(', ');
if (finalNotes.trim()) {
finalNotes = `${automaticNotesText}. ${finalNotes}`;
} else {
finalNotes = automaticNotesText;
}
}
const report = await prisma.report.create({
data: {
employeeId: user.id,
@ -121,9 +149,9 @@ export const action = async ({ request }: ActionFunctionArgs) => {
},
pipelineLength: {
main: parseInt(pipelineMain as string) || 0,
ext1: parseInt(pipelineExt1 as string) || 0,
ext1: ext1Value,
reserve: parseInt(pipelineReserve as string) || 0,
ext2: parseInt(pipelineExt2 as string) || 0
ext2: ext2Value
},
stats: {
Dozers: parseInt(statsDozers as string) || 0,
@ -134,7 +162,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
},
timeSheet,
stoppages,
notes: notes || null
notes: finalNotes || null
}
});
@ -206,6 +234,7 @@ export default function NewReport() {
const [currentStep, setCurrentStep] = useState(1);
const totalSteps = 4;
const [showZeroEquipmentConfirm, setShowZeroEquipmentConfirm] = useState(false);
const isSubmitting = navigation.state === "submitting";
@ -225,6 +254,18 @@ export default function NewReport() {
return false;
}
// Validate reclamation stoppages have notes
const invalidStoppages = stoppageEntries.filter(entry =>
entry.responsible === 'reclamation' && !entry.note.trim()
);
if (invalidStoppages.length > 0) {
alert('Please add notes for all reclamation stoppages before submitting.');
event.preventDefault();
event.stopPropagation();
return false;
}
// console.log("Allowing form submission");
// console.log("Form being submitted with data:", formData);
// console.log("Time sheet entries:", timeSheetEntries);
@ -334,6 +375,37 @@ export default function NewReport() {
}));
};
// Auto-calculate equipment counts based on time sheet entries
useEffect(() => {
const counts = { dozers: 0, excavators: 0, loaders: 0 };
timeSheetEntries.forEach(entry => {
if (entry.machine) {
const equipmentItem = equipment.find(item =>
`${item.model} (${item.number})` === entry.machine
);
if (equipmentItem) {
const category = equipmentItem.category.toLowerCase();
if (category.includes('dozer')) {
counts.dozers++;
} else if (category.includes('excavator')) {
counts.excavators++;
} else if (category.includes('loader')) {
counts.loaders++;
}
}
}
});
// Update form data with calculated counts
setFormData(prev => ({
...prev,
statsDozers: counts.dozers.toString(),
statsExc: counts.excavators.toString(),
statsLoaders: counts.loaders.toString()
}));
}, [timeSheetEntries, equipment]);
// Stoppage management
const addStoppageEntry = () => {
const newEntry = {
@ -341,8 +413,8 @@ export default function NewReport() {
from: '',
to: '',
total: '00:00',
reason: '',
responsible: '',
reason: '', // Will be set to 'none' for reclamation by default
responsible: 'reclamation', // Default to reclamation
note: ''
};
setStoppageEntries([...stoppageEntries, newEntry]);
@ -359,6 +431,13 @@ export default function NewReport() {
if (['from', 'to'].includes(field)) {
updatedEntry.total = calculateStoppageTime(updatedEntry.from, updatedEntry.to);
}
// Handle responsible party change
if (field === 'responsible') {
if (value === 'reclamation') {
updatedEntry.reason = ''; // Set to empty for reclamation (will show as "None")
}
// If changing to dredger, keep current reason or allow user to select
}
return updatedEntry;
}
return entry;
@ -370,6 +449,16 @@ export default function NewReport() {
event.preventDefault();
event.stopPropagation();
}
// Check if we're on step 3 (Equipment & Time Sheet) and have zero equipment
if (currentStep === 3) {
const totalEquipment = parseInt(formData.statsDozers) + parseInt(formData.statsExc) + parseInt(formData.statsLoaders);
if (totalEquipment === 0) {
setShowZeroEquipmentConfirm(true);
return;
}
}
// console.log("Next step clicked, current step:", currentStep);
if (currentStep < totalSteps) {
setCurrentStep(currentStep + 1);
@ -377,6 +466,17 @@ export default function NewReport() {
}
};
const confirmZeroEquipmentAndProceed = () => {
setShowZeroEquipmentConfirm(false);
if (currentStep < totalSteps) {
setCurrentStep(currentStep + 1);
}
};
const cancelZeroEquipmentConfirm = () => {
setShowZeroEquipmentConfirm(false);
};
const prevStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
@ -393,6 +493,36 @@ export default function NewReport() {
}
};
// Validation functions for each step
const isStep1Valid = () => {
return formData.shift &&
formData.areaId &&
formData.dredgerLocationId &&
formData.dredgerLineLength &&
!isNaN(parseInt(formData.dredgerLineLength));
};
const isStep2Valid = () => {
return formData.reclamationLocationId &&
formData.shoreConnection &&
!isNaN(parseInt(formData.shoreConnection));
};
const isStep3Valid = () => {
// Step 3 has no required fields - equipment and time sheet are optional
return true;
};
const isCurrentStepValid = () => {
switch (currentStep) {
case 1: return isStep1Valid();
case 2: return isStep2Valid();
case 3: return isStep3Valid();
case 4: return true; // Step 4 has no required fields
default: return false;
}
};
return (
<DashboardLayout user={user}>
<div className="max-w-full mx-auto">
@ -594,9 +724,9 @@ export default function NewReport() {
<div>
<h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Equipment Statistics</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
<div><label htmlFor="statsDozers" className="block text-sm font-medium text-gray-700 mb-2">Dozers</label><input type="number" id="statsDozers" name="statsDozers" min="0" value={formData.statsDozers} onChange={(e) => updateFormData('statsDozers', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label htmlFor="statsExc" className="block text-sm font-medium text-gray-700 mb-2">Excavators</label><input type="number" id="statsExc" name="statsExc" min="0" value={formData.statsExc} onChange={(e) => updateFormData('statsExc', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label htmlFor="statsLoaders" className="block text-sm font-medium text-gray-700 mb-2">Loaders</label><input type="number" id="statsLoaders" name="statsLoaders" min="0" value={formData.statsLoaders} onChange={(e) => updateFormData('statsLoaders', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label htmlFor="statsDozers" className="block text-sm font-medium text-gray-700 mb-2">Dozers <span className="text-xs text-gray-500">(Auto-calculated)</span></label><input type="number" id="statsDozers" name="statsDozers" min="0" value={formData.statsDozers} readOnly className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 text-gray-700 cursor-not-allowed" title="Auto-calculated based on time sheet entries" /></div>
<div><label htmlFor="statsExc" className="block text-sm font-medium text-gray-700 mb-2">Excavators <span className="text-xs text-gray-500">(Auto-calculated)</span></label><input type="number" id="statsExc" name="statsExc" min="0" value={formData.statsExc} readOnly className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 text-gray-700 cursor-not-allowed" title="Auto-calculated based on time sheet entries" /></div>
<div><label htmlFor="statsLoaders" className="block text-sm font-medium text-gray-700 mb-2">Loaders <span className="text-xs text-gray-500">(Auto-calculated)</span></label><input type="number" id="statsLoaders" name="statsLoaders" min="0" value={formData.statsLoaders} readOnly className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 text-gray-700 cursor-not-allowed" title="Auto-calculated based on time sheet entries" /></div>
<div><label htmlFor="statsForeman" className="block text-sm font-medium text-gray-700 mb-2">Foreman</label><select id="statsForeman" name="statsForeman" value={formData.statsForeman} onChange={(e) => updateFormData('statsForeman', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"><option value="">Select foreman</option>{foremen.map((foreman) => (<option key={foreman.id} value={foreman.name}>{foreman.name}</option>))}</select></div>
<div><label htmlFor="statsLaborer" className="block text-sm font-medium text-gray-700 mb-2">Laborers</label><input type="number" id="statsLaborer" name="statsLaborer" min="0" value={formData.statsLaborer} onChange={(e) => updateFormData('statsLaborer', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
</div>
@ -653,11 +783,11 @@ export default function NewReport() {
<div><label className="block text-sm font-medium text-gray-700 mb-1">From</label><input type="time" value={entry.from} onChange={(e) => updateStoppageEntry(entry.id, 'from', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">To</label><input type="time" value={entry.to} onChange={(e) => updateStoppageEntry(entry.id, 'to', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">Total</label><input type="text" value={entry.total} readOnly className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">Reason</label><input type="text" value={entry.reason} onChange={(e) => updateStoppageEntry(entry.id, 'reason', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" placeholder="Stoppage reason" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">Responsible</label><input type="text" value={entry.responsible} onChange={(e) => updateStoppageEntry(entry.id, 'responsible', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" placeholder="Responsible party" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">Reason</label><select value={entry.reason} onChange={(e) => updateStoppageEntry(entry.id, 'reason', e.target.value)} disabled={entry.responsible === 'reclamation'} className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 ${entry.responsible === 'reclamation' ? 'bg-gray-100 text-gray-500 cursor-not-allowed border-gray-300' : 'border-gray-300'}`}><option value="">None</option><option value="maintenance">Maintenance</option><option value="shift">Shift</option><option value="lubrication">Lubrication</option><option value="check">Check</option></select></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">Responsible</label><select value={entry.responsible} onChange={(e) => updateStoppageEntry(entry.id, 'responsible', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"><option value="reclamation">Reclamation</option><option value="dredger">Dredger</option></select></div>
<div className="flex items-end"><button type="button" onClick={() => removeStoppageEntry(entry.id)} className="w-full px-3 py-2 border border-red-300 text-red-700 rounded-md hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">Remove</button></div>
</div>
<div className="mt-4"><label className="block text-sm font-medium text-gray-700 mb-1">Notes</label><input type="text" value={entry.note} onChange={(e) => updateStoppageEntry(entry.id, 'note', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" placeholder="Additional notes" /></div>
<div className="mt-4"><label className="block text-sm font-medium text-gray-700 mb-1">Notes {entry.responsible === 'reclamation' && <span className="text-red-500">*</span>}</label><input type="text" value={entry.note} onChange={(e) => updateStoppageEntry(entry.id, 'note', e.target.value)} className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 ${entry.responsible === 'reclamation' && !entry.note.trim() ? 'border-red-300 bg-red-50' : 'border-gray-300'}`} placeholder={entry.responsible === 'reclamation' ? 'Notes required for reclamation stoppages' : 'Additional notes'} /></div>
</div>
))}
</div>
@ -694,7 +824,7 @@ export default function NewReport() {
<div className="flex space-x-3">
{currentStep < totalSteps ? (
<button type="button" onClick={(e) => nextStep(e)} className="flex-1 sm:flex-none inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<button type="button" onClick={(e) => nextStep(e)} disabled={!isCurrentStepValid()} className={`flex-1 sm:flex-none inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 ${!isCurrentStepValid() ? 'text-gray-400 bg-gray-300 cursor-not-allowed' : 'text-white bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500'}`}>
Next<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" /></svg>
</button>
) : (
@ -754,6 +884,44 @@ export default function NewReport() {
<input type="hidden" name="notes" value={formData.notes} />
)}
</Form>
{/* Zero Equipment Confirmation Modal */}
{showZeroEquipmentConfirm && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="mt-3 text-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-yellow-100">
<svg className="h-6 w-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg leading-6 font-medium text-gray-900 mt-4">No Equipment Added</h3>
<div className="mt-2 px-7 py-3">
<p className="text-sm text-gray-500">
You haven't added any equipment to the time sheet. This means all equipment counts (Dozers, Excavators, Loaders) will be 0.
</p>
<p className="text-sm text-gray-500 mt-2">
Are you sure you want to continue without any equipment entries?
</p>
</div>
<div className="flex gap-3 mt-4">
<button
onClick={cancelZeroEquipmentConfirm}
className="flex-1 px-4 py-2 bg-gray-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-300"
>
Go Back
</button>
<button
onClick={confirmZeroEquipmentAndProceed}
className="flex-1 px-4 py-2 bg-yellow-600 text-white text-base font-medium rounded-md shadow-sm hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-yellow-300"
>
Continue Anyway
</button>
</div>
</div>
</div>
</div>
)}
</div>
</DashboardLayout>
);

View File

@ -3,7 +3,7 @@ import { json, redirect } from "@remix-run/node";
import { Form, Link, useActionData, useSearchParams } from "@remix-run/react";
import { createUserSession, getUserId, verifyLogin } from "~/utils/auth.server";
export const meta: MetaFunction = () => [{ title: "Sign In - Alhaffer Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Sign In - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const userId = await getUserId(request);
@ -51,7 +51,7 @@ export default function SignIn() {
<img
className="mx-auto h-20 sm:h-28 w-auto"
src="/clogo-sm.png"
alt="Alhaffer Reports System"
alt="Phosphat Report"
/>
<h2 className="mt-4 sm:mt-6 text-2xl sm:text-3xl font-extrabold text-gray-900">
Sign in to your account

View File

@ -4,7 +4,7 @@ import { Form, Link, useActionData } from "@remix-run/react";
import { createUser, createUserSession, getUserId } from "~/utils/auth.server";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Sign Up - Alhaffer Report System" }];
export const meta: MetaFunction = () => [{ title: "Sign Up - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const userId = await getUserId(request);

View File

@ -6,7 +6,7 @@ import DashboardLayout from "~/components/DashboardLayout";
import { useState } from "react";
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Stoppages Analysis - Alhaffer Reporting System" }];
export const meta: MetaFunction = () => [{ title: "Stoppages Analysis - Phosphat Report" }];
interface StoppageEntry {
id: string;

View File

@ -0,0 +1,76 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
- NODE_ENV=production
image: phosphat-report:latest
container_name: phosphat-report-app
restart: unless-stopped
ports:
- "${APP_PORT:-5173}:3000"
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=file:/app/data/production.db
- SESSION_SECRET=${SESSION_SECRET}
- SUPER_ADMIN=${SUPER_ADMIN}
- SUPER_ADMIN_EMAIL=${SUPER_ADMIN_EMAIL}
- SUPER_ADMIN_PASSWORD=${SUPER_ADMIN_PASSWORD}
- MAIL_HOST=${MAIL_HOST:-}
- MAIL_PORT=${MAIL_PORT:-587}
- MAIL_SECURE=${MAIL_SECURE:-false}
- MAIL_USERNAME=${MAIL_USERNAME:-}
- MAIL_PASSWORD=${MAIL_PASSWORD:-}
- MAIL_FROM_NAME=${MAIL_FROM_NAME:-Phosphat Report System}
- MAIL_FROM_EMAIL=${MAIL_FROM_EMAIL:-}
volumes:
- app_data:/app/data
- app_logs:/app/logs
networks:
- app_network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health", "||", "exit", "1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
backup:
image: alpine:latest
container_name: phosphat-report-backup
restart: unless-stopped
volumes:
- app_data:/data:ro
- backup_data:/backup
command: >
sh -c "
apk add --no-cache dcron sqlite &&
echo '0 2 * * * cp /data/production.db /backup/production_$(date +%Y%m%d_%H%M%S).db && find /backup -name \"production_*.db\" -mtime +7 -delete' | crontab - &&
crond -f
"
networks:
- app_network
depends_on:
- app
volumes:
app_data:
driver: local
app_logs:
driver: local
backup_data:
driver: local
networks:
app_network:
driver: bridge

76
docker-compose.yml Normal file
View File

@ -0,0 +1,76 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
- NODE_ENV=production
image: phosphat-report:latest
container_name: phosphat-report-app
restart: unless-stopped
ports:
- "${APP_PORT:-5173}:3000"
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=file:/app/data/production.db
- SESSION_SECRET=${SESSION_SECRET}
- SUPER_ADMIN=${SUPER_ADMIN}
- SUPER_ADMIN_EMAIL=${SUPER_ADMIN_EMAIL}
- SUPER_ADMIN_PASSWORD=${SUPER_ADMIN_PASSWORD}
- MAIL_HOST=${MAIL_HOST:-}
- MAIL_PORT=${MAIL_PORT:-587}
- MAIL_SECURE=${MAIL_SECURE:-false}
- MAIL_USERNAME=${MAIL_USERNAME:-}
- MAIL_PASSWORD=${MAIL_PASSWORD:-}
- MAIL_FROM_NAME=${MAIL_FROM_NAME:-Phosphat Report System}
- MAIL_FROM_EMAIL=${MAIL_FROM_EMAIL:-}
volumes:
- app_data:/app/data
- app_logs:/app/logs
networks:
- app_network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health", "||", "exit", "1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
backup:
image: alpine:latest
container_name: phosphat-report-backup
restart: unless-stopped
volumes:
- app_data:/data:ro
- backup_data:/backup
command: >
sh -c "
apk add --no-cache dcron sqlite &&
echo '0 2 * * * cp /data/production.db /backup/production_$(date +%Y%m%d_%H%M%S).db && find /backup -name \"production_*.db\" -mtime +7 -delete' | crontab - &&
crond -f
"
networks:
- app_network
depends_on:
- app
volumes:
app_data:
driver: local
app_logs:
driver: local
backup_data:
driver: local
networks:
app_network:
driver: bridge

Binary file not shown.

View File

@ -6,6 +6,62 @@ const prisma = new PrismaClient();
async function main() {
console.log('🌱 Seeding database...');
// Seed Areas
const areas = await Promise.all([
prisma.area.upsert({
where: { name: 'Petra' },
update: {},
create: { name: 'Petra' }
}),
prisma.area.upsert({
where: { name: 'Jarash' },
update: {},
create: { name: 'Jarash' }
}),
prisma.area.upsert({
where: { name: 'Rum' },
update: {},
create: { name: 'Rum' }
})
]);
// Seed DredgerLocations
const dredgerLocations = await Promise.all([
prisma.dredgerLocation.upsert({
where: { name: 'SP1-1' },
update: {},
create: { name: 'SP1-1', class: 'SP' }
}),
prisma.dredgerLocation.upsert({
where: { name: 'SP1-2' },
update: {},
create: { name: 'SP1-2', class: 'SP' }
}),
prisma.dredgerLocation.upsert({
where: { name: 'C01' },
update: {},
create: { name: 'C01', class: 'C' }
}),
prisma.dredgerLocation.upsert({
where: { name: 'D1' },
update: {},
create: { name: 'D1', class: 'D' }
})
]);
// Seed ReclamationLocations
const reclamationLocations = await Promise.all([
prisma.reclamationLocation.upsert({
where: { name: 'Eastern Shoreline' },
update: {},
create: { name: 'Eastern Shoreline' }
}),
prisma.reclamationLocation.upsert({
where: { name: 'Western Shoreline' },
update: {},
create: { name: 'Western Shoreline' }
})
]);
// Seed Super Admin Employee
const superAdminUsername = process.env.SUPER_ADMIN || 'superadmin';
@ -24,12 +80,61 @@ async function main() {
}
});
// Seed Foreman
await prisma.foreman.upsert({
where: { id: 1 },
update: {},
create: {
name: 'John Smith'
}
});
// Seed Equipment
const equipment = await Promise.all([
prisma.equipment.upsert({
where: { id: 1 },
update: {},
create: { id: 1, category: 'Dozer', model: 'Dozer6', number: 1 }
}),
prisma.equipment.upsert({
where: { id: 2 },
update: {},
create: { id: 2, category: 'Dozer', model: 'Dozer6', number: 2 }
}),
prisma.equipment.upsert({
where: { id: 3 },
update: {},
create: { id: 3, category: 'Dozer', model: 'Dozer7', number: 1 }
}),
prisma.equipment.upsert({
where: { id: 4 },
update: {},
create: { id: 4, category: 'Dozer', model: 'Dozer8', number: 1 }
}),
prisma.equipment.upsert({
where: { id: 5 },
update: {},
create: { id: 5, category: 'Loader', model: 'Loader', number: 1 }
}),
prisma.equipment.upsert({
where: { id: 6 },
update: {},
create: { id: 6, category: 'Excavator', model: 'Exc.', number: 1 }
}),
prisma.equipment.upsert({
where: { id: 7 },
update: {},
create: { id: 7, category: 'Excavator', model: 'Exc.', number: 9 }
})
]);
console.log('✅ Database seeded successfully!');
console.log(`Created ${areas.length} areas`);
console.log(`Created ${dredgerLocations.length} dredger locations`);
console.log(`Created ${reclamationLocations.length} reclamation locations`);
console.log(`Created 1 employee`);
console.log(`Created 1 foreman`);
console.log(`Created ${equipment.length} equipment records`);
}
main()

View File

@ -6,7 +6,75 @@ const prisma = new PrismaClient()
async function main() {
console.log('🌱 Seeding database...')
// Seed Areas
const areas = await Promise.all([
prisma.area.upsert({
where: { name: 'Petra' },
update: {},
create: { name: 'Petra' }
}),
prisma.area.upsert({
where: { name: 'Jarash' },
update: {},
create: { name: 'Jarash' }
}),
prisma.area.upsert({
where: { name: 'Rum' },
update: {},
create: { name: 'Rum' }
})
])
// Seed DredgerLocations
const dredgerLocations = await Promise.all([
prisma.dredgerLocation.upsert({
where: { name: 'SP1-1' },
update: {},
create: { name: 'SP1-1', class: 'SP' }
}),
prisma.dredgerLocation.upsert({
where: { name: 'SP1-2' },
update: {},
create: { name: 'SP1-2', class: 'SP' }
}),
prisma.dredgerLocation.upsert({
where: { name: 'C01' },
update: {},
create: { name: 'C01', class: 'C' }
}),
prisma.dredgerLocation.upsert({
where: { name: 'D1' },
update: {},
create: { name: 'D1', class: 'D' }
})
])
// Seed ReclamationLocations
const reclamationLocations = await Promise.all([
prisma.reclamationLocation.upsert({
where: { name: 'Eastern Shoreline' },
update: {},
create: { name: 'Eastern Shoreline' }
}),
prisma.reclamationLocation.upsert({
where: { name: 'Western Shoreline' },
update: {},
create: { name: 'Western Shoreline' }
})
])
// Seed Employee
// const employee = await prisma.employee.upsert({
// where: { username: 'superuser' },
// update: {},
// create: {
// name: 'Super Admin User',
// authLevel: 3,
// username: '',
// email: '@gmail.com',
// password: bcrypt.hashSync('', 10)
// }
// })
// Seed Employee
//use the .env file SUPER_ADMIN, SUPER_ADMIN_EMAIL and SUPER_ADMIN_PASSWORD
@ -24,9 +92,61 @@ async function main() {
})
// Seed Foreman
const foreman = await prisma.foreman.upsert({
where: { id: 1 },
update: {},
create: {
name: 'John Smith'
}
})
// Seed Equipment
const equipment = await Promise.all([
prisma.equipment.upsert({
where: { id: 1 },
update: {},
create: { id: 1, category: 'Dozer', model: 'Dozer6', number: 1 }
}),
prisma.equipment.upsert({
where: { id: 2 },
update: {},
create: { id: 2, category: 'Dozer', model: 'Dozer6', number: 2 }
}),
prisma.equipment.upsert({
where: { id: 3 },
update: {},
create: { id: 3, category: 'Dozer', model: 'Dozer7', number: 1 }
}),
prisma.equipment.upsert({
where: { id: 4 },
update: {},
create: { id: 4, category: 'Dozer', model: 'Dozer8', number: 1 }
}),
prisma.equipment.upsert({
where: { id: 5 },
update: {},
create: { id: 5, category: 'Loader', model: 'Loader', number: 1 }
}),
prisma.equipment.upsert({
where: { id: 6 },
update: {},
create: { id: 6, category: 'Excavator', model: 'Exc.', number: 1 }
}),
prisma.equipment.upsert({
where: { id: 7 },
update: {},
create: { id: 7, category: 'Excavator', model: 'Exc.', number: 9 }
})
])
console.log('✅ Database seeded successfully!')
console.log(`Created ${areas.length} areas`)
console.log(`Created ${dredgerLocations.length} dredger locations`)
console.log(`Created ${reclamationLocations.length} reclamation locations`)
console.log(`Created 1 employee`)
console.log(`Created 1 foreman`)
console.log(`Created ${equipment.length} equipment records`)
}
main()