agenda: services nome unico por owner + cadastro in-flow

services: useServices.save e ServiceQuickCreateDialog agora validam nome
unico por owner (ilike, case-insensitive; ignora self no update). Antes
era possivel criar dois servicos com nome igual via paths diferentes.

cadastro in-flow: ComponentCadastroRapido e PatientCadastroDialog ganham
prop hideViewListButton. Quando true (uso dentro de outro fluxo, ex:
cadastrar paciente direto no AgendaEventDialog), esconde "Salvar e ver
pacientes" — navegar pra lista abandonaria o evento em edicao.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-11 10:44:27 -03:00
parent 8b0e633aac
commit 8e3c09d1b1
4 changed files with 49 additions and 7 deletions
+10 -3
View File
@@ -90,7 +90,13 @@ const props = defineProps({
initialData: { type: Object, default: () => ({}) }, initialData: { type: Object, default: () => ({}) },
closeOnCreated: { type: Boolean, default: true }, closeOnCreated: { type: Boolean, default: true },
resetOnOpen: { type: Boolean, default: true } resetOnOpen: { type: Boolean, default: true },
// Quando true (uso "in-flow", ex: dentro do AgendaEventDialog),
// esconde o botao "Salvar e ver pacientes" — navegar pra lista
// abandonaria o fluxo onde o cadastro foi aberto. Default false
// mantem o botao visivel pra usos standalone (sidebar, menu).
hideViewListButton: { type: Boolean, default: false }
}); });
const emit = defineEmits(['update:modelValue', 'created']); const emit = defineEmits(['update:modelValue', 'created']);
@@ -346,9 +352,10 @@ async function submit(mode = 'only') {
<template #footer> <template #footer>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<Button label="Cancelar" severity="secondary" text :disabled="saving" @click="close" /> <Button label="Cancelar" severity="secondary" text :disabled="saving" @click="close" />
<!-- Na rota de pacientes: "Salvar" --> <!-- Na rota de pacientes OU em fluxo (hideViewListButton): "Salvar" / "Salvar e fechar" -->
<Button v-if="isOnPatientsPage" label="Salvar" :loading="saving" :disabled="saving" @click="submit('only')" /> <Button v-if="isOnPatientsPage" label="Salvar" :loading="saving" :disabled="saving" @click="submit('only')" />
<!-- Fora da rota de pacientes: "Salvar e fechar" + "Salvar e ver pacientes" --> <Button v-else-if="hideViewListButton" label="Salvar e fechar" :loading="saving" :disabled="saving" @click="submit('only')" />
<!-- Standalone fora da lista: "Salvar e fechar" + "Salvar e ver pacientes" -->
<template v-else> <template v-else>
<Button label="Salvar e fechar" severity="secondary" outlined :loading="saving" :disabled="saving" @click="submit('only')" /> <Button label="Salvar e fechar" severity="secondary" outlined :loading="saving" :disabled="saving" @click="submit('only')" />
<Button label="Salvar e ver pacientes" :loading="saving" :disabled="saving" @click="submit('view')" /> <Button label="Salvar e ver pacientes" :loading="saving" :disabled="saving" @click="submit('view')" />
+10 -3
View File
@@ -33,7 +33,13 @@ import PatientsCadastroPage from '@/features/patients/cadastro/PatientsCadastroP
const props = defineProps({ const props = defineProps({
modelValue: { type: Boolean, default: false }, modelValue: { type: Boolean, default: false },
patientId: { type: String, default: null } patientId: { type: String, default: null },
// Quando true (uso "in-flow", ex: aberto de dentro do AgendaEventDialog),
// esconde o botao "Salvar e ver pacientes" — navegar pra lista
// abandonaria o fluxo onde o cadastro foi aberto. Default false
// mantem o botao visivel pra usos standalone (Pacientes, Melissa).
hideViewListButton: { type: Boolean, default: false }
}); });
const emit = defineEmits(['update:modelValue', 'created']); const emit = defineEmits(['update:modelValue', 'created']);
@@ -188,9 +194,10 @@ async function onCreated(data) {
<template #footer> <template #footer>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<Button label="Cancelar" severity="secondary" text :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="isOpen = false" /> <Button label="Cancelar" severity="secondary" text :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="isOpen = false" />
<!-- Na rota de pacientes: "Salvar" --> <!-- Na rota de pacientes OU em fluxo (hideViewListButton): um botao -->
<Button v-if="isOnPatientsPage" label="Salvar" :loading="!!pageRef?.saving?.value" :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="submitWith('only')" /> <Button v-if="isOnPatientsPage" label="Salvar" :loading="!!pageRef?.saving?.value" :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="submitWith('only')" />
<!-- Fora da rota de pacientes: "Salvar e fechar" + "Salvar e ver pacientes" --> <Button v-else-if="hideViewListButton" label="Salvar e fechar" :loading="!!pageRef?.saving?.value" :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="submitWith('only')" />
<!-- Standalone fora da lista: "Salvar e fechar" + "Salvar e ver pacientes" -->
<template v-else> <template v-else>
<Button label="Salvar e fechar" severity="secondary" outlined :loading="pendingMode === 'only' && !!pageRef?.saving?.value" :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="submitWith('only')" /> <Button label="Salvar e fechar" severity="secondary" outlined :loading="pendingMode === 'only' && !!pageRef?.saving?.value" :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="submitWith('only')" />
<Button label="Salvar e ver pacientes" :loading="pendingMode === 'view' && !!pageRef?.saving?.value" :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="submitWith('view')" /> <Button label="Salvar e ver pacientes" :loading="pendingMode === 'view' && !!pageRef?.saving?.value" :disabled="!!pageRef?.saving?.value || !!pageRef?.deleting?.value" @click="submitWith('view')" />
@@ -67,10 +67,23 @@ async function onSave() {
} }
saving.value = true; saving.value = true;
try { try {
const name = form.value.name.trim().slice(0, 120);
// Nome unico por owner (case-insensitive) — espelha a validacao
// do useServices.save() pra impedir duplicata tambem quando o
// cadastro vem do quick-create dentro do AgendaEventDialog.
const { data: dups, error: dupErr } = await supabase.from('services').select('id').eq('owner_id', ownerId).ilike('name', name).limit(1);
if (dupErr) throw dupErr;
if (dups && dups.length > 0) {
toast.add({ severity: 'warn', summary: 'Nome em uso', detail: 'Já existe um serviço com este nome.', life: 3500 });
saving.value = false;
return;
}
const payload = { const payload = {
owner_id: ownerId, owner_id: ownerId,
tenant_id: tid, tenant_id: tid,
name: form.value.name.trim().slice(0, 120), name,
price: Number(form.value.price), price: Number(form.value.price),
duration_min: form.value.duration_min ? Number(form.value.duration_min) : null, duration_min: form.value.duration_min ? Number(form.value.duration_min) : null,
description: form.value.description?.trim().slice(0, 500) || null, description: form.value.description?.trim().slice(0, 500) || null,
@@ -54,6 +54,21 @@ export function useServices() {
async function save(payload) { async function save(payload) {
error.value = ''; error.value = '';
try { try {
const name = payload.name?.trim();
if (!name) throw new Error('Nome do serviço é obrigatório.');
if (!payload.owner_id) throw new Error('Owner ausente.');
// Nome unico por owner (case-insensitive). No update,
// ignora o proprio id pra nao conflitar consigo mesmo
// quando o usuario salva sem mudar o nome.
let dupQuery = supabase.from('services').select('id').eq('owner_id', payload.owner_id).ilike('name', name).limit(1);
if (payload.id) dupQuery = dupQuery.neq('id', payload.id);
const { data: dups, error: dupErr } = await dupQuery;
if (dupErr) throw dupErr;
if (dups && dups.length > 0) {
throw new Error('Já existe um serviço com este nome.');
}
if (payload.id) { if (payload.id) {
const { id, owner_id, tenant_id, ...fields } = payload; const { id, owner_id, tenant_id, ...fields } = payload;
const { error: err } = await supabase.from('services').update(fields).eq('id', id).eq('owner_id', owner_id); const { error: err } = await supabase.from('services').update(fields).eq('id', id).eq('owner_id', owner_id);