AgendaPro v1.0 - multi-tenant SaaS, mobile calendar, public booking
Multi-tenant scheduling SaaS (AgendaPro-equivalent): - Backend: Node + Express + node:sqlite, multi-tenant (admin/owner/employee), versioned migrations, 4 templates (estetica-spa, barberia, clinica, blank). - Frontend: React 18 + TS + Vite + Tailwind, FullCalendar drag-and-drop, Recharts dashboard, mobile-first (day/list views on mobile). - Features: calendar+services+employees+clients+tickets, dashboard with best employee/service, top tickets, top/frequent clients, commissions, cancellation policy + no-show tracking, public online booking (/b/:slug), cash register (cierre de caja), reminders/notifications center. - Verification: 65/65 e2e API, 58/58 visual (0 overflow, 0 console errors), typecheck clean, vite build OK.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { Router } from "express";
|
||||
import { db } from "../db.ts";
|
||||
import type { AuthedRequest } from "../lib/auth.ts";
|
||||
import { err, ownerOnly } from "../lib/auth.ts";
|
||||
|
||||
export const settingsRouter = Router();
|
||||
|
||||
const FIELDS = [
|
||||
"name",
|
||||
"industry",
|
||||
"phone",
|
||||
"address",
|
||||
"slug",
|
||||
"booking_enabled",
|
||||
"cancel_window_hours",
|
||||
"cancel_penalty_pct",
|
||||
"require_deposit",
|
||||
"deposit_pct",
|
||||
"timezone",
|
||||
] as const;
|
||||
|
||||
settingsRouter.get("/", (req: AuthedRequest, res) => {
|
||||
const b = db
|
||||
.prepare(
|
||||
`SELECT id, name, industry, currency, currency_symbol, phone, address, slug, booking_enabled,
|
||||
cancel_window_hours, cancel_penalty_pct, require_deposit, deposit_pct, timezone
|
||||
FROM businesses WHERE id = ?`
|
||||
)
|
||||
.get(req.user!.business_id) as any;
|
||||
if (!b) return err(res, 404, "Negocio no encontrado");
|
||||
res.json({ settings: b });
|
||||
});
|
||||
|
||||
settingsRouter.patch("/", ownerOnly, (req: AuthedRequest, res) => {
|
||||
const existing = db.prepare(`SELECT * FROM businesses WHERE id = ?`).get(req.user!.business_id) as any;
|
||||
if (!existing) return err(res, 404, "Negocio no encontrado");
|
||||
const body = req.body ?? {};
|
||||
const merged: any = {};
|
||||
for (const f of FIELDS) {
|
||||
if (body[f] !== undefined) merged[f] = body[f];
|
||||
}
|
||||
// slug uniqueness check
|
||||
if (merged.slug !== undefined) {
|
||||
merged.slug = String(merged.slug).trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||||
if (!merged.slug) return err(res, 400, "El slug no puede quedar vacío");
|
||||
const clash = db.prepare(`SELECT id FROM businesses WHERE slug = ? AND id != ?`).get(merged.slug, req.user!.business_id);
|
||||
if (clash) return err(res, 409, "Esa URL ya está en uso");
|
||||
}
|
||||
const sets = Object.keys(merged).map((k) => `${k} = @${k}`);
|
||||
if (sets.length === 0) return res.json({ settings: existing });
|
||||
merged.id = req.user!.business_id;
|
||||
db.prepare(`UPDATE businesses SET ${sets.join(", ")} WHERE id = @id`).run(merged);
|
||||
const updated = db
|
||||
.prepare(`SELECT id, name, industry, currency, currency_symbol, phone, address, slug, booking_enabled, cancel_window_hours, cancel_penalty_pct, require_deposit, deposit_pct, timezone FROM businesses WHERE id = ?`)
|
||||
.get(req.user!.business_id);
|
||||
res.json({ settings: updated });
|
||||
});
|
||||
Reference in New Issue
Block a user