Compare commits

...

7 Commits

Author SHA1 Message Date
0ac7cabe92 ccg 2025-09-08 13:36:42 +03:00
1d1bb79928 v 2025-09-07 04:11:48 +03:00
7580162816 ggg 2025-09-07 04:05:44 +03:00
86f3fa7f1d Commit message title 2025-08-26 10:12:51 +03:00
2f7791989c feat: Add new user authentication for v2.1 2025-08-19 01:24:07 +03:00
641544f717 feat: Add new user authentication for v2.00 2025-08-19 01:04:09 +03:00
2caf98ad0f feat: Add new user authentication for v2.0 2025-08-19 01:03:47 +03:00
23 changed files with 844 additions and 143 deletions

View File

@ -49,6 +49,9 @@ coverage
ehthumbs.db
Thumbs.db
# Explicitly include start.sh
!start.sh
# Git
.git
.gitignore

View File

@ -1,30 +0,0 @@
# Dokploy Environment Variables
# Use these values in your Dokploy environment variables section
NODE_ENV=production
APP_PORT=5173
# Database (uses Docker volume)
DATABASE_URL=file:/app/data/production.db
# Security - CHANGE THESE VALUES!
SESSION_SECRET=your-super-secure-session-secret-change-this-in-production-min-32-chars
ENCRYPTION_KEY=production-secure-encryption-key!
SUPER_ADMIN=superadmin
SUPER_ADMIN_EMAIL=admin@yourcompany.com
SUPER_ADMIN_PASSWORD=YourSecurePassword123!
# Domain (set to your actual domain)
DOMAIN=your-domain.com
# Mail Settings (optional - for password reset features)
MAIL_HOST=
MAIL_PORT=587
MAIL_SECURE=false
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM_NAME=Phosphat Report System
MAIL_FROM_EMAIL=
# Logging
LOG_LEVEL=info

View File

@ -1,39 +0,0 @@
# Production Environment Variables
# Copy this file and rename to .env for production deployment
# Make sure to change all default values for security
# Application Settings
NODE_ENV=production
APP_PORT=5173
DOMAIN=your-domain.com
# Database
DATABASE_URL="file:/app/data/production.db"
# Security
SESSION_SECRET="your-super-secure-session-secret-change-this-in-production-min-32-chars"
ENCRYPTION_KEY="production-secure-encryption-key!"
# Super Admin Account (created on first run)
SUPER_ADMIN="superadmin"
SUPER_ADMIN_EMAIL="admin@yourcompany.com"
SUPER_ADMIN_PASSWORD="YourSecurePassword123!"
# Storage Paths (for bind mounts)
DATA_PATH=./data
BACKUP_PATH=./backups
# Backup Schedule (cron format)
BACKUP_SCHEDULE="0 2 * * *"
# Mail Settings (optional - for password reset features)
MAIL_HOST=""
MAIL_PORT="587"
MAIL_SECURE="false"
MAIL_USERNAME=""
MAIL_PASSWORD=""
MAIL_FROM_NAME="Phosphat Report System"
MAIL_FROM_EMAIL=""
# Logging (optional)
LOG_LEVEL="info"

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."

147
DEPLOYMENT_GUIDE.md Normal file
View File

@ -0,0 +1,147 @@
# Deployment Guide for Phosphat Report App
This guide will help you deploy the Phosphat Report application on your VPS using the provided `compose.yml` file.
## Prerequisites
- Docker and Docker Compose installed on your VPS
- Git (to clone the repository)
- At least 1GB RAM and 10GB disk space
## Quick Deployment
1. **Clone the repository** to your VPS:
```bash
git clone <your-repo-url>
cd phosphat-report-app
```
2. **Deploy the application**:
```bash
docker-compose -f compose.yml up -d --build
```
3. **Check the status**:
```bash
docker-compose -f compose.yml ps
```
4. **Access your application**:
- URL: `http://your-vps-ip:3000`
- Default login: `superadmin` / `P@ssw0rd123!`
## Environment Variables (Hardcoded in compose.yml)
The following environment variables are already configured in the `compose.yml` file:
- **NODE_ENV**: `production`
- **DATABASE_URL**: `file:/app/data/production.db`
- **SESSION_SECRET**: `your-super-secure-session-secret-change-this-min-32-chars`
- **SUPER_ADMIN**: `superadmin`
- **SUPER_ADMIN_EMAIL**: `admin@yourcompany.com`
- **SUPER_ADMIN_PASSWORD**: `P@ssw0rd123!`
- **MAIL_HOST**: `smtp.gmail.com`
- **MAIL_PORT**: `587`
- **MAIL_USERNAME**: `your-email@gmail.com`
- **MAIL_PASSWORD**: `your-app-password`
## Services Included
### Main Application (`app`)
- **Port**: 3000
- **Database**: SQLite with persistent storage
- **Health Check**: Available at `/health` endpoint
- **Resource Limits**: 512MB RAM, 0.5 CPU
### Backup Service (`backup`)
- **Purpose**: Automatic daily database backups at 2 AM
- **Retention**: Keeps backups for 7 days
- **Location**: `/backup` volume
## Useful Commands
### View logs:
```bash
docker-compose -f compose.yml logs -f app
```
### Stop services:
```bash
docker-compose -f compose.yml down
```
### Restart services:
```bash
docker-compose -f compose.yml restart
```
### Manual backup:
```bash
docker-compose -f compose.yml exec app cp /app/data/production.db /app/data/backup_$(date +%Y%m%d_%H%M%S).db
```
### Check health:
```bash
curl http://localhost:3000/health
```
## Volumes
- **app_data**: Stores the SQLite database
- **app_logs**: Application logs
- **backup_data**: Database backups
## Security Notes
1. **Change default passwords** after first login
2. **Update email settings** in the application
3. **Configure firewall** to only allow necessary ports
4. **Use HTTPS** with a reverse proxy (nginx/traefik) for production
## Troubleshooting
### Application won't start:
```bash
# Check logs
docker-compose -f compose.yml logs app
# Rebuild without cache
docker-compose -f compose.yml build --no-cache app
```
### Database issues:
```bash
# Reset database (WARNING: This will delete all data)
docker-compose -f compose.yml down
docker volume rm $(docker volume ls -q | grep app_data)
docker-compose -f compose.yml up -d
```
### Port conflicts:
If port 3000 is already in use, edit the `compose.yml` file and change:
```yaml
ports:
- "3001:3000" # Change 3000 to any available port
```
## Updating the Application
1. **Pull latest changes**:
```bash
git pull origin main
```
2. **Rebuild and restart**:
```bash
docker-compose -f compose.yml up -d --build
```
## Support
For issues and support:
1. Check the application logs
2. Verify all services are running
3. Test the health endpoint
4. Check database connectivity
The application should be accessible at `http://your-vps-ip:3000` after successful deployment.

