feat: tip and payment splitting between staff roles (#34)
* feat: multi-groomer calendar view with per-groomer filtering Add groomer view mode to the appointments calendar: - Toggle between "Status" (existing) and "Groomer" color coding - Per-groomer visibility toggles with color-coded buttons - Appointments colored by assigned groomer in groomer view - Groomer name shown on appointment blocks in groomer view - Unassigned appointments shown in neutral gray Satisfies groombook/groombook#11 requirements for side-by-side/unified groomer schedule visibility and per-groomer filter/toggle. Co-Authored-By: Paperclip <noreply@paperclip.ing> * feat: tip and payment splitting between staff roles Implements groombook/groombook#12 — track which staff worked on each pet and calculate tip distribution based on who was involved. Changes: - DB: Add bather_staff_id to appointments (optional secondary staff) - DB: Add invoice_tip_splits table (per-staff tip share ledger) - API: appointments POST/PATCH accept batherStaffId - API: GET /invoices/:id now includes tipSplits[] - API: POST /invoices/:id/tip-splits — saves tip distribution - API: GET /reports/tip-splits — payroll summary of tip earnings - Frontend: Bather/Assistant select on New Appointment form - Frontend: Tip Distribution section in Invoice Detail modal - Auto-populates 70%/30% split when bather is assigned - Editable percentages before payment; saved on Mark as Paid - Displays recorded splits on paid invoices Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix: remove unused staff import from invoices route Co-Authored-By: Paperclip <noreply@paperclip.ing> --------- Co-authored-by: Groom Book CTO <cto@groombook.app> Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit was merged in pull request #34.
This commit is contained in:
committed by
GitHub
parent
1b3a23bd52
commit
4ab5597fd5
@@ -9,6 +9,7 @@ import {
|
||||
appointments,
|
||||
clients,
|
||||
invoices,
|
||||
invoiceTipSplits,
|
||||
services,
|
||||
staff,
|
||||
} from "@groombook/db";
|
||||
@@ -303,6 +304,37 @@ reportsRouter.get("/clients", async (c) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tip splits payroll report ────────────────────────────────────────────────
|
||||
// GET /api/reports/tip-splits?from=&to=
|
||||
// Aggregates tip earnings per staff member for the period
|
||||
|
||||
reportsRouter.get("/tip-splits", async (c) => {
|
||||
const db = getDb();
|
||||
const from = parseDate(c.req.query("from"), defaultFrom());
|
||||
const to = parseDate(c.req.query("to"), defaultTo());
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
staffId: invoiceTipSplits.staffId,
|
||||
staffName: invoiceTipSplits.staffName,
|
||||
totalTipCents: sql<number>`SUM(${invoiceTipSplits.shareCents})::int`,
|
||||
invoiceCount: sql<number>`COUNT(DISTINCT ${invoiceTipSplits.invoiceId})::int`,
|
||||
})
|
||||
.from(invoiceTipSplits)
|
||||
.innerJoin(invoices, eq(invoiceTipSplits.invoiceId, invoices.id))
|
||||
.where(
|
||||
and(
|
||||
eq(invoices.status, "paid"),
|
||||
gte(invoices.paidAt, from),
|
||||
lt(invoices.paidAt, to)
|
||||
)
|
||||
)
|
||||
.groupBy(invoiceTipSplits.staffId, invoiceTipSplits.staffName)
|
||||
.orderBy(sql`SUM(${invoiceTipSplits.shareCents}) DESC`);
|
||||
|
||||
return c.json({ from: from.toISOString(), to: to.toISOString(), rows });
|
||||
});
|
||||
|
||||
// ─── CSV export ───────────────────────────────────────────────────────────────
|
||||
// GET /api/reports/export.csv?type=revenue|appointments|services&from=&to=
|
||||
|
||||
|
||||
Reference in New Issue
Block a user