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
@@ -69,6 +69,7 @@ interface BookingForm {
|
||||
petId: string;
|
||||
serviceId: string;
|
||||
staffId: string;
|
||||
batherStaffId: string;
|
||||
date: string;
|
||||
startTime: string;
|
||||
notes: string;
|
||||
@@ -82,6 +83,7 @@ const EMPTY_FORM: BookingForm = {
|
||||
petId: "",
|
||||
serviceId: "",
|
||||
staffId: "",
|
||||
batherStaffId: "",
|
||||
date: formatDate(new Date()),
|
||||
startTime: "09:00",
|
||||
notes: "",
|
||||
@@ -208,6 +210,7 @@ export function AppointmentsPage() {
|
||||
petId: form.petId,
|
||||
serviceId: form.serviceId,
|
||||
staffId: form.staffId || undefined,
|
||||
batherStaffId: form.batherStaffId || undefined,
|
||||
startTime: startISO,
|
||||
endTime: endISO,
|
||||
notes: form.notes || undefined,
|
||||
@@ -496,6 +499,18 @@ export function AppointmentsPage() {
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Bather / Assistant (optional)">
|
||||
<select
|
||||
value={form.batherStaffId}
|
||||
onChange={(e) => setForm((f) => ({ ...f, batherStaffId: e.target.value }))}
|
||||
style={inputStyle}
|
||||
>
|
||||
<option value="">— none —</option>
|
||||
{staff.filter((s) => s.active).map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Date">
|
||||
<input
|
||||
type="date"
|
||||
@@ -638,6 +653,7 @@ function AppointmentDetail({
|
||||
const client = clients.find((c) => c.id === appt.clientId);
|
||||
const service = services.find((s) => s.id === appt.serviceId);
|
||||
const groomer = staff.find((s) => s.id === appt.staffId);
|
||||
const bather = staff.find((s) => s.id === appt.batherStaffId);
|
||||
const transitions = STATUS_TRANSITIONS[appt.status] ?? [];
|
||||
|
||||
function handleDeleteClick() {
|
||||
@@ -675,6 +691,7 @@ function AppointmentDetail({
|
||||
["Client", client?.name ?? "—"],
|
||||
["Service", service?.name ?? "—"],
|
||||
["Groomer", groomer?.name ?? "Unassigned"],
|
||||
...(bather ? [["Bather/Asst.", bather.name] as [string, string]] : []),
|
||||
["Start", new Date(appt.startTime).toLocaleString()],
|
||||
["End", new Date(appt.endTime).toLocaleString()],
|
||||
["Status", appt.status.replace("_", " ")],
|
||||
|
||||
Reference in New Issue
Block a user