added listing shift plans

This commit is contained in:
2025-10-10 19:49:49 +02:00
parent 541af0afee
commit 9cc0778384
5 changed files with 176 additions and 24 deletions

View File

@@ -96,6 +96,7 @@ export const getShiftPlan = async (req: AuthRequest, res: Response): Promise<voi
shifts: assignedShifts.map(shift => ({ shifts: assignedShifts.map(shift => ({
id: shift.id, id: shift.id,
date: shift.date, date: shift.date,
name: shift.name,
startTime: shift.start_time, startTime: shift.start_time,
endTime: shift.end_time, endTime: shift.end_time,
requiredEmployees: shift.required_employees, requiredEmployees: shift.required_employees,
@@ -163,6 +164,7 @@ export const createShiftPlan = async (req: AuthRequest, res: Response): Promise<
shifts: assignedShifts.map(shift => ({ shifts: assignedShifts.map(shift => ({
id: shift.id, id: shift.id,
date: shift.date, date: shift.date,
name: shift.name,
startTime: shift.start_time, startTime: shift.start_time,
endTime: shift.end_time, endTime: shift.end_time,
requiredEmployees: shift.required_employees, requiredEmployees: shift.required_employees,
@@ -308,7 +310,8 @@ async function generateShiftsFromTemplate(shiftPlanId: string, templateId: strin
// Generate shifts for each day in the date range // Generate shifts for each day in the date range
for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) { for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) {
const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc. // Convert JS day (0=Sunday) to our format (1=Monday, 7=Sunday)
const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay();
// Find template shifts for this day of week // Find template shifts for this day of week
const shiftsForDay = templateShifts.filter(shift => shift.day_of_week === dayOfWeek); const shiftsForDay = templateShifts.filter(shift => shift.day_of_week === dayOfWeek);
@@ -317,12 +320,13 @@ async function generateShiftsFromTemplate(shiftPlanId: string, templateId: strin
const shiftId = uuidv4(); const shiftId = uuidv4();
await db.run( await db.run(
`INSERT INTO assigned_shifts (id, shift_plan_id, date, start_time, end_time, required_employees, assigned_employees) `INSERT INTO assigned_shifts (id, shift_plan_id, date, name, start_time, end_time, required_employees, assigned_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[ [
shiftId, shiftId,
shiftPlanId, shiftPlanId,
date.toISOString().split('T')[0], date.toISOString().split('T')[0],
templateShift.name,
templateShift.start_time, templateShift.start_time,
templateShift.end_time, templateShift.end_time,
templateShift.required_employees, templateShift.required_employees,

View File

@@ -0,0 +1,2 @@
-- backend/src/database/migrations/003_add_shift_name.sql
ALTER TABLE assigned_shifts ADD COLUMN name TEXT;

View File

@@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS shift_templates (
CREATE TABLE IF NOT EXISTS template_shifts ( CREATE TABLE IF NOT EXISTS template_shifts (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
template_id TEXT NOT NULL, template_id TEXT NOT NULL,
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 7), day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 5),
name TEXT NOT NULL, name TEXT NOT NULL,
start_time TEXT NOT NULL, start_time TEXT NOT NULL,
end_time TEXT NOT NULL, end_time TEXT NOT NULL,
@@ -64,7 +64,7 @@ CREATE TABLE IF NOT EXISTS assigned_shifts (
CREATE TABLE IF NOT EXISTS employee_availabilities ( CREATE TABLE IF NOT EXISTS employee_availabilities (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL, employee_id TEXT NOT NULL,
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6), day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 4),
start_time TEXT NOT NULL, start_time TEXT NOT NULL,
end_time TEXT NOT NULL, end_time TEXT NOT NULL,
is_available BOOLEAN DEFAULT FALSE, is_available BOOLEAN DEFAULT FALSE,

View File

@@ -1,14 +1,80 @@
// frontend/src/pages/ShiftPlans/ShiftPlanList.tsx // frontend/src/pages/ShiftPlans/ShiftPlanList.tsx
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import { shiftPlanService, ShiftPlan } from '../../services/shiftPlanService';
import { useNotification } from '../../contexts/NotificationContext';
const ShiftPlanList: React.FC = () => { const ShiftPlanList: React.FC = () => {
const { hasRole } = useAuth(); const { hasRole } = useAuth();
const navigate = useNavigate();
const { showNotification } = useNotification();
const [shiftPlans, setShiftPlans] = useState<ShiftPlan[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadShiftPlans();
}, []);
const loadShiftPlans = async () => {
try {
const plans = await shiftPlanService.getShiftPlans();
setShiftPlans(plans);
} catch (error) {
console.error('Error loading shift plans:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Die Schichtpläne konnten nicht geladen werden.'
});
} finally {
setLoading(false);
}
};
const handleDelete = async (id: string) => {
if (!window.confirm('Möchten Sie diesen Schichtplan wirklich löschen?')) {
return;
}
try {
await shiftPlanService.deleteShiftPlan(id);
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Der Schichtplan wurde erfolgreich gelöscht.'
});
loadShiftPlans();
} catch (error) {
console.error('Error deleting shift plan:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Der Schichtplan konnte nicht gelöscht werden.'
});
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
if (loading) {
return <div>Lade Schichtpläne...</div>;
}
return ( return (
<div> <div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '30px' }}> <div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px'
}}>
<h1>📅 Schichtpläne</h1> <h1>📅 Schichtpläne</h1>
{hasRole(['admin', 'instandhalter']) && ( {hasRole(['admin', 'instandhalter']) && (
<Link to="/shift-plans/new"> <Link to="/shift-plans/new">
@@ -17,28 +83,107 @@ const ShiftPlanList: React.FC = () => {
backgroundColor: '#3498db', backgroundColor: '#3498db',
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '4px' borderRadius: '4px',
cursor: 'pointer'
}}> }}>
+ Neuen Plan + Neuen Plan
</button> </button>
</Link> </Link>
)} )}
</div> </div>
<div style={{ {shiftPlans.length === 0 ? (
padding: '40px', <div style={{
textAlign: 'center', padding: '40px',
backgroundColor: '#f8f9fa', textAlign: 'center',
borderRadius: '8px', backgroundColor: '#f8f9fa',
border: '2px dashed #dee2e6' borderRadius: '8px',
}}> border: '2px dashed #dee2e6'
<div style={{ fontSize: '48px', marginBottom: '20px' }}>📋</div> }}>
<h3>Schichtpläne Übersicht</h3> <div style={{ fontSize: '48px', marginBottom: '20px' }}>📋</div>
<p>Hier werden alle Schichtpläne angezeigt.</p> <h3>Keine Schichtpläne vorhanden</h3>
<p style={{ fontSize: '14px', color: '#6c757d' }}> <p>Erstellen Sie Ihren ersten Schichtplan!</p>
Diese Seite wird demnächst mit Funktionen gefüllt. </div>
</p> ) : (
</div> <div style={{ display: 'grid', gap: '20px' }}>
{shiftPlans.map(plan => (
<div
key={plan.id}
style={{
padding: '20px',
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<div>
<h3 style={{ margin: '0 0 10px 0' }}>{plan.name}</h3>
<div style={{ color: '#666', fontSize: '14px' }}>
<p style={{ margin: '0' }}>
Zeitraum: {formatDate(plan.startDate)} - {formatDate(plan.endDate)}
</p>
<p style={{ margin: '5px 0 0 0' }}>
Status: <span style={{
color: plan.status === 'published' ? '#2ecc71' : '#f1c40f',
fontWeight: 'bold'
}}>
{plan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
</span>
</p>
</div>
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<button
onClick={() => navigate(`/shift-plans/${plan.id}`)}
style={{
padding: '8px 16px',
backgroundColor: '#2ecc71',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Anzeigen
</button>
{hasRole(['admin', 'instandhalter']) && (
<>
<button
onClick={() => navigate(`/shift-plans/${plan.id}/edit`)}
style={{
padding: '8px 16px',
backgroundColor: '#f1c40f',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Bearbeiten
</button>
<button
onClick={() => handleDelete(plan.id)}
style={{
padding: '8px 16px',
backgroundColor: '#e74c3c',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Löschen
</button>
</>
)}
</div>
</div>
))}
</div>
)}
</div> </div>
); );
}; };

View File

@@ -26,6 +26,7 @@ export interface ShiftPlanShift {
id: string; id: string;
shiftPlanId: string; shiftPlanId: string;
date: string; date: string;
name: string;
startTime: string; startTime: string;
endTime: string; endTime: string;
requiredEmployees: number; requiredEmployees: number;