mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
added init files
This commit is contained in:
8
backend/dockerfile
Normal file
8
backend/dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
# backend/Dockerfile
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY . .
|
||||
EXPOSE 3001
|
||||
CMD ["node", "dist/server.js"]
|
||||
2810
backend/package-lock.json
generated
Normal file
2810
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
backend/package.json
Normal file
28
backend/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "schichtplan-backend",
|
||||
"version": "1.0.0",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"dev": "ts-node src/server.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"sqlite3": "^5.1.6",
|
||||
"uuid": "^9.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"bcryptjs": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"typescript": "^5.0.0",
|
||||
"ts-node": "^10.9.0"
|
||||
}
|
||||
}
|
||||
168
backend/src/controllers/authController.ts
Normal file
168
backend/src/controllers/authController.ts
Normal 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' });
|
||||
}
|
||||
};
|
||||
334
backend/src/controllers/shiftPlanController.ts
Normal file
334
backend/src/controllers/shiftPlanController.ts
Normal 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([])
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
280
backend/src/controllers/shiftTemplateController.ts
Normal file
280
backend/src/controllers/shiftTemplateController.ts
Normal 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' });
|
||||
}
|
||||
};
|
||||
24
backend/src/database/schema.sql
Normal file
24
backend/src/database/schema.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- Zusätzliche Tabellen für shift_plans
|
||||
CREATE TABLE IF NOT EXISTS shift_plans (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
start_date TEXT NOT NULL,
|
||||
end_date TEXT NOT NULL,
|
||||
template_id TEXT,
|
||||
status TEXT CHECK(status IN ('draft', 'published')) DEFAULT 'draft',
|
||||
created_by TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||
FOREIGN KEY (template_id) REFERENCES shift_templates(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assigned_shifts (
|
||||
id TEXT PRIMARY KEY,
|
||||
shift_plan_id TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT NOT NULL,
|
||||
required_employees INTEGER DEFAULT 1,
|
||||
assigned_employees TEXT DEFAULT '[]', -- JSON array of user IDs
|
||||
FOREIGN KEY (shift_plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE
|
||||
);
|
||||
40
backend/src/middleware/auth.ts
Normal file
40
backend/src/middleware/auth.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// backend/src/middleware/auth.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const authMiddleware = (req: AuthRequest, res: Response, next: NextFunction): void => {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
res.status(401).json({ error: 'Access denied. No token provided.' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: 'Invalid token.' });
|
||||
}
|
||||
};
|
||||
|
||||
export const requireRole = (roles: string[]) => {
|
||||
return (req: AuthRequest, res: Response, next: NextFunction): void => {
|
||||
if (!req.user || !roles.includes(req.user.role)) {
|
||||
res.status(403).json({ error: 'Access denied. Insufficient permissions.' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
35
backend/src/models/Shift.ts
Normal file
35
backend/src/models/Shift.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// backend/src/models/Shift.ts
|
||||
export interface ShiftTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
shifts: TemplateShift[];
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
export interface TemplateShift {
|
||||
dayOfWeek: number; // 0-6
|
||||
name: string;
|
||||
startTime: string; // "08:00"
|
||||
endTime: string; // "12:00"
|
||||
requiredEmployees: number;
|
||||
}
|
||||
|
||||
export interface ShiftPlan {
|
||||
id: string;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
templateId?: string;
|
||||
shifts: AssignedShift[];
|
||||
status: 'draft' | 'published';
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
export interface AssignedShift {
|
||||
id: string;
|
||||
date: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
requiredEmployees: number;
|
||||
assignedEmployees: string[];
|
||||
}
|
||||
35
backend/src/models/ShiftTemplate.ts
Normal file
35
backend/src/models/ShiftTemplate.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// backend/src/models/ShiftTemplate.ts
|
||||
export interface ShiftTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
isDefault: boolean;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
shifts: TemplateShift[];
|
||||
}
|
||||
|
||||
export interface TemplateShift {
|
||||
id: string;
|
||||
templateId: string;
|
||||
dayOfWeek: number;
|
||||
name: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
requiredEmployees: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface CreateShiftTemplateRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
isDefault: boolean;
|
||||
shifts: Omit<TemplateShift, 'id' | 'templateId'>[];
|
||||
}
|
||||
|
||||
export interface UpdateShiftTemplateRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
isDefault?: boolean;
|
||||
shifts?: Omit<TemplateShift, 'id' | 'templateId'>[];
|
||||
}
|
||||
15
backend/src/models/User.ts
Normal file
15
backend/src/models/User.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// backend/src/models/User.ts
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
password: string; // gehashed
|
||||
name: string;
|
||||
role: 'admin' | 'instandhalter' | 'user';
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface UserSession {
|
||||
userId: string;
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}
|
||||
13
backend/src/routes/auth.ts
Normal file
13
backend/src/routes/auth.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// backend/src/routes/auth.ts
|
||||
import express from 'express';
|
||||
import { login, register, logout, getCurrentUser } from '../controllers/authController';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/login', login);
|
||||
router.post('/register', register);
|
||||
router.post('/logout', authMiddleware, logout);
|
||||
router.get('/me', authMiddleware, getCurrentUser);
|
||||
|
||||
export default router;
|
||||
21
backend/src/routes/shiftPlans.ts
Normal file
21
backend/src/routes/shiftPlans.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// backend/src/routes/shiftPlans.ts
|
||||
import express from 'express';
|
||||
import { authMiddleware, requireRole } from '../middleware/auth';
|
||||
import {
|
||||
getShiftPlans,
|
||||
getShiftPlan,
|
||||
createShiftPlan,
|
||||
updateShiftPlan,
|
||||
deleteShiftPlan
|
||||
} from '../controllers/shiftPlanController';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.get('/', getShiftPlans);
|
||||
router.get('/:id', getShiftPlan);
|
||||
router.post('/', requireRole(['admin', 'instandhalter']), createShiftPlan);
|
||||
router.put('/:id', updateShiftPlan);
|
||||
router.delete('/:id', deleteShiftPlan);
|
||||
|
||||
export default router;
|
||||
21
backend/src/routes/shiftTemplates.ts
Normal file
21
backend/src/routes/shiftTemplates.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// backend/src/routes/shiftTemplates.ts
|
||||
import express from 'express';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import {
|
||||
getTemplates,
|
||||
getTemplate,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate
|
||||
} from '../controllers/shiftTemplateController';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.get('/', getTemplates);
|
||||
router.get('/:id', getTemplate);
|
||||
router.post('/', createTemplate);
|
||||
router.put('/:id', updateTemplate);
|
||||
router.delete('/:id', deleteTemplate);
|
||||
|
||||
export default router;
|
||||
52
backend/src/scripts/seedData.ts
Normal file
52
backend/src/scripts/seedData.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// backend/src/scripts/seedData.ts
|
||||
import { db } from '../services/databaseService';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export const seedData = async () => {
|
||||
try {
|
||||
// Admin User erstellen
|
||||
const adminId = uuidv4();
|
||||
const hashedPassword = await bcrypt.hash('admin123', 10);
|
||||
|
||||
await db.run(
|
||||
`INSERT OR IGNORE INTO users (id, email, password, name, role) VALUES (?, ?, ?, ?, ?)`,
|
||||
[adminId, 'admin@schichtplan.de', hashedPassword, 'System Administrator', 'admin']
|
||||
);
|
||||
|
||||
// Standard Vorlage erstellen
|
||||
const templateId = uuidv4();
|
||||
|
||||
await db.run(
|
||||
`INSERT OR IGNORE INTO shift_templates (id, name, description, is_default, created_by) VALUES (?, ?, ?, ?, ?)`,
|
||||
[templateId, 'Standard Woche', 'Standard Schichtplan für Montag bis Freitag', 1, adminId]
|
||||
);
|
||||
|
||||
// Standard Schichten
|
||||
const shifts = [
|
||||
{ day: 1, name: 'Vormittag', start: '08:00', end: '12:00', employees: 2 },
|
||||
{ day: 1, name: 'Nachmittag', start: '11:30', end: '15:30', employees: 2 },
|
||||
{ day: 2, name: 'Vormittag', start: '08:00', end: '12:00', employees: 2 },
|
||||
{ day: 2, name: 'Nachmittag', start: '11:30', end: '15:30', employees: 2 },
|
||||
{ day: 3, name: 'Vormittag', start: '08:00', end: '12:00', employees: 2 },
|
||||
{ day: 3, name: 'Nachmittag', start: '11:30', end: '15:30', employees: 2 },
|
||||
{ day: 4, name: 'Vormittag', start: '08:00', end: '12:00', employees: 2 },
|
||||
{ day: 4, name: 'Nachmittag', start: '11:30', end: '15:30', employees: 2 },
|
||||
{ day: 5, name: 'Vormittag', start: '08:00', end: '12:00', employees: 2 }
|
||||
];
|
||||
|
||||
for (const shift of shifts) {
|
||||
await db.run(
|
||||
`INSERT OR IGNORE INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[uuidv4(), templateId, shift.day, shift.name, shift.start, shift.end, shift.employees]
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Test data seeded successfully');
|
||||
} catch (error) {
|
||||
console.error('Error seeding test data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Beim Start ausführen
|
||||
seedData();
|
||||
44
backend/src/server.ts
Normal file
44
backend/src/server.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// backend/src/server.ts
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { db } from './services/databaseService.js';
|
||||
import { seedData } from './scripts/seedData.js';
|
||||
import authRoutes from './routes/auth.js';
|
||||
import shiftTemplateRoutes from './routes/shiftTemplates.js';
|
||||
import shiftPlanRoutes from './routes/shiftPlans.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/shift-templates', shiftTemplateRoutes);
|
||||
app.use('/api/shift-plans', shiftPlanRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
console.error('Unhandled error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, async () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
await seedData();
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('Shutting down gracefully...');
|
||||
await db.close();
|
||||
process.exit(0);
|
||||
});
|
||||
129
backend/src/services/databaseService.ts
Normal file
129
backend/src/services/databaseService.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
// backend/src/services/databaseService.ts
|
||||
import sqlite3 from 'sqlite3';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// __dirname für ES Modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const dbPath = path.join(__dirname, '../../database/schichtplan.db');
|
||||
|
||||
export class DatabaseService {
|
||||
private db: sqlite3.Database;
|
||||
|
||||
constructor() {
|
||||
this.db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('Database connection error:', err);
|
||||
} else {
|
||||
console.log('Connected to SQLite database');
|
||||
this.initializeDatabase();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private initializeDatabase() {
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
role TEXT CHECK(role IN ('admin', 'instandhalter', 'user')) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shift_templates (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
created_by TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS template_shifts (
|
||||
id TEXT PRIMARY KEY,
|
||||
template_id TEXT NOT NULL,
|
||||
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6),
|
||||
name TEXT NOT NULL,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT NOT NULL,
|
||||
required_employees INTEGER DEFAULT 1,
|
||||
color TEXT DEFAULT '#3498db',
|
||||
FOREIGN KEY (template_id) REFERENCES shift_templates(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shift_plans (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
start_date TEXT NOT NULL,
|
||||
end_date TEXT NOT NULL,
|
||||
template_id TEXT,
|
||||
status TEXT CHECK(status IN ('draft', 'published')) DEFAULT 'draft',
|
||||
created_by TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||
FOREIGN KEY (template_id) REFERENCES shift_templates(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assigned_shifts (
|
||||
id TEXT PRIMARY KEY,
|
||||
shift_plan_id TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT NOT NULL,
|
||||
required_employees INTEGER DEFAULT 1,
|
||||
assigned_employees TEXT DEFAULT '[]',
|
||||
FOREIGN KEY (shift_plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE
|
||||
);
|
||||
`;
|
||||
|
||||
this.db.exec(schema, (err) => {
|
||||
if (err) {
|
||||
console.error('Database initialization error:', err);
|
||||
} else {
|
||||
console.log('Database initialized successfully');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(sql: string, params: any[] = []): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(sql, params, function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get<T>(sql: string, params: any[] = []): Promise<T | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get(sql, params, (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row as T);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
all<T>(sql: string, params: any[] = []): Promise<T[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all(sql, params, (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows as T[]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.close((err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new DatabaseService();
|
||||
20
backend/tsconfig.json
Normal file
20
backend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
// backend/tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user