feat(GRO-1173): apply buffer rules UI changes to extracted groombook/web repo
CI / Test (pull_request) Successful in 15s
CI / Lint & Typecheck (pull_request) Failing after 18s
CI / Build & Push Docker Image (pull_request) Has been skipped

This commit ports the GRO-1173 admin UI changes from the app monorepo
into the extracted groombook/web repo, using the correct source paths
(src/ instead of apps/web/src/):

- New BufferRulesSection component (full CRUD UI for /api/buffer-rules)
- Default Buffer (minutes) field added to service create/edit form
- Size Category and Coat Type dropdowns added to PetForm (portal)
- @groombook/types Service interface extended with defaultBufferMinutes
- BufferRulesSection embedded in Settings page

The PetForm already had coatType — this commit adds petSizeCategory
and renders both fields with proper dropdown selectors.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Flea Flicker
2026-05-21 11:12:45 +00:00
parent f414d2589f
commit ec17f1e885
5 changed files with 321 additions and 1 deletions
+19 -1
View File
@@ -6,6 +6,7 @@ interface ServiceForm {
description: string;
priceStr: string;
durationMinutes: number;
defaultBufferMinutes: number;
active: boolean;
}
@@ -14,6 +15,7 @@ const EMPTY_FORM: ServiceForm = {
description: "",
priceStr: "",
durationMinutes: 60,
defaultBufferMinutes: 0,
active: true,
};
@@ -55,6 +57,7 @@ export function ServicesPage() {
description: s.description ?? "",
priceStr: (s.basePriceCents / 100).toFixed(2),
durationMinutes: s.durationMinutes,
defaultBufferMinutes: s.defaultBufferMinutes ?? 0,
active: s.active,
});
setFormError(null);
@@ -76,6 +79,7 @@ export function ServicesPage() {
description: form.description || undefined,
basePriceCents: Math.round(price * 100),
durationMinutes: form.durationMinutes,
defaultBufferMinutes: form.defaultBufferMinutes,
active: form.active,
};
const res = editing
@@ -138,7 +142,7 @@ export function ServicesPage() {
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 14 }}>
<thead>
<tr style={{ background: "#f8fafc" }}>
{["Name", "Description", "Price", "Duration", "Status", ""].map((h) => (
{["Name", "Description", "Price", "Duration", "Default Buffer", "Status", ""].map((h) => (
<th key={h} style={{ textAlign: "left", padding: "0.55rem 0.75rem", borderBottom: "1px solid #e5e7eb", fontSize: 11, fontWeight: 600, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.04em" }}>
{h}
</th>
@@ -152,6 +156,7 @@ export function ServicesPage() {
<td style={tdStyle}>{s.description ?? "—"}</td>
<td style={tdStyle}>${(s.basePriceCents / 100).toFixed(2)}</td>
<td style={tdStyle}>{s.durationMinutes} min</td>
<td style={tdStyle}>{(s as Service & { defaultBufferMinutes?: number }).defaultBufferMinutes ?? 0} min</td>
<td style={tdStyle}>
<button
onClick={() => toggleActive(s)}
@@ -240,6 +245,19 @@ export function ServicesPage() {
style={inputStyle}
/>
</Field>
<Field label="Default Buffer (minutes)">
<input
type="number"
min="0"
step="1"
value={form.defaultBufferMinutes}
onChange={(e) => setForm((f) => ({ ...f, defaultBufferMinutes: parseInt(e.target.value) || 0 }))}
style={inputStyle}
/>
<p style={{ fontSize: 12, color: "#9ca3af", marginTop: "0.2rem" }}>
Default buffer time applied when no specific rule matches
</p>
</Field>
<Field label="Status">
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem", cursor: "pointer" }}>
<input
+5
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from "react";
import { useBranding } from "../BrandingContext.js";
import { BufferRulesSection } from "../components/BufferRules.js";
interface AuthProviderConfig {
id: number;
@@ -533,6 +534,10 @@ issuerUrl: authForm.issuerUrl,
{saving ? "Saving..." : "Save Changes"}
</button>
{/* Buffer Rules Section */}
<hr style={{ margin: "2rem 0", border: "none", borderTop: "1px solid #e5e7eb" }} />
<BufferRulesSection />
{/* Auth Provider Section — super users only */}
{currentUser?.isSuperUser && (
<>