Files
AgendaPro/server/routes/employees.ts
T
AgendaPro Dev d796538fd9 feat: rebrand AgendaPro -> AgendaMax + calendar fixes + current-week demo seed
- Rebrand all user-facing text to AgendaMax
- Fix sidebar scrolling: card container no longer overflows html (flex h-full min-h-0)
- Fix calendar toolbar hover: active buttons keep readable contrast
- Sticky calendar toolbar (month/week/day always visible on scroll)
- Seed guarantees 2-4 appointments per day for current week (Mon-Sat)
2026-07-27 10:11:16 -06:00

153 lines
6.6 KiB
TypeScript

import { Router } from "express";
import { db } from "../db.ts";
import type { AuthedRequest } from "../lib/auth.ts";
import { err, ownerOnly } from "../lib/auth.ts";
import type { EmployeeStats } from "../../shared/types.ts";
export const employeesRouter = Router();
// commission_pct is sensitive (drives payouts) — only owner/admin may read it.
const EMP_PUBLIC_COLS =
"id, business_id, name, email, phone, color, role, active, rating, hire_date, created_at, specialties, working_hours, efficiency_score";
function empCols(req: AuthedRequest): string {
return req.user!.role === "owner" || req.user!.role === "admin" ? `${EMP_PUBLIC_COLS}, commission_pct` : EMP_PUBLIC_COLS;
}
function attachServices(emp: any) {
const ids = db
.prepare(`SELECT service_id FROM employee_services WHERE employee_id = ?`)
.all(emp.id)
.map((r: any) => r.service_id);
emp.service_ids = ids;
return emp;
}
function computeStats(employeeId: number, businessId: number): EmployeeStats {
const completed = db
.prepare(
`SELECT COUNT(*) c, COALESCE(SUM(price),0) s FROM appointments WHERE employee_id = ? AND status = 'completed'`
)
.get(employeeId) as { c: number; s: number };
const total = db
.prepare(`SELECT COUNT(*) c FROM appointments WHERE employee_id = ?`)
.get(employeeId) as { c: number };
const rating = db
.prepare(
`SELECT COALESCE((SELECT AVG(rating) FROM reviews WHERE employee_id = ?), (SELECT rating FROM employees WHERE id = ?)) r`
)
.get(employeeId, employeeId) as { r: number };
const topEmp = db
.prepare(
`SELECT COUNT(*) c FROM appointments WHERE employee_id = ? AND status IN ('completed','scheduled')`
)
.get(employeeId) as { c: number };
const totalBiz = db
.prepare(
`SELECT COUNT(*) c FROM appointments WHERE business_id = ? AND status IN ('completed','scheduled')`
)
.get(businessId) as { c: number };
const utilization = totalBiz.c > 0 ? Math.round((topEmp.c / totalBiz.c) * 100) : 0;
return {
appointments_total: total.c,
appointments_completed: completed.c,
revenue_total: completed.s,
avg_rating: Math.round((rating.r ?? 5) * 10) / 10,
share_pct: utilization,
};
}
employeesRouter.get("/", (req: AuthedRequest, res) => {
const rows = db
.prepare(`SELECT ${empCols(req)} FROM employees WHERE business_id = ? ORDER BY active DESC, name`)
.all(req.user!.business_id) as any[];
const out = rows.map((r) => {
attachServices(r);
r.stats = computeStats(r.id, req.user!.business_id as number);
return r;
});
res.json({ employees: out });
});
employeesRouter.get("/:id", (req: AuthedRequest, res) => {
const emp = db.prepare(`SELECT ${empCols(req)} FROM employees WHERE id = ? AND business_id = ?`).get(
Number(req.params.id),
req.user!.business_id
) as any;
if (!emp) return err(res, 404, "Empleado no encontrado");
attachServices(emp);
emp.stats = computeStats(emp.id, req.user!.business_id as number);
res.json({ employee: emp });
});
employeesRouter.post("/", ownerOnly, (req: AuthedRequest, res) => {
const { name, role, email, phone, color, service_ids, specialties, working_hours, efficiency_score } = req.body ?? {};
if (!name) return err(res, 400, "El nombre es obligatorio");
const r = db
.prepare(
`INSERT INTO employees (business_id, name, role, email, phone, color, specialties, working_hours, efficiency_score)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`
)
.get(
req.user!.business_id,
name,
role || "Especialista",
email || null,
phone || null,
color || "#3b66ff",
JSON.stringify(Array.isArray(specialties) ? specialties : []),
working_hours ? (typeof working_hours === "string" ? working_hours : JSON.stringify(working_hours)) : null,
typeof efficiency_score === "number" ? efficiency_score : 50
) as any;
if (Array.isArray(service_ids)) {
const ins = db.prepare(`INSERT OR IGNORE INTO employee_services (employee_id, service_id) VALUES (?, ?)`);
for (const sid of service_ids) ins.run(r.id, Number(sid));
}
attachServices(r);
r.stats = computeStats(r.id, req.user!.business_id as number);
res.status(201).json({ employee: r });
});
employeesRouter.patch("/:id", ownerOnly, (req: AuthedRequest, res) => {
const id = Number(req.params.id);
const existing = db.prepare(`SELECT * FROM employees WHERE id = ? AND business_id = ?`).get(id, req.user!.business_id as number) as any;
if (!existing) return err(res, 404, "Empleado no encontrado");
const { name, role, email, phone, color, active, service_ids, specialties, working_hours, efficiency_score } = req.body ?? {};
const merged = {
name: name ?? existing.name,
role: role ?? existing.role,
email: email ?? existing.email,
phone: phone ?? existing.phone,
color: color ?? existing.color,
active: active === undefined ? existing.active : active ? 1 : 0,
specialties: specialties !== undefined
? (typeof specialties === "string" ? specialties : JSON.stringify(Array.isArray(specialties) ? specialties : []))
: existing.specialties,
working_hours: working_hours !== undefined
? (working_hours === null ? null : typeof working_hours === "string" ? working_hours : JSON.stringify(working_hours))
: existing.working_hours,
efficiency_score: efficiency_score !== undefined ? Number(efficiency_score) : (existing.efficiency_score ?? 50),
};
db.prepare(
`UPDATE employees SET name = ?, role = ?, email = ?, phone = ?, color = ?, active = ?, specialties = ?, working_hours = ?, efficiency_score = ? WHERE id = ?`
).run(merged.name, merged.role, merged.email, merged.phone, merged.color, merged.active, merged.specialties, merged.working_hours, merged.efficiency_score, id);
if (Array.isArray(service_ids)) {
db.prepare(`DELETE FROM employee_services WHERE employee_id = ?`).run(id);
const ins = db.prepare(`INSERT OR IGNORE INTO employee_services (employee_id, service_id) VALUES (?, ?)`);
for (const sid of service_ids) ins.run(id, Number(sid));
}
const updated = db.prepare(`SELECT * FROM employees WHERE id = ?`).get(id) as any;
attachServices(updated);
updated.stats = computeStats(updated.id, req.user!.business_id as number);
res.json({ employee: updated });
});
employeesRouter.delete("/:id", ownerOnly, (req: AuthedRequest, res) => {
const id = Number(req.params.id);
const existing = db
.prepare(`SELECT id FROM employees WHERE id = ? AND business_id = ?`)
.get(id, req.user!.business_id as number);
if (!existing) return err(res, 404, "Empleado no encontrado");
db.prepare(`DELETE FROM employees WHERE id = ?`).run(id);
res.json({ ok: true });
});