View File

@ -54,39 +54,6 @@ COPY --from=deps --chown=remix:nodejs /app/node_modules ./node_modules
RUN mkdir -p /app/data /app/logs && \
chown -R remix:nodejs /app/data /app/logs
# Create startup script
COPY --chown=remix:nodejs <<EOF /app/start.sh
#!/bin/sh
set -e
echo "Starting Phosphat Report Application..."
# Run database migrations
echo "Running database migrations..."
npx prisma db push --accept-data-loss
# Run seed using production script
echo "Seeding database..."
if [ -f "scripts/seed-production.js" ]; then
echo "Using production seed script..."
node scripts/seed-production.js
else
echo "Production seed script not found, trying alternative methods..."
if [ -f "prisma/seed.js" ]; then
echo "Using JavaScript seed file..."
node prisma/seed.js
else
echo "No seeding method available, skipping..."
fi
fi
echo "Database setup complete. Starting application on port 3000..."
export PORT=3000
exec npx remix-serve ./build/server/index.js
EOF
RUN chmod +x /app/start.sh
USER remix
EXPOSE 3000
@ -100,4 +67,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
CMD ["/app/start.sh"]
CMD ["sh", "-c", "echo 'Starting Phosphat Report Application...' && npx prisma db push --accept-data-loss && echo 'Seeding database...' && (test -f scripts/seed-production.js && node scripts/seed-production.js || test -f prisma/seed.js && node prisma/seed.js || echo 'No seeding method available, skipping...') && echo 'Database setup complete. Starting application on port 3000...' && exec npx remix-serve ./build/server/index.js"]

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 (
@ -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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Areas Management - Alhaffer Report System" }];
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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Dashboard - Alhaffer Report System" }];
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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Dredger Locations Management - Alhaffer Report System" }];
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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Employee Management - Alhaffer Report System" }];
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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Equipment Management - Alhaffer Report System" }];
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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Foreman Management - Alhaffer Report System" }];
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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Reclamation Locations Management - Alhaffer Report System" }];
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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Report Sheets - Alhaffer Report System" }];
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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Reports Management - Alhaffer Report System" }];
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();
@ -875,7 +997,7 @@ export default function Reports() {
{/* Total Reports */}
<div className="bg-gray-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-gray-900">{stats.totalReports}</div>
<div className="text-xs text-gray-600 mt-1">Total Reports</div>
<div className="text-xs text-gray-600 mt-1">Total Shifts</div>
</div>
{/* Day Shift Count */}
@ -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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "New Report - Alhaffer Report System" }];
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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Sign In - Alhaffer Report System" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const userId = await getUserId(request);

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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Sign Up - Alhaffer Report System" }];
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 - Phosphat Report" }];
export const meta: MetaFunction = () => [{ title: "Stoppages Analysis - Alhaffer Report System" }];
interface StoppageEntry {
id: string;

75
compose.yml Normal file
View File

@ -0,0 +1,75 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: phosphat-report-app
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=file:/app/data/production.db
- SESSION_SECRET=your-super-secure-session-secret-change-this-min-32-chars
- SUPER_ADMIN=superadmin
- SUPER_ADMIN_EMAIL=admin@yourcompany.com
- SUPER_ADMIN_PASSWORD=P@ssw0rd123!
- MAIL_HOST=smtp.gmail.com
- MAIL_PORT=587
- MAIL_SECURE=false
- MAIL_USERNAME=your-email@gmail.com
- MAIL_PASSWORD=your-app-password
- MAIL_FROM_NAME=Phosphat Report System
- MAIL_FROM_EMAIL=noreply@yourcompany.com
- ENCRYPTION_KEY=phosphat-report-default-key-32b
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.

27
start.sh Normal file
View File

@ -0,0 +1,27 @@
#!/bin/sh
set -e
echo "Starting Phosphat Report Application..."
# Run database migrations
echo "Running database migrations..."
npx prisma db push --accept-data-loss
# Run seed using production script
echo "Seeding database..."
if [ -f "scripts/seed-production.js" ]; then
echo "Using production seed script..."
node scripts/seed-production.js
else
echo "Production seed script not found, trying alternative methods..."
if [ -f "prisma/seed.js" ]; then
echo "Using JavaScript seed file..."
node prisma/seed.js
else
echo "No seeding method available, skipping..."
fi
fi
echo "Database setup complete. Starting application on port 3000..."
export PORT=3000
exec npx remix-serve ./build/server/index.js