Compare commits

..

16 Commits

9 changed files with 1336 additions and 223 deletions

View File

@@ -1,15 +1,8 @@
# Single stage build for workspaces
FROM node:20-bullseye AS builder
FROM node:20-bookworm AS builder
WORKDIR /app
# Install Python + OR-Tools
RUN apt-get update && apt-get install -y python3 python3-pip build-essential \
&& pip install --no-cache-dir ortools
# Create symlink so python3 is callable as python
RUN ln -sf /usr/bin/python3 /usr/bin/python
# Copy root package files first
COPY package*.json ./
COPY tsconfig.base.json ./
@@ -34,9 +27,6 @@ RUN npm run build --only=production --workspace=backend
# Build frontend
RUN npm run build --only=production --workspace=frontend
# Verify Python and OR-Tools installation
RUN python -c "from ortools.sat.python import cp_model; print('OR-Tools installed successfully')"
# Production stage
FROM node:20-bookworm
@@ -59,7 +49,20 @@ COPY --from=builder /app/frontend/dist/ ./frontend-build/
COPY --from=builder /app/ecosystem.config.cjs ./
COPY --from=builder /app/backend/src/database/ ./dist/database/
COPY --from=builder /app/backend/src/database/ ./database/
# should be obsolete with the line above
#COPY --from=builder /app/backend/src/database/ ./database/
COPY --from=builder /app/backend/src/python-scripts/ ./python-scripts/
# Install Python + OR-Tools
RUN apt-get update && apt-get install -y python3 python3-pip build-essential \
&& pip install --no-cache-dir --break-system-packages ortools
# Create symlink so python3 is callable as python
RUN ln -sf /usr/bin/python3 /usr/bin/python
# Verify Python and OR-Tools installation
RUN python -c "from ortools.sat.python import cp_model; print('OR-Tools installed successfully')"
# Copy init script and env template
COPY docker-init.sh /usr/local/bin/

View File

@@ -16,7 +16,7 @@
"dependencies": {
"@types/bcrypt": "^6.0.0",
"@types/node": "24.9.2",
"vite":"7.1.12",
"vite": "7.1.12",
"bcrypt": "^6.0.0",
"bcryptjs": "^2.4.3",
"express": "^4.18.2",
@@ -25,7 +25,9 @@
"uuid": "^9.0.0",
"express-rate-limit": "8.1.0",
"helmet": "8.1.0",
"express-validator": "7.3.0"
"express-validator": "7.3.0",
"exceljs": "4.4.0",
"playwright": "^1.37.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,9 @@ import {
updateShiftPlan,
deleteShiftPlan,
createFromPreset,
clearAssignments
clearAssignments,
exportShiftPlanToExcel,
exportShiftPlanToPDF
} from '../controllers/shiftPlanController.js';
import {
validateShiftPlan,
@@ -30,4 +32,7 @@ router.put('/:id', validateId, validateShiftPlanUpdate, handleValidationErrors,
router.delete('/:id', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), deleteShiftPlan);
router.post('/:id/clear-assignments', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), clearAssignments);
router.get('/:id/export/excel', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), exportShiftPlanToExcel);
router.get('/:id/export/pdf', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), exportShiftPlanToPDF);
export default router;

View File

@@ -27,7 +27,9 @@
"esbuild": "^0.21.0",
"terser": "5.44.0",
"babel-plugin-transform-remove-console": "6.9.4",
"framer-motion": "12.23.24"
"framer-motion": "12.23.24",
"file-saver": "2.0.5",
"@types/file-saver": "2.0.5"
},
"scripts": {
"dev": "vite dev",

View File

@@ -19,6 +19,8 @@ export const designTokens = {
9: '#cda8f0',
10: '#ebd7fa',
},
manager: '#CC0000',
// Semantic Colors
primary: '#51258f',

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ export class ApiClient {
return token ? { 'Authorization': `Bearer ${token}` } : {};
}
private async handleApiResponse<T>(response: Response): Promise<T> {
private async handleApiResponse<T>(response: Response, responseType: 'json' | 'blob' = 'json'): Promise<T> {
if (!response.ok) {
let errorData;
@@ -61,7 +61,12 @@ export class ApiClient {
);
}
// For successful responses, try to parse as JSON
// Handle blob responses (for file downloads)
if (responseType === 'blob') {
return response.blob() as Promise<T>;
}
// For successful JSON responses, try to parse as JSON
try {
const responseText = await response.text();
return responseText ? JSON.parse(responseText) : {} as T;
@@ -71,7 +76,7 @@ export class ApiClient {
}
}
async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
async request<T>(endpoint: string, options: RequestInit = {}, responseType: 'json' | 'blob' = 'json'): Promise<T> {
const url = `${this.baseURL}${endpoint}`;
const config: RequestInit = {
@@ -85,7 +90,7 @@ export class ApiClient {
try {
const response = await fetch(url, config);
return await this.handleApiResponse<T>(response);
return await this.handleApiResponse<T>(response, responseType);
} catch (error) {
// Re-throw the error to be caught by useBackendValidation
if (error instanceof ApiError) {

View File

@@ -126,4 +126,60 @@ export const shiftPlanService = {
throw error;
}
},
async exportShiftPlanToExcel(planId: string): Promise<Blob> {
try {
console.log('📊 Exporting shift plan to Excel:', planId);
// Use the apiClient with blob response handling
const blob = await apiClient.request<Blob>(`/shift-plans/${planId}/export/excel`, {
method: 'GET',
}, 'blob');
console.log('✅ Excel export successful');
return blob;
} catch (error: any) {
console.error('❌ Error exporting to Excel:', error);
if (error.statusCode === 401) {
localStorage.removeItem('token');
localStorage.removeItem('employee');
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
if (error.statusCode === 404) {
throw new Error('Schichtplan nicht gefunden');
}
throw new Error('Fehler beim Excel-Export des Schichtplans');
}
},
async exportShiftPlanToPDF(planId: string): Promise<Blob> {
try {
console.log('📄 Exporting shift plan to PDF:', planId);
// Use the apiClient with blob response handling
const blob = await apiClient.request<Blob>(`/shift-plans/${planId}/export/pdf`, {
method: 'GET',
}, 'blob');
console.log('✅ PDF export successful');
return blob;
} catch (error: any) {
console.error('❌ Error exporting to PDF:', error);
if (error.statusCode === 401) {
localStorage.removeItem('token');
localStorage.removeItem('employee');
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
if (error.statusCode === 404) {
throw new Error('Schichtplan nicht gefunden');
}
throw new Error('Fehler beim PDF-Export des Schichtplans');
}
},
};