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:
@@ -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: só "Salvar" -->
|
<!-- Na rota de pacientes OU em fluxo (hideViewListButton): só "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')" />
|
||||||
|
|||||||
@@ -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: só "Salvar" -->
|
<!-- Na rota de pacientes OU em fluxo (hideViewListButton): só 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user