added init files

This commit is contained in:
2025-10-08 02:32:39 +02:00
parent 8d65129e24
commit c70145ca50
51 changed files with 23237 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
// backend/src/controllers/authController.ts
import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService';
import { AuthRequest } from '../middleware/auth';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
export const login = async (req: Request, res: Response): Promise<void> => {
try {
const { email, password } = req.body;
if (!email || !password) {
res.status(400).json({ error: 'Email and password are required' });
return;
}
// User aus Datenbank holen
const user = await db.get<any>(
'SELECT * FROM users WHERE email = ?',
[email]
);
if (!user) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
// Passwort vergleichen
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
// JWT Token generieren
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
JWT_SECRET as jwt.Secret,
{ expiresIn: JWT_EXPIRES_IN as jwt.SignOptions['expiresIn'] }
);
// User ohne Passwort zurückgeben
const { password: _, ...userWithoutPassword } = user;
res.json({
user: userWithoutPassword,
token,
expiresIn: JWT_EXPIRES_IN
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const register = async (req: Request, res: Response): Promise<void> => {
try {
const { email, password, name, role = 'user' } = req.body;
if (!email || !password || !name) {
res.status(400).json({ error: 'Email, password and name are required' });
return;
}
// Check if user already exists
const existingUser = await db.get<any>(
'SELECT id FROM users WHERE email = ?',
[email]
);
if (existingUser) {
res.status(409).json({ error: 'User already exists' });
return;
}
// Validate role
const validRoles = ['admin', 'instandhalter', 'user'];
if (!validRoles.includes(role)) {
res.status(400).json({ error: 'Invalid role' });
return;
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
const userId = uuidv4();
// Create user
await db.run(
'INSERT INTO users (id, email, password, name, role) VALUES (?, ?, ?, ?, ?)',
[userId, email, hashedPassword, name, role]
);
// Generate token
const token = jwt.sign(
{
userId,
email,
role
},
JWT_SECRET as jwt.Secret,
{ expiresIn: JWT_EXPIRES_IN as jwt.SignOptions['expiresIn'] }
);
// Return user without password
const user = {
id: userId,
email,
name,
role,
createdAt: new Date().toISOString()
};
res.status(201).json({
user,
token,
expiresIn: JWT_EXPIRES_IN
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const logout = async (req: AuthRequest, res: Response): Promise<void> => {
try {
// Bei JWT gibt es keinen Server-side logout, aber wir können den Token client-seitig entfernen
res.json({ message: 'Logged out successfully' });
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const getCurrentUser = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({ error: 'Not authenticated' });
return;
}
const user = await db.get<any>(
'SELECT id, email, name, role, created_at FROM users WHERE id = ?',
[userId]
);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json(user);
} catch (error) {
console.error('Get current user error:', error);
res.status(500).json({ error: 'Internal server error' });
}
};

View File

@@ -0,0 +1,334 @@
// backend/src/controllers/shiftPlanController.ts
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService';
import { AuthRequest } from '../middleware/auth';
export interface ShiftPlan {
id: string;
name: string;
startDate: string;
endDate: string;
templateId?: string;
status: 'draft' | 'published';
createdBy: string;
createdAt: string;
}
export interface AssignedShift {
id: string;
shiftPlanId: string;
date: string;
startTime: string;
endTime: string;
requiredEmployees: number;
assignedEmployees: string[];
}
export interface CreateShiftPlanRequest {
name: string;
startDate: string;
endDate: string;
templateId?: string;
}
export const getShiftPlans = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const userId = req.user?.userId;
const userRole = req.user?.role;
let query = `
SELECT sp.*, u.name as created_by_name
FROM shift_plans sp
LEFT JOIN users u ON sp.created_by = u.id
`;
// Regular users can only see published plans
if (userRole === 'user') {
query += ` WHERE sp.status = 'published'`;
}
query += ` ORDER BY sp.created_at DESC`;
const shiftPlans = await db.all<ShiftPlan>(query);
res.json(shiftPlans);
} catch (error) {
console.error('Error fetching shift plans:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const getShiftPlan = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const { id } = req.params;
const userId = req.user?.userId;
const userRole = req.user?.role;
let query = `
SELECT sp.*, u.name as created_by_name
FROM shift_plans sp
LEFT JOIN users u ON sp.created_by = u.id
WHERE sp.id = ?
`;
// Regular users can only see published plans
if (userRole === 'user') {
query += ` AND sp.status = 'published'`;
}
const shiftPlan = await db.get<ShiftPlan>(query, [id]);
if (!shiftPlan) {
res.status(404).json({ error: 'Shift plan not found' });
return;
}
// Load assigned shifts
const assignedShifts = await db.all<any>(`
SELECT * FROM assigned_shifts
WHERE shift_plan_id = ?
ORDER BY date, start_time
`, [id]);
const shiftPlanWithShifts = {
...shiftPlan,
shifts: assignedShifts.map(shift => ({
id: shift.id,
date: shift.date,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
assignedEmployees: JSON.parse(shift.assigned_employees || '[]')
}))
};
res.json(shiftPlanWithShifts);
} catch (error) {
console.error('Error fetching shift plan:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const createShiftPlan = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const { name, startDate, endDate, templateId }: CreateShiftPlanRequest = req.body;
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
if (!name || !startDate || !endDate) {
res.status(400).json({ error: 'Name, start date and end date are required' });
return;
}
const shiftPlanId = uuidv4();
await db.run('BEGIN TRANSACTION');
try {
// Create shift plan
await db.run(
`INSERT INTO shift_plans (id, name, start_date, end_date, template_id, status, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[shiftPlanId, name, startDate, endDate, templateId, 'draft', userId]
);
// If template is provided, generate shifts from template
if (templateId) {
await generateShiftsFromTemplate(shiftPlanId, templateId, startDate, endDate);
}
await db.run('COMMIT');
// Return created shift plan
const createdPlan = await db.get<ShiftPlan>(`
SELECT sp.*, u.name as created_by_name
FROM shift_plans sp
LEFT JOIN users u ON sp.created_by = u.id
WHERE sp.id = ?
`, [shiftPlanId]);
const assignedShifts = await db.all<any>(`
SELECT * FROM assigned_shifts
WHERE shift_plan_id = ?
ORDER BY date, start_time
`, [shiftPlanId]);
res.status(201).json({
...createdPlan,
shifts: assignedShifts.map(shift => ({
id: shift.id,
date: shift.date,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
assignedEmployees: JSON.parse(shift.assigned_employees || '[]')
}))
});
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Error creating shift plan:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const updateShiftPlan = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const { id } = req.params;
const { name, status, shifts } = req.body;
const userId = req.user?.userId;
// Check if shift plan exists
const existingPlan: any = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]);
if (!existingPlan) {
res.status(404).json({ error: 'Shift plan not found' });
return;
}
// Check permissions (only admin/instandhalter or creator can update)
if (existingPlan.created_by !== userId && !['admin', 'instandhalter'].includes(req.user?.role || '')) {
res.status(403).json({ error: 'Insufficient permissions' });
return;
}
await db.run('BEGIN TRANSACTION');
try {
// Update shift plan
if (name !== undefined || status !== undefined) {
await db.run(
`UPDATE shift_plans
SET name = COALESCE(?, name),
status = COALESCE(?, status)
WHERE id = ?`,
[name, status, id]
);
}
// Update shifts if provided
if (shifts) {
for (const shift of shifts) {
await db.run(
`UPDATE assigned_shifts
SET required_employees = ?,
assigned_employees = ?
WHERE id = ? AND shift_plan_id = ?`,
[shift.requiredEmployees, JSON.stringify(shift.assignedEmployees || []), shift.id, id]
);
}
}
await db.run('COMMIT');
// Return updated shift plan
const updatedPlan = await db.get<ShiftPlan>(`
SELECT sp.*, u.name as created_by_name
FROM shift_plans sp
LEFT JOIN users u ON sp.created_by = u.id
WHERE sp.id = ?
`, [id]);
const assignedShifts = await db.all<any>(`
SELECT * FROM assigned_shifts
WHERE shift_plan_id = ?
ORDER BY date, start_time
`, [id]);
res.json({
...updatedPlan,
shifts: assignedShifts.map(shift => ({
id: shift.id,
date: shift.date,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
assignedEmployees: JSON.parse(shift.assigned_employees || '[]')
}))
});
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Error updating shift plan:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const deleteShiftPlan = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const { id } = req.params;
const userId = req.user?.userId;
// Check if shift plan exists
const existingPlan: any = await db.get('SELECT * FROM shift_plans WHERE id = ?', [id]);
if (!existingPlan) {
res.status(404).json({ error: 'Shift plan not found' });
return;
}
// Check permissions (only admin/instandhalter or creator can delete)
if (existingPlan.created_by !== userId && !['admin', 'instandhalter'].includes(req.user?.role || '')) {
res.status(403).json({ error: 'Insufficient permissions' });
return;
}
await db.run('DELETE FROM shift_plans WHERE id = ?', [id]);
// Assigned shifts will be automatically deleted due to CASCADE
res.status(204).send();
} catch (error) {
console.error('Error deleting shift plan:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
// Helper function to generate shifts from template
async function generateShiftsFromTemplate(shiftPlanId: string, templateId: string, startDate: string, endDate: string): Promise<void> {
// Get template shifts
const templateShifts = await db.all<any>(`
SELECT * FROM template_shifts
WHERE template_id = ?
ORDER BY day_of_week, start_time
`, [templateId]);
const start = new Date(startDate);
const end = new Date(endDate);
// Generate shifts for each day in the date range
for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) {
const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc.
// Find template shifts for this day of week
const shiftsForDay = templateShifts.filter(shift => shift.day_of_week === dayOfWeek);
for (const templateShift of shiftsForDay) {
const shiftId = uuidv4();
await db.run(
`INSERT INTO assigned_shifts (id, shift_plan_id, date, start_time, end_time, required_employees, assigned_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
shiftId,
shiftPlanId,
date.toISOString().split('T')[0],
templateShift.start_time,
templateShift.end_time,
templateShift.required_employees,
JSON.stringify([])
]
);
}
}
}

View File

@@ -0,0 +1,280 @@
// backend/src/controllers/shiftTemplateController.ts
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService';
import { ShiftTemplate, CreateShiftTemplateRequest, UpdateShiftTemplateRequest } from '../models/ShiftTemplate';
export const getTemplates = async (req: Request, res: Response): Promise<void> => {
try {
const templates = await db.all<ShiftTemplate>(`
SELECT st.*, u.name as created_by_name
FROM shift_templates st
LEFT JOIN users u ON st.created_by = u.id
ORDER BY st.created_at DESC
`);
// Für jede Vorlage die Schichten laden
const templatesWithShifts = await Promise.all(
templates.map(async (template) => {
const shifts = await db.all<any>(`
SELECT * FROM template_shifts
WHERE template_id = ?
ORDER BY day_of_week, start_time
`, [template.id]);
return {
...template,
shifts: shifts.map(shift => ({
id: shift.id,
dayOfWeek: shift.day_of_week,
name: shift.name,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
color: shift.color
}))
};
})
);
res.json(templatesWithShifts);
} catch (error) {
console.error('Error fetching templates:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const getTemplate = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params;
const template = await db.get<ShiftTemplate>(`
SELECT st.*, u.name as created_by_name
FROM shift_templates st
LEFT JOIN users u ON st.created_by = u.id
WHERE st.id = ?
`, [id]);
if (!template) {
res.status(404).json({ error: 'Template not found' });
return;
}
const shifts = await db.all<any>(`
SELECT * FROM template_shifts
WHERE template_id = ?
ORDER BY day_of_week, start_time
`, [id]);
const templateWithShifts = {
...template,
shifts: shifts.map(shift => ({
id: shift.id,
dayOfWeek: shift.day_of_week,
name: shift.name,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
color: shift.color
}))
};
res.json(templateWithShifts);
} catch (error) {
console.error('Error fetching template:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const createTemplate = async (req: Request, res: Response): Promise<void> => {
try {
const { name, description, isDefault, shifts }: CreateShiftTemplateRequest = req.body;
const userId = (req as any).user?.userId; // From auth middleware
if (!userId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const templateId = uuidv4();
// Start transaction
await db.run('BEGIN TRANSACTION');
try {
// Insert template
await db.run(
`INSERT INTO shift_templates (id, name, description, is_default, created_by)
VALUES (?, ?, ?, ?, ?)`,
[templateId, name, description, isDefault ? 1 : 0, userId]
);
// Insert shifts
for (const shift of shifts) {
const shiftId = uuidv4();
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[shiftId, templateId, shift.dayOfWeek, shift.name, shift.startTime, shift.endTime, shift.requiredEmployees, shift.color || '#3498db']
);
}
// If this is set as default, remove default from other templates
if (isDefault) {
await db.run(
`UPDATE shift_templates SET is_default = 0 WHERE id != ? AND is_default = 1`,
[templateId]
);
}
await db.run('COMMIT');
// Return created template
const createdTemplate = await db.get<ShiftTemplate>(`
SELECT st.*, u.name as created_by_name
FROM shift_templates st
LEFT JOIN users u ON st.created_by = u.id
WHERE st.id = ?
`, [templateId]);
const templateShifts = await db.all<any>(`
SELECT * FROM template_shifts
WHERE template_id = ?
ORDER BY day_of_week, start_time
`, [templateId]);
res.status(201).json({
...createdTemplate,
shifts: templateShifts.map(shift => ({
id: shift.id,
dayOfWeek: shift.day_of_week,
name: shift.name,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
color: shift.color
}))
});
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Error creating template:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const updateTemplate = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params;
const { name, description, isDefault, shifts }: UpdateShiftTemplateRequest = req.body;
// Check if template exists
const existingTemplate = await db.get('SELECT * FROM shift_templates WHERE id = ?', [id]);
if (!existingTemplate) {
res.status(404).json({ error: 'Template not found' });
return;
}
await db.run('BEGIN TRANSACTION');
try {
// Update template
if (name !== undefined || description !== undefined || isDefault !== undefined) {
await db.run(
`UPDATE shift_templates
SET name = COALESCE(?, name),
description = COALESCE(?, description),
is_default = COALESCE(?, is_default)
WHERE id = ?`,
[name, description, isDefault ? 1 : 0, id]
);
}
// If updating shifts, replace all shifts
if (shifts) {
// Delete existing shifts
await db.run('DELETE FROM template_shifts WHERE template_id = ?', [id]);
// Insert new shifts
for (const shift of shifts) {
const shiftId = uuidv4();
await db.run(
`INSERT INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[shiftId, id, shift.dayOfWeek, shift.name, shift.startTime, shift.endTime, shift.requiredEmployees, shift.color || '#3498db']
);
}
}
// If this is set as default, remove default from other templates
if (isDefault) {
await db.run(
`UPDATE shift_templates SET is_default = 0 WHERE id != ? AND is_default = 1`,
[id]
);
}
await db.run('COMMIT');
// Return updated template
const updatedTemplate = await db.get<ShiftTemplate>(`
SELECT st.*, u.name as created_by_name
FROM shift_templates st
LEFT JOIN users u ON st.created_by = u.id
WHERE st.id = ?
`, [id]);
const templateShifts = await db.all<any>(`
SELECT * FROM template_shifts
WHERE template_id = ?
ORDER BY day_of_week, start_time
`, [id]);
res.json({
...updatedTemplate,
shifts: templateShifts.map(shift => ({
id: shift.id,
dayOfWeek: shift.day_of_week,
name: shift.name,
startTime: shift.start_time,
endTime: shift.end_time,
requiredEmployees: shift.required_employees,
color: shift.color
}))
});
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Error updating template:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const deleteTemplate = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params;
// Check if template exists
const existingTemplate = await db.get('SELECT * FROM shift_templates WHERE id = ?', [id]);
if (!existingTemplate) {
res.status(404).json({ error: 'Template not found' });
return;
}
await db.run('DELETE FROM shift_templates WHERE id = ?', [id]);
// Template shifts will be automatically deleted due to CASCADE
res.status(204).send();
} catch (error) {
console.error('Error deleting template:', error);
res.status(500).json({ error: 'Internal server error' });
}
};