38955-vm/server.ts
2026-03-03 20:42:30 +00:00

404 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from 'express';
import { createServer as createViteServer } from 'vite';
import cors from 'cors';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import db, { initDb } from './server/db.ts';
import { GoogleGenAI, Type } from "@google/genai";
const JWT_SECRET = process.env.JWT_SECRET || 'mua-super-secret-key-2026';
let ai: GoogleGenAI | null = null;
function getAI() {
if (!ai) {
const key = process.env.GEMINI_API_KEY;
if (!key) throw new Error('GEMINI_API_KEY is required');
ai = new GoogleGenAI({ apiKey: key });
}
return ai;
}
async function startServer() {
const app = express();
const PORT = 3000;
app.use(cors());
app.use(express.json());
// Initialize DB
initDb();
// Authentication Middleware
const authenticate = (req: any, res: any, next: any) => {
const authHeader = req.headers.authorization;
if (!authHeader) return res.status(401).json({ error: 'Unauthorized' });
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
// Check if user still exists and has the same role/branch
const user = db.prepare('SELECT * FROM users WHERE id = ?').get((req.user as any).id) as any;
if (!user) return res.status(401).json({ error: 'User not found' });
req.user.role = user.role;
req.user.branch_id = user.branch_id;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
const requireAdmin = (req: any, res: any, next: any) => {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden: Admin only' });
next();
};
// --- API Routes ---
// Login
app.post('/api/auth/login', (req, res) => {
const { email, password } = req.body;
const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email) as any;
if (!user || !bcrypt.compareSync(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ id: user.id, role: user.role, branch_id: user.branch_id }, JWT_SECRET, { expiresIn: '12h' });
let branchName = null;
if (user.branch_id) {
const branch = db.prepare('SELECT name FROM branches WHERE id = ?').get(user.branch_id) as any;
branchName = branch ? branch.name : null;
}
res.json({ token, user: { id: user.id, email: user.email, role: user.role, branch_id: user.branch_id, branch_name: branchName } });
});
// Get current user info
app.get('/api/auth/me', authenticate, (req: any, res) => {
const user = db.prepare('SELECT id, email, role, branch_id FROM users WHERE id = ?').get(req.user.id) as any;
if (!user) return res.status(404).json({ error: 'User not found' });
let branchName = null;
if (user.branch_id) {
const branch = db.prepare('SELECT name FROM branches WHERE id = ?').get(user.branch_id) as any;
branchName = branch ? branch.name : null;
}
res.json({ user: { ...user, branch_name: branchName } });
});
// Dashboard Data
app.get('/api/dashboard', authenticate, (req: any, res) => {
let branchId;
if (req.user.role === 'manager') {
branchId = req.user.branch_id;
} else if (req.user.role === 'admin') {
branchId = req.query.branch_id;
if (!branchId) return res.status(400).json({ error: 'Branch ID required for admin' });
} else {
return res.status(403).json({ error: 'Forbidden' });
}
const { start, end } = req.query; // timestamps
if (!start || !end) return res.status(400).json({ error: 'Missing start or end' });
// Get all employees for this branch
const employees = db.prepare('SELECT * FROM employees WHERE branch_id = ?').all(branchId) as any[];
// Get shifts for the week
const shifts = db.prepare('SELECT * FROM shifts WHERE branch_id = ? AND start_time >= ? AND start_time <= ?').all(branchId, start, end) as any[];
// Calculate total hours per employee
const employeeHours: Record<number, number> = {};
employees.forEach(emp => employeeHours[emp.id] = 0);
shifts.forEach(shift => {
const hours = (shift.end_time - shift.start_time) / (1000 * 60 * 60);
if (employeeHours[shift.employee_id] !== undefined) {
employeeHours[shift.employee_id] += hours;
}
});
const chartData = employees.map(emp => ({
name: emp.name,
hours: employeeHours[emp.id] || 0,
status: emp.status
}));
const totalHours = Object.values(employeeHours).reduce((a, b) => a + b, 0);
res.json({
totalHours,
chartData
});
});
// Shifts
app.get('/api/shifts', authenticate, (req: any, res) => {
const branchId = req.user.role === 'manager' ? req.user.branch_id : req.query.branch_id;
if (!branchId) return res.status(400).json({ error: 'Branch ID required' });
const { start, end } = req.query;
if (!start || !end) return res.status(400).json({ error: 'Missing start or end' });
const shifts = db.prepare('SELECT * FROM shifts WHERE branch_id = ? AND start_time >= ? AND start_time <= ?').all(branchId, start, end);
res.json(shifts);
});
app.post('/api/shifts/generate', authenticate, async (req: any, res) => {
let branchId;
if (req.user.role === 'manager') {
branchId = req.user.branch_id;
} else if (req.user.role === 'admin') {
branchId = req.body.branch_id;
if (!branchId) return res.status(400).json({ error: 'Branch ID required for admin' });
} else {
return res.status(403).json({ error: 'Forbidden' });
}
const { start, end } = req.body;
if (!start || !end) return res.status(400).json({ error: 'Missing start or end' });
try {
// Get employees
const employees = db.prepare('SELECT id, name FROM employees WHERE branch_id = ? AND status = ?').all(branchId, 'active') as any[];
if (employees.length === 0) {
return res.status(400).json({ error: 'No active employees found for this branch.' });
}
const prompt = `
You are an expert shift scheduling AI for a gelato shop.
Generate a weekly shift schedule for the following employees:
${JSON.stringify(employees)}
The week starts at timestamp ${start} and ends at ${end}.
Rules:
1. Store hours are roughly 08:00 to 22:00.
2. Each shift must be between 4 and 11 hours long.
3. Each employee should work around 30-45 hours this week. Do NOT exceed 45 hours per employee.
4. Ensure there is at least 1 person in the store at all times during store hours.
5. Return the start_time and end_time as Unix timestamps (milliseconds).
6. Distribute the shifts fairly.
`;
const response = await ai.models.generateContent({
model: "gemini-3.1-pro-preview",
contents: prompt,
config: {
responseMimeType: "application/json",
responseSchema: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
employee_id: { type: Type.INTEGER },
start_time: { type: Type.NUMBER },
end_time: { type: Type.NUMBER }
},
required: ["employee_id", "start_time", "end_time"]
}
}
}
});
const generatedShifts = JSON.parse(response.text || '[]');
res.json(generatedShifts);
} catch (err: any) {
console.error('AI Generation Error:', err);
res.status(500).json({ error: 'Failed to generate shifts' });
}
});
app.post('/api/shifts/bulk', authenticate, (req: any, res) => {
let branchId;
if (req.user.role === 'manager') {
branchId = req.user.branch_id;
} else if (req.user.role === 'admin') {
branchId = req.body.branch_id;
if (!branchId) return res.status(400).json({ error: 'Branch ID required for admin' });
} else {
return res.status(403).json({ error: 'Forbidden' });
}
const { shifts } = req.body;
if (!Array.isArray(shifts)) return res.status(400).json({ error: 'Shifts must be an array' });
try {
const insert = db.prepare('INSERT INTO shifts (employee_id, branch_id, start_time, end_time) VALUES (?, ?, ?, ?)');
const insertMany = db.transaction((shiftsToInsert) => {
for (const shift of shiftsToInsert) {
insert.run(shift.employee_id, branchId, shift.start_time, shift.end_time);
}
});
insertMany(shifts);
res.json({ success: true, count: shifts.length });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/shifts', authenticate, (req: any, res) => {
let branchId;
if (req.user.role === 'manager') {
branchId = req.user.branch_id;
} else if (req.user.role === 'admin') {
branchId = req.body.branch_id;
if (!branchId) return res.status(400).json({ error: 'Branch ID required for admin' });
} else {
return res.status(403).json({ error: 'Forbidden' });
}
const { employee_id, start_time, end_time } = req.body;
if (!employee_id || !start_time || !end_time) return res.status(400).json({ error: 'Missing fields' });
// BR-01: Past edit restriction
const now = Date.now();
const startOfDay = new Date().setHours(0, 0, 0, 0);
if (start_time < startOfDay) {
return res.status(400).json({ error: 'Geçmiş günlere ait vardiya eklenemez veya değiştirilemez.' });
}
// AC 2.5: End time must be after start time
if (end_time <= start_time) {
return res.status(400).json({ error: 'Vardiya bitiş saati, başlangıç saatinden önce veya aynı olamaz.' });
}
// BR-02: Time constraint (4-11 hours)
const durationHours = (end_time - start_time) / (1000 * 60 * 60);
if (durationHours < 4 || durationHours > 11) {
return res.status(400).json({ error: 'Vardiya bloku minimum 4 saat, maksimum 11 saat olmalıdır.' });
}
// AC 2.6: Overlap check
const existingShift = db.prepare(`
SELECT 1 FROM shifts
WHERE employee_id = ?
AND ((start_time < ? AND end_time > ?) OR (start_time >= ? AND start_time < ?))
`).get(employee_id, end_time, start_time, start_time, end_time);
if (existingShift) {
return res.status(400).json({ error: 'Bu personel belirtilen saatlerde zaten planlanmış.' });
}
// Insert shift
const result = db.prepare('INSERT INTO shifts (employee_id, branch_id, start_time, end_time) VALUES (?, ?, ?, ?)').run(employee_id, branchId, start_time, end_time);
res.json({ id: result.lastInsertRowid, employee_id, branch_id: branchId, start_time, end_time });
});
// Employees
app.get('/api/employees', authenticate, (req: any, res) => {
const branchId = req.user.role === 'manager' ? req.user.branch_id : req.query.branch_id;
if (!branchId) return res.status(400).json({ error: 'Branch ID required' });
const employees = db.prepare('SELECT * FROM employees WHERE branch_id = ?').all(branchId);
res.json(employees);
});
// Admin Routes
app.get('/api/admin/summary', authenticate, requireAdmin, (req, res) => {
const { start, end } = req.query;
if (!start || !end) return res.status(400).json({ error: 'Missing start or end' });
try {
const branchesCount = db.prepare('SELECT COUNT(*) as count FROM branches').get() as { count: number };
const employeesCount = db.prepare('SELECT COUNT(*) as count FROM employees WHERE status = ?').get('active') as { count: number };
const shiftsCount = db.prepare('SELECT COUNT(*) as count FROM shifts WHERE start_time >= ? AND start_time <= ?').get(start, end) as { count: number };
res.json({
totalBranches: branchesCount.count,
totalActiveEmployees: employeesCount.count,
totalPlannedShifts: shiftsCount.count
});
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.get('/api/admin/branches', authenticate, requireAdmin, (req, res) => {
const branches = db.prepare('SELECT * FROM branches').all();
res.json(branches);
});
app.post('/api/admin/branches', authenticate, requireAdmin, (req, res) => {
const { name } = req.body;
try {
const result = db.prepare('INSERT INTO branches (name) VALUES (?)').run(name);
res.json({ id: result.lastInsertRowid, name });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
app.get('/api/admin/users', authenticate, requireAdmin, (req, res) => {
const users = db.prepare('SELECT id, email, role, branch_id FROM users').all();
res.json(users);
});
app.post('/api/admin/users', authenticate, requireAdmin, (req, res) => {
const { email, password, role, branch_id } = req.body;
try {
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(password, salt);
const result = db.prepare('INSERT INTO users (email, password, role, branch_id) VALUES (?, ?, ?, ?)').run(email, hash, role, branch_id || null);
res.json({ id: result.lastInsertRowid, email, role, branch_id });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
app.put('/api/admin/users/:id', authenticate, requireAdmin, (req, res) => {
const { role, branch_id } = req.body;
try {
db.prepare('UPDATE users SET role = ?, branch_id = ? WHERE id = ?').run(role, branch_id || null, req.params.id);
res.json({ success: true });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
app.post('/api/admin/employees', authenticate, requireAdmin, (req, res) => {
const { name, branch_id } = req.body;
try {
const result = db.prepare('INSERT INTO employees (name, branch_id) VALUES (?, ?)').run(name, branch_id);
res.json({ id: result.lastInsertRowid, name, branch_id, status: 'active' });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
app.put('/api/admin/employees/:id', authenticate, requireAdmin, (req, res) => {
const { status } = req.body;
try {
db.prepare('UPDATE employees SET status = ? WHERE id = ?').run(status, req.params.id);
res.json({ success: true });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
// Vite middleware for development
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "spa",
});
app.use(vite.middlewares);
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`Server running on http://localhost:${PORT}`);
});
}
startServer();