added shiftemplates

This commit is contained in:
2025-10-08 22:14:46 +02:00
parent bc15e644b8
commit ceb7058d0b
10 changed files with 737 additions and 171 deletions

View File

@@ -86,6 +86,57 @@ export const getTemplate = async (req: Request, res: Response): Promise<void> =>
} }
}; };
export const createDefaultTemplate = async (userId: string): Promise<string> => {
try {
const templateId = uuidv4();
await db.run('BEGIN TRANSACTION');
try {
// Erstelle die Standard-Vorlage
await db.run(
`INSERT INTO shift_templates (id, name, description, is_default, created_by)
VALUES (?, ?, ?, ?, ?)`,
[templateId, 'Standardwoche', 'Mo-Do: 2 Schichten, Fr: 1 Schicht', true, userId]
);
// Vormittagsschicht Mo-Do
for (let day = 1; day <= 4; day++) {
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, day, 'Vormittagsschicht', '08:00', '12:00', 1]
);
}
// Nachmittagsschicht Mo-Do
for (let day = 1; day <= 4; day++) {
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, day, 'Nachmittagsschicht', '11:30', '15:30', 1]
);
}
// Freitag nur Vormittagsschicht
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, 5, 'Vormittagsschicht', '08:00', '12:00', 1]
);
await db.run('COMMIT');
return templateId;
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Error creating default template:', error);
throw error;
}
};
export const createTemplate = async (req: Request, res: Response): Promise<void> => { export const createTemplate = async (req: Request, res: Response): Promise<void> => {
try { try {
const { name, description, isDefault, shifts }: CreateShiftTemplateRequest = req.body; const { name, description, isDefault, shifts }: CreateShiftTemplateRequest = req.body;
@@ -96,6 +147,12 @@ export const createTemplate = async (req: Request, res: Response): Promise<void>
return; return;
} }
// Wenn diese Vorlage als Standard markiert werden soll,
// zuerst alle anderen Vorlagen auf nicht-Standard setzen
if (isDefault) {
await db.run('UPDATE shift_templates SET is_default = 0');
}
const templateId = uuidv4(); const templateId = uuidv4();
// Start transaction // Start transaction
@@ -182,6 +239,12 @@ export const updateTemplate = async (req: Request, res: Response): Promise<void>
await db.run('BEGIN TRANSACTION'); await db.run('BEGIN TRANSACTION');
try { try {
// Wenn diese Vorlage als Standard markiert werden soll,
// zuerst alle anderen Vorlagen auf nicht-Standard setzen
if (isDefault) {
await db.run('UPDATE shift_templates SET is_default = 0');
}
// Update template // Update template
if (name !== undefined || description !== undefined || isDefault !== undefined) { if (name !== undefined || description !== undefined || isDefault !== undefined) {
await db.run( await db.run(

View File

@@ -0,0 +1,97 @@
.editorContainer {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.title {
font-size: 24px;
color: #2c3e50;
margin: 0;
}
.buttons {
display: flex;
gap: 10px;
}
.previewButton {
padding: 8px 16px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.previewButton:hover {
background-color: #2980b9;
}
.saveButton {
padding: 8px 16px;
background-color: #2ecc71;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.saveButton:hover {
background-color: #27ae60;
}
.formGroup {
margin-bottom: 20px;
}
.formGroup label {
display: block;
margin-bottom: 8px;
color: #34495e;
font-weight: 500;
}
.formGroup input[type="text"],
.formGroup textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #bdc3c7;
border-radius: 4px;
font-size: 14px;
}
.formGroup textarea {
min-height: 100px;
resize: vertical;
}
.defaultCheckbox {
margin-top: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.defaultCheckbox input[type="checkbox"] {
width: 16px;
height: 16px;
}
.defaultCheckbox label {
margin: 0;
font-size: 14px;
}
.previewContainer {
margin-top: 30px;
border-top: 1px solid #ddd;
padding-top: 20px;
}

View File

@@ -4,8 +4,15 @@ import { useParams, useNavigate } from 'react-router-dom';
import { ShiftTemplate, TemplateShift, DEFAULT_DAYS } from '../../types/shiftTemplate'; import { ShiftTemplate, TemplateShift, DEFAULT_DAYS } from '../../types/shiftTemplate';
import { shiftTemplateService } from '../../services/shiftTemplateService'; import { shiftTemplateService } from '../../services/shiftTemplateService';
import ShiftDayEditor from './components/ShiftDayEditor'; import ShiftDayEditor from './components/ShiftDayEditor';
import DefaultTemplateView from './components/DefaultTemplateView';
import styles from './ShiftTemplateEditor.module.css';
const defaultShift: Omit<TemplateShift, 'id'> = { interface ExtendedTemplateShift extends Omit<TemplateShift, 'id'> {
id?: string;
isPreview?: boolean;
}
const defaultShift: ExtendedTemplateShift = {
dayOfWeek: 1, // Montag dayOfWeek: 1, // Montag
name: '', name: '',
startTime: '08:00', startTime: '08:00',
@@ -27,6 +34,7 @@ const ShiftTemplateEditor: React.FC = () => {
}); });
const [loading, setLoading] = useState(isEditing); const [loading, setLoading] = useState(isEditing);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [showPreview, setShowPreview] = useState(false);
useEffect(() => { useEffect(() => {
if (isEditing) { if (isEditing) {
@@ -104,84 +112,93 @@ const ShiftTemplateEditor: React.FC = () => {
})); }));
}; };
// Preview-Daten für die DefaultTemplateView vorbereiten
const previewTemplate: ShiftTemplate = {
id: 'preview',
name: template.name || 'Vorschau',
description: template.description,
shifts: template.shifts.map(shift => ({
...shift,
id: shift.id || 'preview-' + Date.now()
})),
createdBy: 'preview',
createdAt: new Date().toISOString(),
isDefault: template.isDefault
};
if (loading) return <div>Lade Vorlage...</div>; if (loading) return <div>Lade Vorlage...</div>;
return ( return (
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}> <div className={styles.editorContainer}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '30px' }}> <div className={styles.header}>
<h1>{isEditing ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}</h1> <h1 className={styles.title}>{isEditing ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen'}</h1>
<div style={{ display: 'flex', gap: '10px' }}> <div className={styles.buttons}>
<button <button
onClick={() => navigate('/shift-templates')} className={styles.previewButton}
style={{ padding: '10px 20px', border: '1px solid #6c757d', background: 'white', color: '#6c757d' }} onClick={() => setShowPreview(!showPreview)}
> >
Abbrechen {showPreview ? 'Editor anzeigen' : 'Vorschau'}
</button> </button>
<button <button
className={styles.saveButton}
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
style={{ padding: '10px 20px', backgroundColor: saving ? '#6c757d' : '#007bff', color: 'white', border: 'none' }}
> >
{saving ? 'Speichern...' : 'Speichern'} {saving ? 'Speichern...' : 'Speichern'}
</button> </button>
</div> </div>
</div> </div>
{/* Template Meta Information */} {showPreview ? (
<div style={{ marginBottom: '30px', padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}> <DefaultTemplateView template={previewTemplate} />
<div style={{ marginBottom: '15px' }}> ) : (
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}> <>
Vorlagenname * <div className={styles.formGroup}>
</label> <label>Vorlagenname *</label>
<input <input
type="text" type="text"
value={template.name} value={template.name}
onChange={(e) => setTemplate(prev => ({ ...prev, name: e.target.value }))} onChange={(e) => setTemplate(prev => ({ ...prev, name: e.target.value }))}
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }} placeholder="z.B. Standard Woche, Teilzeit Modell, etc."
placeholder="z.B. Standard Woche, Teilzeit Modell, etc." />
/> </div>
</div>
<div style={{ marginBottom: '15px' }}> <div className={styles.formGroup}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}> <label>Beschreibung</label>
Beschreibung <textarea
</label> value={template.description || ''}
<textarea onChange={(e) => setTemplate(prev => ({ ...prev, description: e.target.value }))}
value={template.description || ''} placeholder="Beschreibung der Vorlage (optional)"
onChange={(e) => setTemplate(prev => ({ ...prev, description: e.target.value }))} />
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px', minHeight: '60px' }} </div>
placeholder="Beschreibung der Vorlage (optional)"
/>
</div>
<div> <div className={styles.defaultCheckbox}>
<label style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<input <input
type="checkbox" type="checkbox"
id="isDefault"
checked={template.isDefault} checked={template.isDefault}
onChange={(e) => setTemplate(prev => ({ ...prev, isDefault: e.target.checked }))} onChange={(e) => setTemplate(prev => ({ ...prev, isDefault: e.target.checked }))}
/> />
Als Standardvorlage festlegen <label htmlFor="isDefault">Als Standardvorlage festlegen</label>
</label> </div>
</div>
</div>
{/* Schichten pro Tag */} <div style={{ marginTop: '30px' }}>
<div> <h2>Schichten pro Wochentag</h2>
<h2 style={{ marginBottom: '20px' }}>Schichten pro Wochentag</h2> <div style={{ display: 'grid', gap: '20px', marginTop: '20px' }}>
<div style={{ display: 'grid', gap: '20px' }}> {DEFAULT_DAYS.map(day => (
{DEFAULT_DAYS.map(day => ( <ShiftDayEditor
<ShiftDayEditor key={day.id}
key={day.id} day={day}
day={day} shifts={template.shifts.filter(s => s.dayOfWeek === day.id)}
shifts={template.shifts.filter(s => s.dayOfWeek === day.id)} onAddShift={() => addShift(day.id)}
onAddShift={() => addShift(day.id)} onUpdateShift={updateShift}
onUpdateShift={updateShift} onRemoveShift={removeShift}
onRemoveShift={removeShift} />
/> ))}
))} </div>
</div> </div>
</div> </>
)}
</div> </div>
); );
}; };

View File

@@ -0,0 +1,101 @@
.templateList {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.createButton {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.createButton:hover {
background-color: #0056b3;
}
.templateGrid {
display: grid;
gap: 15px;
}
.templateCard {
border: 1px solid #ddd;
padding: 15px;
border-radius: 8px;
background-color: #f9f9f9;
}
.templateHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.templateInfo h3 {
margin: 0 0 5px 0;
}
.templateInfo p {
margin: 0 0 10px 0;
color: #666;
}
.templateMeta {
font-size: 14px;
color: #888;
}
.defaultBadge {
color: green;
margin-left: 10px;
}
.actionButtons {
display: flex;
gap: 10px;
}
.viewButton {
padding: 5px 10px;
border: 1px solid #007bff;
color: #007bff;
background: white;
cursor: pointer;
}
.useButton {
padding: 5px 10px;
background-color: #28a745;
color: white;
border: none;
cursor: pointer;
}
.deleteButton {
padding: 5px 10px;
background-color: #dc3545;
color: white;
border: none;
cursor: pointer;
}
.viewButton:hover {
background-color: #e6f0ff;
}
.useButton:hover {
background-color: #218838;
}
.deleteButton:hover {
background-color: #c82333;
}

View File

@@ -4,11 +4,14 @@ import { Link } from 'react-router-dom';
import { ShiftTemplate } from '../../types/shiftTemplate'; import { ShiftTemplate } from '../../types/shiftTemplate';
import { shiftTemplateService } from '../../services/shiftTemplateService'; import { shiftTemplateService } from '../../services/shiftTemplateService';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import DefaultTemplateView from './components/DefaultTemplateView';
import styles from './ShiftTemplateList.module.css';
const ShiftTemplateList: React.FC = () => { const ShiftTemplateList: React.FC = () => {
const [templates, setTemplates] = useState<ShiftTemplate[]>([]); const [templates, setTemplates] = useState<ShiftTemplate[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { hasRole } = useAuth(); const { hasRole } = useAuth();
const [selectedTemplate, setSelectedTemplate] = useState<ShiftTemplate | null>(null);
useEffect(() => { useEffect(() => {
loadTemplates(); loadTemplates();
@@ -18,6 +21,11 @@ const ShiftTemplateList: React.FC = () => {
try { try {
const data = await shiftTemplateService.getTemplates(); const data = await shiftTemplateService.getTemplates();
setTemplates(data); setTemplates(data);
// Setze die Standard-Vorlage als ausgewählt
const defaultTemplate = data.find(t => t.isDefault);
if (defaultTemplate) {
setSelectedTemplate(defaultTemplate);
}
} catch (error) { } catch (error) {
console.error('Fehler:', error); console.error('Fehler:', error);
} finally { } finally {
@@ -31,6 +39,9 @@ const ShiftTemplateList: React.FC = () => {
try { try {
await shiftTemplateService.deleteTemplate(id); await shiftTemplateService.deleteTemplate(id);
setTemplates(templates.filter(t => t.id !== id)); setTemplates(templates.filter(t => t.id !== id));
if (selectedTemplate?.id === id) {
setSelectedTemplate(null);
}
} catch (error) { } catch (error) {
console.error('Löschen fehlgeschlagen:', error); console.error('Löschen fehlgeschlagen:', error);
} }
@@ -39,78 +50,85 @@ const ShiftTemplateList: React.FC = () => {
if (loading) return <div>Lade Vorlagen...</div>; if (loading) return <div>Lade Vorlagen...</div>;
return ( return (
<div style={{ padding: '20px' }}> <div className={styles.templateList}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}> <div className={styles.header}>
<h1>Schichtplan Vorlagen</h1> <h1>Schichtplan Vorlagen</h1>
{hasRole(['admin', 'instandhalter']) && ( {hasRole(['admin', 'instandhalter']) && (
<Link to="/shift-templates/new"> <Link to="/shift-templates/new">
<button style={{ padding: '10px 20px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px' }}> <button className={styles.createButton}>
Neue Vorlage Neue Vorlage
</button> </button>
</Link> </Link>
)} )}
</div> </div>
<div style={{ display: 'grid', gap: '15px' }}> <div className={styles.templateGrid}>
{templates.map(template => ( {templates.length === 0 ? (
<div key={template.id} style={{
border: '1px solid #ddd',
padding: '15px',
borderRadius: '8px',
backgroundColor: '#f9f9f9'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h3 style={{ margin: '0 0 5px 0' }}>{template.name}</h3>
{template.description && (
<p style={{ margin: '0 0 10px 0', color: '#666' }}>{template.description}</p>
)}
<div style={{ fontSize: '14px', color: '#888' }}>
{template.shifts.length} Schichttypen Erstellt am {new Date(template.createdAt).toLocaleDateString('de-DE')}
{template.isDefault && <span style={{ color: 'green', marginLeft: '10px' }}> Standard</span>}
</div>
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<Link to={`/shift-templates/${template.id}`}>
<button style={{ padding: '5px 10px', border: '1px solid #007bff', color: '#007bff', background: 'white' }}>
{hasRole(['admin', 'instandhalter']) ? 'Bearbeiten' : 'Ansehen'}
</button>
</Link>
{hasRole(['admin', 'instandhalter']) && (
<>
<Link to={`/shift-plans/new?template=${template.id}`}>
<button style={{ padding: '5px 10px', backgroundColor: '#28a745', color: 'white', border: 'none' }}>
Verwenden
</button>
</Link>
<button
onClick={() => handleDelete(template.id)}
style={{ padding: '5px 10px', backgroundColor: '#dc3545', color: 'white', border: 'none' }}
>
Löschen
</button>
</>
)}
</div>
</div>
</div>
))}
{templates.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}> <div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
<p>Noch keine Vorlagen vorhanden.</p> <p>Noch keine Vorlagen vorhanden.</p>
{hasRole(['admin', 'instandhalter']) && ( {hasRole(['admin', 'instandhalter']) && (
<Link to="/shift-templates/new"> <Link to="/shift-templates/new">
<button style={{ padding: '10px 20px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px' }}> <button className={styles.createButton}>
Erste Vorlage erstellen Erste Vorlage erstellen
</button> </button>
</Link> </Link>
)} )}
</div> </div>
) : (
templates.map(template => (
<div key={template.id} className={styles.templateCard}>
<div className={styles.templateHeader}>
<div className={styles.templateInfo}>
<h3>{template.name}</h3>
{template.description && (
<p>{template.description}</p>
)}
<div className={styles.templateMeta}>
{template.shifts.length} Schichttypen Erstellt am {new Date(template.createdAt).toLocaleDateString('de-DE')}
{template.isDefault && <span className={styles.defaultBadge}> Standard</span>}
</div>
</div>
<div className={styles.actionButtons}>
<button
className={styles.viewButton}
onClick={() => setSelectedTemplate(template)}
>
Vorschau
</button>
{hasRole(['admin', 'instandhalter']) && (
<>
<Link to={`/shift-templates/${template.id}`}>
<button className={styles.viewButton}>
Bearbeiten
</button>
</Link>
<Link to={`/shift-plans/new?template=${template.id}`}>
<button className={styles.useButton}>
Verwenden
</button>
</Link>
<button
onClick={() => handleDelete(template.id)}
className={styles.deleteButton}
>
Löschen
</button>
</>
)}
</div>
</div>
</div>
))
)} )}
</div> </div>
{selectedTemplate && (
<div style={{ marginTop: '30px' }}>
<DefaultTemplateView template={selectedTemplate} />
</div>
)}
</div> </div>
); );
}; };

View File

@@ -0,0 +1,48 @@
.defaultTemplateView {
padding: 20px;
}
.weekView {
display: flex;
gap: 20px;
margin-top: 20px;
overflow-x: auto;
}
.dayColumn {
min-width: 200px;
background: #f5f6fa;
padding: 15px;
border-radius: 8px;
}
.dayColumn h3 {
margin: 0 0 15px 0;
color: #2c3e50;
text-align: center;
}
.shiftsContainer {
display: flex;
flex-direction: column;
gap: 10px;
}
.shiftCard {
background: white;
padding: 12px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.shiftCard h4 {
margin: 0 0 8px 0;
color: #34495e;
font-size: 0.9em;
}
.shiftCard p {
margin: 0;
color: #7f8c8d;
font-size: 0.85em;
}

View File

@@ -0,0 +1,55 @@
// frontend/src/pages/ShiftTemplates/components/DefaultTemplateView.tsx
import React from 'react';
import { ShiftTemplate } from '../../../types/shiftTemplate';
import styles from './DefaultTemplateView.module.css';
interface DefaultTemplateViewProps {
template: ShiftTemplate;
}
const DefaultTemplateView: React.FC<DefaultTemplateViewProps> = ({ template }) => {
// Gruppiere Schichten nach Wochentag
const shiftsByDay = template.shifts.reduce((acc, shift) => {
const day = shift.dayOfWeek;
if (!acc[day]) {
acc[day] = [];
}
acc[day].push(shift);
return acc;
}, {} as Record<number, typeof template.shifts>);
// Funktion zum Formatieren der Zeit
const formatTime = (time: string) => {
return time.substring(0, 5); // Zeigt nur HH:MM
};
// Wochentagsnamen
const dayNames = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
return (
<div className={styles.defaultTemplateView}>
<h2>{template.name}</h2>
{template.description && <p>{template.description}</p>}
<div className={styles.weekView}>
{[1, 2, 3, 4, 5].map(dayIndex => (
<div key={dayIndex} className={styles.dayColumn}>
<h3>{dayNames[dayIndex]}</h3>
<div className={styles.shiftsContainer}>
{shiftsByDay[dayIndex]?.map(shift => (
<div key={shift.id} className={styles.shiftCard}>
<h4>{shift.name}</h4>
<p>
{formatTime(shift.startTime)} - {formatTime(shift.endTime)}
</p>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
};
export default DefaultTemplateView;

View File

@@ -0,0 +1,137 @@
.dayEditor {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.dayHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.dayName {
font-size: 18px;
color: #2c3e50;
margin: 0;
}
.addButton {
padding: 6px 12px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
}
.addButton:hover {
background-color: #2980b9;
}
.addButton svg {
width: 16px;
height: 16px;
}
.shiftsGrid {
display: grid;
gap: 15px;
}
.shiftCard {
background: white;
padding: 15px;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.shiftHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.shiftTitle {
color: #2c3e50;
font-size: 16px;
font-weight: 500;
margin: 0;
}
.deleteButton {
padding: 4px 8px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.deleteButton:hover {
background-color: #c0392b;
}
.formGroup {
margin-bottom: 12px;
}
.formGroup label {
display: block;
margin-bottom: 4px;
color: #34495e;
font-size: 14px;
}
.formGroup input {
width: 100%;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.formGroup input[type="time"] {
width: auto;
}
.timeInputs {
display: flex;
gap: 15px;
align-items: center;
}
.colorPicker {
width: 100px;
}
.requiredEmployees {
display: flex;
gap: 10px;
align-items: center;
}
.requiredEmployees input {
width: 60px;
text-align: center;
}
.requiredEmployees button {
padding: 4px 8px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.requiredEmployees button:hover {
background: #f5f6fa;
}

View File

@@ -1,6 +1,7 @@
// frontend/src/pages/ShiftTemplates/components/ShiftDayEditor.tsx // frontend/src/pages/ShiftTemplates/components/ShiftDayEditor.tsx
import React from 'react'; import React from 'react';
import { TemplateShift } from '../../../types/shiftTemplate'; import { TemplateShift } from '../../../types/shiftTemplate';
import styles from './ShiftDayEditor.module.css';
interface ShiftDayEditorProps { interface ShiftDayEditorProps {
day: { id: number; name: string }; day: { id: number; name: string };
@@ -18,13 +19,13 @@ const ShiftDayEditor: React.FC<ShiftDayEditorProps> = ({
onRemoveShift onRemoveShift
}) => { }) => {
return ( return (
<div style={{ border: '1px solid #e0e0e0', borderRadius: '8px', padding: '20px' }}> <div className={styles.dayEditor}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}> <div className={styles.dayHeader}>
<h3 style={{ margin: 0 }}>{day.name}</h3> <h3 className={styles.dayName}>{day.name}</h3>
<button <button className={styles.addButton} onClick={onAddShift}>
onClick={onAddShift} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
style={{ padding: '8px 16px', backgroundColor: '#28a745', color: 'white', border: 'none', borderRadius: '4px' }} <path fillRule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clipRule="evenodd" />
> </svg>
Schicht hinzufügen Schicht hinzufügen
</button> </button>
</div> </div>
@@ -34,73 +35,85 @@ const ShiftDayEditor: React.FC<ShiftDayEditorProps> = ({
Keine Schichten für {day.name} Keine Schichten für {day.name}
</div> </div>
) : ( ) : (
<div style={{ display: 'grid', gap: '15px' }}> <div className={styles.shiftsGrid}>
{shifts.map((shift, index) => ( {shifts.map(shift => (
<div key={shift.id} style={{ <div key={shift.id} className={styles.shiftCard}>
display: 'grid', <div className={styles.shiftHeader}>
gridTemplateColumns: '2fr 1fr 1fr 1fr auto', <h4 className={styles.shiftTitle}>Schicht bearbeiten</h4>
gap: '10px', <button
alignItems: 'center', className={styles.deleteButton}
padding: '15px', onClick={() => onRemoveShift(shift.id)}
border: '1px solid #f0f0f0', title="Schicht löschen"
borderRadius: '4px', >
backgroundColor: '#fafafa' <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
}}> <path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
{/* Schicht Name */} </svg>
<div> Löschen
</button>
</div>
<div className={styles.formGroup}>
<input <input
type="text" type="text"
value={shift.name} value={shift.name}
onChange={(e) => onUpdateShift(shift.id, { name: e.target.value })} onChange={(e) => onUpdateShift(shift.id, { name: e.target.value })}
placeholder="Schichtname" placeholder="Schichtname"
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
/> />
</div> </div>
{/* Startzeit */} <div className={styles.timeInputs}>
<div> <div className={styles.formGroup}>
<label style={{ fontSize: '12px', display: 'block', marginBottom: '2px' }}>Start</label> <label>Start</label>
<input <input
type="time" type="time"
value={shift.startTime} value={shift.startTime}
onChange={(e) => onUpdateShift(shift.id, { startTime: e.target.value })} onChange={(e) => onUpdateShift(shift.id, { startTime: e.target.value })}
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }} />
/> </div>
<div className={styles.formGroup}>
<label>Ende</label>
<input
type="time"
value={shift.endTime}
onChange={(e) => onUpdateShift(shift.id, { endTime: e.target.value })}
/>
</div>
</div> </div>
{/* Endzeit */} <div className={styles.formGroup}>
<div> <label>Benötigte Mitarbeiter</label>
<label style={{ fontSize: '12px', display: 'block', marginBottom: '2px' }}>Ende</label> <div className={styles.requiredEmployees}>
<input <button
type="time" onClick={() => onUpdateShift(shift.id, { requiredEmployees: Math.max(1, shift.requiredEmployees - 1) })}
value={shift.endTime} >
onChange={(e) => onUpdateShift(shift.id, { endTime: e.target.value })} -
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }} </button>
/> <input
type="number"
min="1"
value={shift.requiredEmployees}
onChange={(e) => onUpdateShift(shift.id, { requiredEmployees: parseInt(e.target.value) || 1 })}
/>
<button
onClick={() => onUpdateShift(shift.id, { requiredEmployees: shift.requiredEmployees + 1 })}
>
+
</button>
</div>
</div> </div>
{/* Benötigte Mitarbeiter */} {shift.color && (
<div> <div className={styles.formGroup}>
<label style={{ fontSize: '12px', display: 'block', marginBottom: '2px' }}>Mitarbeiter</label> <label>Farbe</label>
<input <input
type="number" type="color"
min="1" value={shift.color}
value={shift.requiredEmployees} onChange={(e) => onUpdateShift(shift.id, { color: e.target.value })}
onChange={(e) => onUpdateShift(shift.id, { requiredEmployees: parseInt(e.target.value) || 1 })} className={styles.colorPicker}
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }} />
/> </div>
</div> )}
{/* Löschen Button */}
<div>
<button
onClick={() => onRemoveShift(shift.id)}
style={{ padding: '8px 12px', backgroundColor: '#dc3545', color: 'white', border: 'none', borderRadius: '4px' }}
title="Schicht löschen"
>
×
</button>
</div>
</div> </div>
))} ))}
</div> </div>

View File

@@ -21,7 +21,13 @@ export const shiftTemplateService = {
throw new Error('Fehler beim Laden der Vorlagen'); throw new Error('Fehler beim Laden der Vorlagen');
} }
return response.json(); const templates = await response.json();
// Sortiere die Vorlagen so, dass die Standard-Vorlage immer zuerst kommt
return templates.sort((a: ShiftTemplate, b: ShiftTemplate) => {
if (a.isDefault && !b.isDefault) return -1;
if (!a.isDefault && b.isDefault) return 1;
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});
}, },
async getTemplate(id: string): Promise<ShiftTemplate> { async getTemplate(id: string): Promise<ShiftTemplate> {
@@ -44,6 +50,17 @@ export const shiftTemplateService = {
}, },
async createTemplate(template: Omit<ShiftTemplate, 'id' | 'createdAt' | 'createdBy'>): Promise<ShiftTemplate> { async createTemplate(template: Omit<ShiftTemplate, 'id' | 'createdAt' | 'createdBy'>): Promise<ShiftTemplate> {
// Wenn diese Vorlage als Standard markiert ist,
// fragen wir den Benutzer, ob er wirklich die Standard-Vorlage ändern möchte
if (template.isDefault) {
const confirm = window.confirm(
'Diese Vorlage wird als neue Standard-Vorlage festgelegt. Die bisherige Standard-Vorlage wird dadurch zu einer normalen Vorlage. Möchten Sie fortfahren?'
);
if (!confirm) {
throw new Error('Operation abgebrochen');
}
}
const response = await fetch(API_BASE, { const response = await fetch(API_BASE, {
method: 'POST', method: 'POST',
headers: { headers: {