From 75801628160ac85ab30137cc12ee255f414453f4 Mon Sep 17 00:00:00 2001 From: yznahmad Date: Sun, 7 Sep 2025 04:05:44 +0300 Subject: [PATCH] ggg --- CARRY_FORWARD_FEATURE.md | 227 +++++++++++++++++++++++++++ app/components/DashboardLayout.tsx | 29 ++-- app/routes/areas.tsx | 2 +- app/routes/dashboard.tsx | 2 +- app/routes/dredger-locations.tsx | 2 +- app/routes/employees.tsx | 2 +- app/routes/equipment.tsx | 2 +- app/routes/foreman.tsx | 2 +- app/routes/reclamation-locations.tsx | 2 +- app/routes/report-sheet.tsx | 2 +- app/routes/reports.tsx | 159 ++++++++++++++++++- app/routes/reports_.new.tsx | 194 +++++++++++++++++++++-- app/routes/signin.tsx | 4 +- app/routes/signup.tsx | 2 +- app/routes/stoppages.tsx | 2 +- docker-compose.dokploy.yml | 76 +++++++++ docker-compose.yml | 76 +++++++++ prisma/dev.db | Bin 102400 -> 102400 bytes prisma/seed.js | 109 ++++++++++++- prisma/seed.ts | 124 ++++++++++++++- 20 files changed, 973 insertions(+), 45 deletions(-) create mode 100644 CARRY_FORWARD_FEATURE.md create mode 100644 docker-compose.dokploy.yml create mode 100644 docker-compose.yml diff --git a/CARRY_FORWARD_FEATURE.md b/CARRY_FORWARD_FEATURE.md new file mode 100644 index 0000000..d2f4e18 --- /dev/null +++ b/CARRY_FORWARD_FEATURE.md @@ -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." \ No newline at end of file diff --git a/app/components/DashboardLayout.tsx b/app/components/DashboardLayout.tsx index 330bc21..632da92 100644 --- a/app/components/DashboardLayout.tsx +++ b/app/components/DashboardLayout.tsx @@ -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,24 +9,23 @@ 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); - + const toggleCollapse = () => { 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 Report System @@ -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} > diff --git a/app/routes/areas.tsx b/app/routes/areas.tsx index a4dace2..d0798a4 100644 --- a/app/routes/areas.tsx +++ b/app/routes/areas.tsx @@ -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); diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx index 6e80077..8434c9c 100644 --- a/app/routes/dashboard.tsx +++ b/app/routes/dashboard.tsx @@ -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); diff --git a/app/routes/dredger-locations.tsx b/app/routes/dredger-locations.tsx index 6c173a3..cf2413b 100644 --- a/app/routes/dredger-locations.tsx +++ b/app/routes/dredger-locations.tsx @@ -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); diff --git a/app/routes/employees.tsx b/app/routes/employees.tsx index 8b2172c..13e18f8 100644 --- a/app/routes/employees.tsx +++ b/app/routes/employees.tsx @@ -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); diff --git a/app/routes/equipment.tsx b/app/routes/equipment.tsx index 4a0c0b6..c9c5994 100644 --- a/app/routes/equipment.tsx +++ b/app/routes/equipment.tsx @@ -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); diff --git a/app/routes/foreman.tsx b/app/routes/foreman.tsx index 00949cd..b3ea82f 100644 --- a/app/routes/foreman.tsx +++ b/app/routes/foreman.tsx @@ -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); diff --git a/app/routes/reclamation-locations.tsx b/app/routes/reclamation-locations.tsx index 6f7348d..af53497 100644 --- a/app/routes/reclamation-locations.tsx +++ b/app/routes/reclamation-locations.tsx @@ -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); diff --git a/app/routes/report-sheet.tsx b/app/routes/report-sheet.tsx index ad072c0..0eb1a6e 100644 --- a/app/routes/report-sheet.tsx +++ b/app/routes/report-sheet.tsx @@ -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; diff --git a/app/routes/reports.tsx b/app/routes/reports.tsx index 9e927d5..b916fa1 100644 --- a/app/routes/reports.tsx +++ b/app/routes/reports.tsx @@ -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 ) : null} + {canCarryForwardReport(report) && ( +
+ + + +
+ )} {canEditReport(report) && ( <> + + )} {canEditReport(report) && (
-
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" />
+
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'} />
))} @@ -694,7 +824,7 @@ export default function NewReport() {
{currentStep < totalSteps ? ( - ) : ( @@ -754,6 +884,44 @@ export default function NewReport() { )} + + {/* Zero Equipment Confirmation Modal */} + {showZeroEquipmentConfirm && ( +
+
+
+
+ + + +
+

No Equipment Added

+
+

+ You haven't added any equipment to the time sheet. This means all equipment counts (Dozers, Excavators, Loaders) will be 0. +

+

+ Are you sure you want to continue without any equipment entries? +

+
+
+ + +
+
+
+
+ )}
); diff --git a/app/routes/signin.tsx b/app/routes/signin.tsx index e4d3cf7..2d40024 100644 --- a/app/routes/signin.tsx +++ b/app/routes/signin.tsx @@ -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() { Alhaffer Reports System

Sign in to your account diff --git a/app/routes/signup.tsx b/app/routes/signup.tsx index 317754c..ab2d5cd 100644 --- a/app/routes/signup.tsx +++ b/app/routes/signup.tsx @@ -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); diff --git a/app/routes/stoppages.tsx b/app/routes/stoppages.tsx index d56b69b..59c44b7 100644 --- a/app/routes/stoppages.tsx +++ b/app/routes/stoppages.tsx @@ -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; diff --git a/docker-compose.dokploy.yml b/docker-compose.dokploy.yml new file mode 100644 index 0000000..b7e4dc3 --- /dev/null +++ b/docker-compose.dokploy.yml @@ -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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b7e4dc3 --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/prisma/dev.db b/prisma/dev.db index 91c8b2b90bfcf6c54ebe9c68674435320991041c..9972fd503c15b349d41218a7e7502aa4c3dd561b 100644 GIT binary patch delta 7489 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 muw5y{Rq@V`Z%n+N6SL7AOpt#%mD?EeqMK0p`% diff --git a/prisma/seed.js b/prisma/seed.js index c97973a..0252645 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -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() diff --git a/prisma/seed.ts b/prisma/seed.ts index 6d55f60..a482859 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -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()