Compare commits
7 Commits
main
...
v2.0-featu
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ac7cabe92 | |||
| 1d1bb79928 | |||
| 7580162816 | |||
| 86f3fa7f1d | |||
| 2f7791989c | |||
| 641544f717 | |||
| 2caf98ad0f |
@ -49,6 +49,9 @@ coverage
|
|||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Explicitly include start.sh
|
||||||
|
!start.sh
|
||||||
|
|
||||||
# Git
|
# Git
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|||||||
30
.env.dokploy
30
.env.dokploy
@ -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
|
|
||||||
@ -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
227
CARRY_FORWARD_FEATURE.md
Normal 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
147
DEPLOYMENT_GUIDE.md
Normal 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.
|
||||||
35
Dockerfile
35
Dockerfile
@ -54,39 +54,6 @@ COPY --from=deps --chown=remix:nodejs /app/node_modules ./node_modules
|
|||||||
RUN mkdir -p /app/data /app/logs && \
|
RUN mkdir -p /app/data /app/logs && \
|
||||||
chown -R remix:nodejs /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
|
USER remix
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
@ -100,4 +67,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
|||||||
|
|
||||||
# Use dumb-init to handle signals properly
|
# Use dumb-init to handle signals properly
|
||||||
ENTRYPOINT ["dumb-init", "--"]
|
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"]
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Form, Link, useLocation } from "@remix-run/react";
|
import { Form, Link, useLocation } from "@remix-run/react";
|
||||||
import type { Employee } from "@prisma/client";
|
import type { Employee } from "@prisma/client";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
interface DashboardLayoutProps {
|
interface DashboardLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -9,14 +9,15 @@ interface DashboardLayoutProps {
|
|||||||
|
|
||||||
export default function DashboardLayout({ children, user }: DashboardLayoutProps) {
|
export default function DashboardLayout({ children, user }: DashboardLayoutProps) {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
// Initialize from localStorage if available
|
|
||||||
if (typeof window !== 'undefined') {
|
// Initialize sidebar state from localStorage after hydration
|
||||||
const saved = localStorage.getItem('sidebar-collapsed');
|
useEffect(() => {
|
||||||
return saved ? JSON.parse(saved) : false;
|
const saved = localStorage.getItem('sidebar-collapsed');
|
||||||
|
if (saved) {
|
||||||
|
setSidebarCollapsed(JSON.parse(saved));
|
||||||
}
|
}
|
||||||
return false;
|
}, []);
|
||||||
});
|
|
||||||
|
|
||||||
const toggleSidebar = () => setSidebarOpen(!sidebarOpen);
|
const toggleSidebar = () => setSidebarOpen(!sidebarOpen);
|
||||||
|
|
||||||
@ -24,9 +25,7 @@ export default function DashboardLayout({ children, user }: DashboardLayoutProps
|
|||||||
const newCollapsed = !sidebarCollapsed;
|
const newCollapsed = !sidebarCollapsed;
|
||||||
setSidebarCollapsed(newCollapsed);
|
setSidebarCollapsed(newCollapsed);
|
||||||
// Persist to localStorage
|
// Persist to localStorage
|
||||||
if (typeof window !== 'undefined') {
|
localStorage.setItem('sidebar-collapsed', JSON.stringify(newCollapsed));
|
||||||
localStorage.setItem('sidebar-collapsed', JSON.stringify(newCollapsed));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -177,8 +176,8 @@ function SidebarContent({
|
|||||||
onItemClick();
|
onItemClick();
|
||||||
}}
|
}}
|
||||||
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md transition-colors duration-150 ${active
|
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'
|
? 'bg-indigo-100 text-indigo-900 border-r-2 border-indigo-500'
|
||||||
: 'text-gray-900 hover:bg-gray-50 hover:text-gray-900'
|
: 'text-gray-900 hover:bg-gray-50 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
title={collapsed ? children?.toString() : undefined}
|
title={collapsed ? children?.toString() : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { prisma } from "~/utils/db.server";
|
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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireAuthLevel(request, 2);
|
const user = await requireAuthLevel(request, 2);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { requireAuthLevel } from "~/utils/auth.server";
|
|||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import { prisma } from "~/utils/db.server";
|
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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireAuthLevel(request, 1);
|
const user = await requireAuthLevel(request, 1);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { prisma } from "~/utils/db.server";
|
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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireAuthLevel(request, 2);
|
const user = await requireAuthLevel(request, 2);
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { useState, useEffect } from "react";
|
|||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { prisma } from "~/utils/db.server";
|
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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireAuthLevel(request, 2);
|
const user = await requireAuthLevel(request, 2);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { prisma } from "~/utils/db.server";
|
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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireAuthLevel(request, 2);
|
const user = await requireAuthLevel(request, 2);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { prisma } from "~/utils/db.server";
|
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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireAuthLevel(request, 2);
|
const user = await requireAuthLevel(request, 2);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { prisma } from "~/utils/db.server";
|
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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireAuthLevel(request, 2);
|
const user = await requireAuthLevel(request, 2);
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import ReportSheetViewModal from "~/components/ReportSheetViewModal";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { prisma } from "~/utils/db.server";
|
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 {
|
interface ReportSheet {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { manageSheet, removeFromSheet } from "~/utils/sheet.server";
|
import { manageSheet, removeFromSheet } from "~/utils/sheet.server";
|
||||||
import { prisma } from "~/utils/db.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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireAuthLevel(request, 1); // All employees can access reports
|
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 (intent === "delete") {
|
||||||
if (typeof id !== "string") {
|
if (typeof id !== "string") {
|
||||||
return json({ errors: { form: "Invalid report ID" } }, { status: 400 });
|
return json({ errors: { form: "Invalid report ID" } }, { status: 400 });
|
||||||
@ -781,6 +887,22 @@ export default function Reports() {
|
|||||||
return true;
|
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 isReportTooOld = (report: any) => {
|
||||||
const reportDate = new Date(report.createdDate);
|
const reportDate = new Date(report.createdDate);
|
||||||
const dayBeforeToday = new Date();
|
const dayBeforeToday = new Date();
|
||||||
@ -875,7 +997,7 @@ export default function Reports() {
|
|||||||
{/* Total Reports */}
|
{/* Total Reports */}
|
||||||
<div className="bg-gray-50 rounded-lg p-4 text-center">
|
<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-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>
|
</div>
|
||||||
|
|
||||||
{/* Day Shift Count */}
|
{/* Day Shift Count */}
|
||||||
@ -1244,6 +1366,24 @@ export default function Reports() {
|
|||||||
Duplicate
|
Duplicate
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : 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) && (
|
{canEditReport(report) && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@ -1354,6 +1494,23 @@ export default function Reports() {
|
|||||||
Duplicate as {report.shift === 'day' ? 'Night' : 'Day'} Shift (Too Old)
|
Duplicate as {report.shift === 'day' ? 'Night' : 'Day'} Shift (Too Old)
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : 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) && (
|
{canEditReport(report) && (
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { manageSheet } from "~/utils/sheet.server";
|
import { manageSheet } from "~/utils/sheet.server";
|
||||||
import { prisma } from "~/utils/db.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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const user = await requireAuthLevel(request, 1); // All employees can create reports
|
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({
|
const report = await prisma.report.create({
|
||||||
data: {
|
data: {
|
||||||
employeeId: user.id,
|
employeeId: user.id,
|
||||||
@ -121,9 +149,9 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
},
|
},
|
||||||
pipelineLength: {
|
pipelineLength: {
|
||||||
main: parseInt(pipelineMain as string) || 0,
|
main: parseInt(pipelineMain as string) || 0,
|
||||||
ext1: parseInt(pipelineExt1 as string) || 0,
|
ext1: ext1Value,
|
||||||
reserve: parseInt(pipelineReserve as string) || 0,
|
reserve: parseInt(pipelineReserve as string) || 0,
|
||||||
ext2: parseInt(pipelineExt2 as string) || 0
|
ext2: ext2Value
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
Dozers: parseInt(statsDozers as string) || 0,
|
Dozers: parseInt(statsDozers as string) || 0,
|
||||||
@ -134,7 +162,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
},
|
},
|
||||||
timeSheet,
|
timeSheet,
|
||||||
stoppages,
|
stoppages,
|
||||||
notes: notes || null
|
notes: finalNotes || null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -206,6 +234,7 @@ export default function NewReport() {
|
|||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
const totalSteps = 4;
|
const totalSteps = 4;
|
||||||
|
const [showZeroEquipmentConfirm, setShowZeroEquipmentConfirm] = useState(false);
|
||||||
|
|
||||||
const isSubmitting = navigation.state === "submitting";
|
const isSubmitting = navigation.state === "submitting";
|
||||||
|
|
||||||
@ -225,6 +254,18 @@ export default function NewReport() {
|
|||||||
return false;
|
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("Allowing form submission");
|
||||||
// console.log("Form being submitted with data:", formData);
|
// console.log("Form being submitted with data:", formData);
|
||||||
// console.log("Time sheet entries:", timeSheetEntries);
|
// 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
|
// Stoppage management
|
||||||
const addStoppageEntry = () => {
|
const addStoppageEntry = () => {
|
||||||
const newEntry = {
|
const newEntry = {
|
||||||
@ -341,8 +413,8 @@ export default function NewReport() {
|
|||||||
from: '',
|
from: '',
|
||||||
to: '',
|
to: '',
|
||||||
total: '00:00',
|
total: '00:00',
|
||||||
reason: '',
|
reason: '', // Will be set to 'none' for reclamation by default
|
||||||
responsible: '',
|
responsible: 'reclamation', // Default to reclamation
|
||||||
note: ''
|
note: ''
|
||||||
};
|
};
|
||||||
setStoppageEntries([...stoppageEntries, newEntry]);
|
setStoppageEntries([...stoppageEntries, newEntry]);
|
||||||
@ -359,6 +431,13 @@ export default function NewReport() {
|
|||||||
if (['from', 'to'].includes(field)) {
|
if (['from', 'to'].includes(field)) {
|
||||||
updatedEntry.total = calculateStoppageTime(updatedEntry.from, updatedEntry.to);
|
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 updatedEntry;
|
||||||
}
|
}
|
||||||
return entry;
|
return entry;
|
||||||
@ -370,6 +449,16 @@ export default function NewReport() {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
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);
|
// console.log("Next step clicked, current step:", currentStep);
|
||||||
if (currentStep < totalSteps) {
|
if (currentStep < totalSteps) {
|
||||||
setCurrentStep(currentStep + 1);
|
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 = () => {
|
const prevStep = () => {
|
||||||
if (currentStep > 1) {
|
if (currentStep > 1) {
|
||||||
setCurrentStep(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 (
|
return (
|
||||||
<DashboardLayout user={user}>
|
<DashboardLayout user={user}>
|
||||||
<div className="max-w-full mx-auto">
|
<div className="max-w-full mx-auto">
|
||||||
@ -594,9 +724,9 @@ export default function NewReport() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Equipment Statistics</h3>
|
<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 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="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</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="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</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="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="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><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>
|
</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">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">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">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">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><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">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 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>
|
||||||
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -694,7 +824,7 @@ export default function NewReport() {
|
|||||||
|
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
{currentStep < totalSteps ? (
|
{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>
|
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>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
@ -754,6 +884,44 @@ export default function NewReport() {
|
|||||||
<input type="hidden" name="notes" value={formData.notes} />
|
<input type="hidden" name="notes" value={formData.notes} />
|
||||||
)}
|
)}
|
||||||
</Form>
|
</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>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { json, redirect } from "@remix-run/node";
|
|||||||
import { Form, Link, useActionData, useSearchParams } from "@remix-run/react";
|
import { Form, Link, useActionData, useSearchParams } from "@remix-run/react";
|
||||||
import { createUserSession, getUserId, verifyLogin } from "~/utils/auth.server";
|
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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const userId = await getUserId(request);
|
const userId = await getUserId(request);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Form, Link, useActionData } from "@remix-run/react";
|
|||||||
import { createUser, createUserSession, getUserId } from "~/utils/auth.server";
|
import { createUser, createUserSession, getUserId } from "~/utils/auth.server";
|
||||||
import { prisma } from "~/utils/db.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) => {
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const userId = await getUserId(request);
|
const userId = await getUserId(request);
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import DashboardLayout from "~/components/DashboardLayout";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { prisma } from "~/utils/db.server";
|
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 {
|
interface StoppageEntry {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
75
compose.yml
Normal file
75
compose.yml
Normal 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
|
||||||
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
27
start.sh
Normal file
27
start.sh
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user