feat(booking-templates): per-company opt-in hiding of system templates (#2004)
* feat(booking-templates): per-company opt-in hiding of system templates Users cannot delete or hide the 26 standard konteringspaket, which clutter the settings panel and every template picker. Deletion stays off the table (shared global rows); instead a company can now hide individual system templates for itself only. - New booking_template_hidden table (insert=hide, delete=unhide), RLS gated on active company + write role; nothing hidden by default - POST/DELETE /api/settings/booking-templates/[id]/hide (system templates only; company/team templates keep their real delete path) - List route decorates rows with per-company is_hidden; pickers filter them out; the settings panel shows hidden ones in a collapsed restore section so hiding is never silent - Classified in full-archive-export exclusions (UI preference, not rakenskapsinformation) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PU1KN431c9gp5zKvFaa1NL * fix(booking-templates): idempotent re-hide, system-only RLS insert, hidden filter in bulk-book Skeptic + CodeRabbit findings on #2004, one pass: - hide upsert now passes ignoreDuplicates (DO NOTHING): the table has no UPDATE policy on purpose, so the DO UPDATE conflict arm turned a concurrent re-hide into an RLS 42501/500; pg test pins the conflict shape - bth_insert policy additionally requires the referenced template to be an active system template (migration is unmerged, edited in place); negative pg test for company templates - BulkBookDialog excludes templates hidden by the company (was reading the table directly and ignoring hides) - panel shows the failure toast when the hide/unhide fetch itself rejects - picker category chips built from the hidden-filtered list Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PU1KN431c9gp5zKvFaa1NL --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5720632832
commit
57d4359d1a
@@ -153,21 +153,34 @@ export default function BulkBookDialog({
|
||||
)
|
||||
|
||||
// Load templates when the dialog opens. RLS scopes to user's companies +
|
||||
// system templates; no company_id filter needed.
|
||||
// system templates; no company_id filter needed. Templates the company hid
|
||||
// (booking_template_hidden, per-company opt-in) are excluded like in the
|
||||
// other pickers; if that lookup fails we show everything (safe direction).
|
||||
useEffect(() => {
|
||||
if (!open || !company) return
|
||||
const companyId = company.id
|
||||
let cancelled = false
|
||||
async function load() {
|
||||
setLoadingTemplates(true)
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('booking_template_library')
|
||||
.select('*')
|
||||
.eq('is_active', true)
|
||||
.order('is_system', { ascending: false })
|
||||
.order('name', { ascending: true })
|
||||
const [templatesRes, hiddenRes] = await Promise.all([
|
||||
supabase
|
||||
.from('booking_template_library')
|
||||
.select('*')
|
||||
.eq('is_active', true)
|
||||
.order('is_system', { ascending: false })
|
||||
.order('name', { ascending: true }),
|
||||
supabase
|
||||
.from('booking_template_hidden')
|
||||
.select('template_id')
|
||||
.eq('company_id', companyId),
|
||||
])
|
||||
if (cancelled) return
|
||||
setTemplates((data ?? []) as BookingTemplateLibrary[])
|
||||
const hiddenIds = new Set(
|
||||
(hiddenRes.error ? [] : hiddenRes.data ?? []).map((r) => r.template_id as string),
|
||||
)
|
||||
const rows = (templatesRes.data ?? []) as BookingTemplateLibrary[]
|
||||
setTemplates(rows.filter((tpl) => !hiddenIds.has(tpl.id)))
|
||||
} finally {
|
||||
if (!cancelled) setLoadingTemplates(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user