diff --git a/app/[locale]/(app)/checkups/[id]/page.tsx b/app/[locale]/(app)/checkups/[id]/page.tsx
index 39b1d80..37ae17a 100644
--- a/app/[locale]/(app)/checkups/[id]/page.tsx
+++ b/app/[locale]/(app)/checkups/[id]/page.tsx
@@ -4,6 +4,7 @@ import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { Link } from "@/i18n/navigation";
import { createSupabaseServerClient } from "@/lib/supabase/server";
+import { fieldsJsonToLabels } from "@/lib/templates/template-schema";
import { Button } from "@/components/ui/button";
import { CheckupForm } from "../checkup-form";
import { DeleteCheckupButton } from "../delete-checkup-button";
@@ -26,7 +27,7 @@ export default async function CheckupPage({
const { data: c } = await supabase
.from("checkups")
.select(
- "id, customer_id, queue_number, status, heart_beat, blood_pressure, temperature, weight, height, symptoms, diagnosis, conclusion, notes, recheck_date",
+ "id, customer_id, queue_number, status, heart_beat, blood_pressure, temperature, weight, height, symptoms, diagnosis, conclusion, notes, recheck_date, template_id, template_values",
)
.eq("id", checkupId)
.eq("deleted", false)
@@ -34,14 +35,35 @@ export default async function CheckupPage({
if (!c) notFound();
- const { data: customer } = await supabase
- .from("customers")
- .select("last_name, first_name, dob, gender, phone")
- .eq("id", c.customer_id)
- .maybeSingle();
+ const [{ data: customer }, { data: templates }] = await Promise.all([
+ supabase.from("customers").select("last_name, first_name, dob, gender, phone").eq("id", c.customer_id).maybeSingle(),
+ supabase
+ .from("checkup_templates")
+ .select("id, name, gender, fields")
+ .eq("deleted", false)
+ .order("name", { ascending: true }),
+ ]);
const patientName = customer ? `${customer.last_name} ${customer.first_name}` : "—";
+ // Prefer templates matching the patient's gender (or applicable to "any");
+ // keep the full list when the patient has no gender on file.
+ const applicableTemplates = customer?.gender
+ ? (templates ?? []).filter((tpl) => tpl.gender === "any" || tpl.gender === customer.gender)
+ : (templates ?? []);
+
+ const templateValues = Array.isArray(c.template_values)
+ ? (c.template_values as unknown[])
+ .filter((v): v is { label: unknown; value: unknown } => !!v && typeof v === "object")
+ .map((v) => ({ label: String(v.label ?? ""), value: String(v.value ?? "") }))
+ : [];
+
+ const templateOptions = applicableTemplates.map((tpl) => ({
+ id: tpl.id,
+ name: tpl.name,
+ labels: fieldsJsonToLabels(tpl.fields),
+ }));
+
return (
@@ -88,6 +110,9 @@ export default async function CheckupPage({
recheckDate: str(c.recheck_date),
status: c.status,
}}
+ templates={templateOptions}
+ initialTemplateId={c.template_id}
+ initialTemplateValues={templateValues}
/>
{t("title")}
diff --git a/app/[locale]/(app)/checkups/actions.ts b/app/[locale]/(app)/checkups/actions.ts
index c8ff5a3..fcd9159 100644
--- a/app/[locale]/(app)/checkups/actions.ts
+++ b/app/[locale]/(app)/checkups/actions.ts
@@ -13,7 +13,12 @@ import { redirect } from "@/i18n/navigation";
import { getServerSession } from "@/lib/auth/get-server-session";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import type { AppRole } from "@/lib/db/roles";
-import { CheckupSaveSchema, parseNum, type CheckupSaveState } from "@/lib/checkups/checkup-schema";
+import {
+ CheckupSaveSchema,
+ parseNum,
+ parseTemplateValues,
+ type CheckupSaveState,
+} from "@/lib/checkups/checkup-schema";
const CLINICAL: AppRole[] = ["admin", "receptionist", "doctor", "nurse"];
const isClinical = (r: AppRole | null | undefined) => !!r && CLINICAL.includes(r);
@@ -55,6 +60,11 @@ export async function saveCheckupAction(
}
const d = parsed.data;
+
+ const templateIdRaw = get("templateId").trim();
+ const templateId = templateIdRaw && Number.isFinite(Number(templateIdRaw)) ? Number(templateIdRaw) : null;
+ const templateValues = parseTemplateValues(get("templateValues"));
+
const supabase = await createSupabaseServerClient();
const { error } = await supabase
.from("checkups")
@@ -70,6 +80,8 @@ export async function saveCheckupAction(
notes: nullIfBlank(d.notes),
recheck_date: nullIfBlank(d.recheckDate),
status: d.status,
+ template_id: templateId,
+ template_values: templateValues,
})
.eq("id", id);
diff --git a/app/[locale]/(app)/checkups/checkup-form.tsx b/app/[locale]/(app)/checkups/checkup-form.tsx
index 68b20b5..d12e83c 100644
--- a/app/[locale]/(app)/checkups/checkup-form.tsx
+++ b/app/[locale]/(app)/checkups/checkup-form.tsx
@@ -6,7 +6,7 @@
* (redirects to the queue on success).
*/
-import { useActionState } from "react";
+import { useActionState, useState } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
@@ -33,13 +33,42 @@ export type CheckupDefaults = {
status: string;
};
-export function CheckupForm({ defaults }: { defaults: CheckupDefaults }) {
+export type CheckupTemplateOption = { id: number; name: string; labels: string[] };
+type TemplateValue = { label: string; value: string };
+
+export function CheckupForm({
+ defaults,
+ templates,
+ initialTemplateId,
+ initialTemplateValues,
+}: {
+ defaults: CheckupDefaults;
+ templates: CheckupTemplateOption[];
+ initialTemplateId: number | null;
+ initialTemplateValues: TemplateValue[];
+}) {
const t = useTranslations("checkups");
const [state, dispatch, isPending] = useActionState
(saveCheckupAction, {
status: "idle",
});
const formError = state.status === "error" ? state.formError : null;
+ // Template picker — plain state, no effects. Selecting a template swaps the
+ // rendered field labels below; typed values are kept per-label so switching
+ // back and forth doesn't lose what was already entered.
+ const [templateId, setTemplateId] = useState(initialTemplateId != null ? String(initialTemplateId) : "");
+ const [templateFieldValues, setTemplateFieldValues] = useState>(() =>
+ Object.fromEntries(initialTemplateValues.map((v) => [v.label, v.value])),
+ );
+
+ const selectedTemplate = templates.find((tpl) => String(tpl.id) === templateId) ?? null;
+ const templateValuesJson = JSON.stringify(
+ (selectedTemplate?.labels ?? []).map((label) => ({
+ label,
+ value: templateFieldValues[label] ?? "",
+ })),
+ );
+
const textField = (name: keyof CheckupDefaults, label: string) => (
@@ -75,6 +104,52 @@ export function CheckupForm({ defaults }: { defaults: CheckupDefaults }) {
{area("symptoms", t("symptoms"))}
+
+ {templates.length > 0 && (
+
+ )}
+
{area("diagnosis", t("diagnosis"))}
{area("conclusion", t("conclusion"))}
{area("notes", t("notes"))}
diff --git a/app/[locale]/(app)/queue/actions.ts b/app/[locale]/(app)/queue/actions.ts
index 4a7f6da..cd42717 100644
--- a/app/[locale]/(app)/queue/actions.ts
+++ b/app/[locale]/(app)/queue/actions.ts
@@ -13,11 +13,19 @@ import { revalidatePath } from "next/cache";
import { getServerSession } from "@/lib/auth/get-server-session";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import type { AppRole } from "@/lib/db/roles";
-import { RegisterCheckupSchema, type RegisterCheckupState } from "@/lib/checkups/checkup-schema";
+import {
+ RegisterCheckupSchema,
+ SetQueueCounterSchema,
+ type RegisterCheckupState,
+ type SetQueueCounterState,
+} from "@/lib/checkups/checkup-schema";
const CLINICAL: AppRole[] = ["admin", "receptionist", "doctor", "nurse"];
const isClinical = (r: AppRole | null | undefined) => !!r && CLINICAL.includes(r);
+const COUNTER_MANAGERS: AppRole[] = ["admin", "receptionist"];
+const canManageCounter = (r: AppRole | null | undefined) => !!r && COUNTER_MANAGERS.includes(r);
+
export async function registerCheckupAction(
_prev: RegisterCheckupState,
formData: FormData,
@@ -73,6 +81,53 @@ export async function registerCheckupAction(
return { status: "success", queueNumber: row?.queue_number ?? null };
}
+/**
+ * Manually sets today's queue counter for a shift (correcting a miscount,
+ * resetting after a printer jam, etc). admin/receptionist only — RLS +
+ * set_queue_counter's role check enforce it; this is defense-in-depth.
+ */
+export async function setQueueCounterAction(
+ _prev: SetQueueCounterState,
+ formData: FormData,
+): Promise
{
+ const t = await getTranslations("queue");
+
+ const session = await getServerSession();
+ if (!canManageCounter(session?.role)) {
+ return { status: "error", formError: t("errorForbidden") };
+ }
+
+ const parsed = SetQueueCounterSchema.safeParse({
+ shiftId: formData.get("shiftId"),
+ value: formData.get("value"),
+ });
+ if (!parsed.success) {
+ return { status: "error", formError: t("errorGeneric") };
+ }
+
+ const { shiftId, value } = parsed.data;
+ const supabase = await createSupabaseServerClient();
+
+ const { error } = await supabase.rpc("set_queue_counter", {
+ p_shift_id: shiftId,
+ p_value: value,
+ });
+ if (error) {
+ return { status: "error", formError: t("errorGeneric") };
+ }
+
+ await supabase.rpc("log_audit", {
+ p_action: "queue.set_counter",
+ p_entity: "daily_queue_counters",
+ p_entity_id: String(shiftId),
+ p_details: { value },
+ });
+
+ const locale = await getLocale();
+ revalidatePath(`/${locale}/queue`);
+ return { status: "success" };
+}
+
export async function callPatientAction(formData: FormData): Promise {
const session = await getServerSession();
if (!isClinical(session?.role)) return;
diff --git a/app/[locale]/(app)/queue/counter-form.tsx b/app/[locale]/(app)/queue/counter-form.tsx
new file mode 100644
index 0000000..9562e4b
--- /dev/null
+++ b/app/[locale]/(app)/queue/counter-form.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+/**
+ * Manual queue-counter override — one instance per shift. admin/receptionist
+ * only (the queue page only renders this for those roles); posts to
+ * setQueueCounterAction, which calls the set_queue_counter RPC.
+ */
+
+import { useActionState } from "react";
+import { useTranslations } from "next-intl";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import type { SetQueueCounterState } from "@/lib/checkups/checkup-schema";
+import { setQueueCounterAction } from "./actions";
+
+export function CounterForm({ shiftId, currentValue }: { shiftId: number; currentValue: number }) {
+ const t = useTranslations("queue");
+ const [state, dispatch, isPending] = useActionState(
+ setQueueCounterAction,
+ { status: "idle" },
+ );
+
+ return (
+
+ );
+}
diff --git a/app/[locale]/(app)/queue/page.tsx b/app/[locale]/(app)/queue/page.tsx
index 7391e02..eef1f3e 100644
--- a/app/[locale]/(app)/queue/page.tsx
+++ b/app/[locale]/(app)/queue/page.tsx
@@ -8,11 +8,15 @@
import { getTranslations } from "next-intl/server";
import { Link } from "@/i18n/navigation";
import { createSupabaseServerClient } from "@/lib/supabase/server";
+import { getServerSession } from "@/lib/auth/get-server-session";
import { Button } from "@/components/ui/button";
import { RegisterForm } from "./register-form";
import { QueueRealtime } from "./queue-realtime";
+import { CounterForm } from "./counter-form";
import { callPatientAction } from "./actions";
+const COUNTER_MANAGERS = new Set(["admin", "receptionist"]);
+
const STATUS_STYLE: Record = {
waiting: "bg-muted text-foreground",
in_progress: "bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-200",
@@ -25,9 +29,11 @@ export default async function QueuePage({ params }: { params: Promise<{ locale:
const tShift = await getTranslations("shifts");
const supabase = await createSupabaseServerClient();
+ const session = await getServerSession();
+ const canManageCounter = COUNTER_MANAGERS.has(session?.role ?? "");
const today = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Ho_Chi_Minh" }).format(new Date());
- const [{ data: checkups }, { data: patients }, { data: shifts }, { data: doctors }] =
+ const [{ data: checkups }, { data: patients }, { data: shifts }, { data: doctors }, { data: counters }] =
await Promise.all([
supabase
.from("checkups")
@@ -39,6 +45,7 @@ export default async function QueuePage({ params }: { params: Promise<{ locale:
supabase.rpc("search_customers", { q: "" }),
supabase.from("shifts").select("id, code").order("sort_order", { ascending: true }),
supabase.from("doctors").select("id, last_name, first_name").eq("deleted", false).order("last_name"),
+ supabase.from("daily_queue_counters").select("shift_id, last_number").eq("day", today),
]);
const rows = checkups ?? [];
@@ -50,12 +57,34 @@ export default async function QueuePage({ params }: { params: Promise<{ locale:
const custName = new Map((custs ?? []).map((c) => [c.id, `${c.last_name} ${c.first_name}`]));
const docName = new Map((doctors ?? []).map((d) => [d.id, `${d.last_name} ${d.first_name}`]));
const shiftCode = new Map((shifts ?? []).map((s) => [s.id, s.code]));
+ const counterByShift = new Map((counters ?? []).map((c) => [c.shift_id, c.last_number]));
return (
{t("title")}
+
+ {t("counters")}
+
+ {(shifts ?? []).map((s) => (
+ -
+
+
+ {tShift(s.code)} · {t("counter")}
+
+
+ {counterByShift.get(s.id) ?? 0}
+
+
+ {canManageCounter && (
+
+ )}
+
+ ))}
+
+
+
{t("registerTitle")}
; formError: string | null }
| { status: "success"; queueNumber: number | null };
+// ── Manual queue-counter override (admin/receptionist) ───────────────────────
+export const SetQueueCounterSchema = z.object({
+ shiftId: z.coerce.number().int().min(1),
+ value: z.coerce.number().int().min(0),
+});
+export type SetQueueCounterInput = z.infer;
+
+export type SetQueueCounterState =
+ | { status: "idle" }
+ | { status: "error"; formError: string | null }
+ | { status: "success" };
+
// ── Doctor fills the checkup ─────────────────────────────────────────────────
const dateOrEmpty = z
.string()
@@ -55,3 +67,34 @@ export function parseNum(s: string): number | null {
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
+
+// ── Applied checkup-template field values ────────────────────────────────────
+// Ordered [{ label, value }] snapshot of the template chosen on the checkup
+// form, serialized to a single hidden `templateValues` JSON input (same trick
+// as the prescription composer's line arrays).
+export const TemplateValuesSchema = z
+ .array(
+ z.object({
+ label: z.string().trim().min(1).max(500),
+ value: z.string().max(500),
+ }),
+ )
+ .max(50);
+export type TemplateValues = z.infer;
+
+/**
+ * Parses the `templateValues` hidden-input JSON string. Invalid JSON or a
+ * shape that fails validation is treated as "no template values" (null)
+ * rather than failing the whole checkup save.
+ */
+export function parseTemplateValues(raw: string): TemplateValues | null {
+ if (!raw.trim()) return null;
+ let json: unknown;
+ try {
+ json = JSON.parse(raw);
+ } catch {
+ return null;
+ }
+ const parsed = TemplateValuesSchema.safeParse(json);
+ return parsed.success ? parsed.data : null;
+}
diff --git a/lib/templates/template-schema.ts b/lib/templates/template-schema.ts
index ad216b3..f390699 100644
--- a/lib/templates/template-schema.ts
+++ b/lib/templates/template-schema.ts
@@ -34,9 +34,13 @@ export function fieldsTextToJson(text: string): { label: string }[] {
/** Inverse: render a stored layout back to one-label-per-line text for editing. */
export function fieldsJsonToText(fields: unknown): string {
- if (!Array.isArray(fields)) return "";
+ return fieldsJsonToLabels(fields).join("\n");
+}
+
+/** Extract just the ordered labels from a stored `fields` layout. */
+export function fieldsJsonToLabels(fields: unknown): string[] {
+ if (!Array.isArray(fields)) return [];
return fields
.map((f) => (f && typeof f === "object" && "label" in f ? String((f as { label: unknown }).label) : ""))
- .filter(Boolean)
- .join("\n");
+ .filter(Boolean);
}
diff --git a/messages/en.json b/messages/en.json
index d0c32ae..040b729 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -141,7 +141,12 @@
"open": "Open",
"status": { "waiting": "Waiting", "in_progress": "In progress", "done": "Done" },
"errorForbidden": "You do not have permission to manage the queue.",
- "errorGeneric": "Something went wrong. Please try again."
+ "errorGeneric": "Something went wrong. Please try again.",
+ "counters": "Shift counters",
+ "counter": "Current number",
+ "setCounter": "Set",
+ "saving": "Saving…",
+ "saved": "Saved."
},
"checkups": {
"title": "Checkup",
@@ -163,7 +168,10 @@
"delete": "Delete",
"confirmDelete": "Are you sure you want to delete this checkup?",
"errorForbidden": "You do not have permission to edit checkups.",
- "errorGeneric": "Something went wrong. Please try again."
+ "errorGeneric": "Something went wrong. Please try again.",
+ "template": "Checkup template",
+ "templateNone": "— No template —",
+ "templateFields": "Template fields"
},
"billing": {
"title": "Prescription & billing",
diff --git a/messages/vi.json b/messages/vi.json
index 0f11374..0bc60aa 100644
--- a/messages/vi.json
+++ b/messages/vi.json
@@ -141,7 +141,12 @@
"open": "Mở",
"status": { "waiting": "Chờ khám", "in_progress": "Đang khám", "done": "Đã khám" },
"errorForbidden": "Bạn không có quyền thao tác hàng chờ.",
- "errorGeneric": "Đã xảy ra lỗi. Vui lòng thử lại."
+ "errorGeneric": "Đã xảy ra lỗi. Vui lòng thử lại.",
+ "counters": "Số thứ tự các ca",
+ "counter": "Số thứ tự hiện tại",
+ "setCounter": "Đặt lại",
+ "saving": "Đang lưu…",
+ "saved": "Đã lưu."
},
"checkups": {
"title": "Phiếu khám",
@@ -163,7 +168,10 @@
"delete": "Xóa",
"confirmDelete": "Bạn có chắc chắn muốn xóa phiếu khám này không?",
"errorForbidden": "Bạn không có quyền chỉnh sửa phiếu khám.",
- "errorGeneric": "Đã xảy ra lỗi. Vui lòng thử lại."
+ "errorGeneric": "Đã xảy ra lỗi. Vui lòng thử lại.",
+ "template": "Mẫu khám",
+ "templateNone": "— Không dùng mẫu —",
+ "templateFields": "Thông tin theo mẫu"
},
"billing": {
"title": "Đơn thuốc & thanh toán",
diff --git a/supabase/migrations/20260725150000_bsk_counter_and_template_values.sql b/supabase/migrations/20260725150000_bsk_counter_and_template_values.sql
new file mode 100644
index 0000000..cfc61a7
--- /dev/null
+++ b/supabase/migrations/20260725150000_bsk_counter_and_template_values.sql
@@ -0,0 +1,68 @@
+-- BSK — closes the last two original-app feature gaps.
+--
+-- (a) Manual queue-counter control (orig SetCounterRequest / GetCounterRequest):
+-- admin/receptionist can set today's per-shift counter directly (e.g. to
+-- correct a miscount or reset after a printer jam) via set_queue_counter().
+-- daily_queue_counters was DEFINER-only (no SELECT policy); staff need to
+-- see the current numbers, so we add a read-only SELECT policy here.
+-- (b) Checkup templates are now actually applied: checkups.template_values
+-- stores the doctor's filled-in snapshot of the chosen template's fields.
+
+-- ─── (a) set_queue_counter — manual counter override ────────────────────────
+CREATE OR REPLACE FUNCTION bsk.set_queue_counter(p_shift_id smallint, p_value integer)
+ RETURNS void
+ LANGUAGE plpgsql
+ VOLATILE
+ SECURITY DEFINER
+ SET search_path = bsk, pg_catalog
+AS $$
+DECLARE
+ v_role bsk.app_role := bsk.current_role();
+BEGIN
+ IF v_role IS NULL OR v_role NOT IN ('admin', 'receptionist') THEN
+ RAISE EXCEPTION 'not authorized to set the queue counter';
+ END IF;
+
+ IF p_value < 0 THEN
+ RAISE EXCEPTION 'counter value must not be negative';
+ END IF;
+
+ PERFORM pg_advisory_xact_lock(hashtext('bsk:queue_counter')::bigint);
+
+ INSERT INTO bsk.daily_queue_counters (day, shift_id, last_number)
+ VALUES ((now() AT TIME ZONE 'Asia/Ho_Chi_Minh')::date, p_shift_id, p_value)
+ ON CONFLICT (day, shift_id) DO UPDATE SET last_number = EXCLUDED.last_number;
+END
+$$;
+
+COMMENT ON FUNCTION bsk.set_queue_counter(smallint, integer) IS
+ 'Manually sets today''s (VN-local) queue counter for a shift. SECURITY DEFINER, '
+ 'role-gated to admin/receptionist. Advisory-locked against register_checkup''s '
+ 'concurrent increment.';
+
+-- daily_queue_counters had RLS enabled with no policies (DEFINER-only writes).
+-- Staff need to see the current numbers, so add a read-only SELECT policy —
+-- writes still only happen through register_checkup() / set_queue_counter().
+DO $$
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM pg_policies
+ WHERE schemaname = 'bsk' AND tablename = 'daily_queue_counters'
+ AND policyname = 'daily_queue_counters_select_enrolled'
+ ) THEN
+ CREATE POLICY daily_queue_counters_select_enrolled ON bsk.daily_queue_counters
+ FOR SELECT USING (bsk.current_role() IS NOT NULL);
+ END IF;
+END
+$$;
+
+GRANT SELECT ON bsk.daily_queue_counters TO authenticated; -- no INSERT/UPDATE/DELETE: DEFINER-only
+GRANT EXECUTE ON FUNCTION bsk.set_queue_counter(smallint, integer) TO authenticated;
+
+-- ─── (b) checkups.template_values — applied template snapshot ───────────────
+ALTER TABLE bsk.checkups ADD COLUMN IF NOT EXISTS template_values jsonb;
+
+COMMENT ON COLUMN bsk.checkups.template_values IS
+ 'Ordered [{"label": "...", "value": "..."}] snapshot of the applied '
+ 'checkup_templates.fields, filled in by the doctor. No new RLS policy needed: '
+ 'the existing checkups_update_clinical policy already covers this column.';
diff --git a/types/supabase-bsk.ts b/types/supabase-bsk.ts
index f999905..dbd89d0 100644
--- a/types/supabase-bsk.ts
+++ b/types/supabase-bsk.ts
@@ -69,6 +69,7 @@ export type Database = {
symptoms: string | null;
temperature: number | null;
template_id: number | null;
+ template_values: Json | null;
updated_at: string;
weight: number | null;
};
@@ -94,6 +95,7 @@ export type Database = {
symptoms?: string | null;
temperature?: number | null;
template_id?: number | null;
+ template_values?: Json | null;
updated_at?: string;
weight?: number | null;
};
@@ -119,11 +121,18 @@ export type Database = {
symptoms?: string | null;
temperature?: number | null;
template_id?: number | null;
+ template_values?: Json | null;
updated_at?: string;
weight?: number | null;
};
Relationships: [];
};
+ daily_queue_counters: {
+ Row: { day: string; last_number: number; shift_id: number };
+ Insert: { day?: string; last_number?: number; shift_id: number };
+ Update: { day?: string; last_number?: number; shift_id?: number };
+ Relationships: [];
+ };
shifts: {
Row: { code: string; id: number; sort_order: number };
Insert: { code: string; id: number; sort_order?: number };
@@ -512,6 +521,10 @@ export type Database = {
};
Returns: number;
};
+ set_queue_counter: {
+ Args: { p_shift_id: number; p_value: number };
+ Returns: undefined;
+ };
set_staff_role: {
Args: { p_user_id: string; p_role: Database["bsk"]["Enums"]["app_role"] };
Returns: undefined;