Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras
This commit is contained in:
@@ -14,462 +14,372 @@
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="visible"
|
||||
modal
|
||||
:draggable="false"
|
||||
:closable="!saving"
|
||||
:dismissableMask="!saving"
|
||||
class="dc-dialog w-[96vw] max-w-2xl"
|
||||
:pt="{ content: { class: 'p-0' }, header: { class: 'pb-0' }, footer: { class: 'pt-0' } }"
|
||||
pt:mask:class="backdrop-blur-xs"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex w-full items-center justify-between gap-3 px-1">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<!-- Dot de cor -->
|
||||
<span
|
||||
class="shrink-0 w-3.5 h-3.5 rounded-full border-2 border-white/30 shadow-[0_0_0_3px_rgba(0,0,0,0.08)] transition-colors duration-200"
|
||||
:style="{ backgroundColor: previewBgColor }"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="text-base font-semibold truncate">
|
||||
{{ form.name || (mode === 'create' ? 'Novo compromisso' : 'Editar compromisso') }}
|
||||
</div>
|
||||
<div class="text-xs opacity-50">
|
||||
{{ mode === 'create' ? 'Novo tipo de compromisso' : 'Editando tipo de compromisso' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
v-if="mode === 'edit' && canDelete"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
text
|
||||
rounded
|
||||
:disabled="saving"
|
||||
v-tooltip.top="'Excluir'"
|
||||
@click="emitDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Banner de preview -->
|
||||
<div
|
||||
class="h-[72px] flex items-center justify-center transition-colors duration-[250ms] rounded-[6px]"
|
||||
:style="{ backgroundColor: previewBgColor }"
|
||||
>
|
||||
<span
|
||||
class="text-base font-bold tracking-[-0.02em] px-[1.1rem] py-[0.35rem] bg-black/15 rounded-full backdrop-blur-sm transition-colors duration-200"
|
||||
:style="{ color: form.text_color || '#ffffff' }"
|
||||
>
|
||||
{{ form.name || 'Nome do compromisso' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Corpo -->
|
||||
<div class="flex flex-col gap-4 mt-4">
|
||||
|
||||
<!-- Nome + Ativo -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1">
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-tag" />
|
||||
<InputText
|
||||
id="cr-nome"
|
||||
v-model="form.name"
|
||||
class="w-full"
|
||||
variant="filled"
|
||||
:disabled="saving || isEditLocked"
|
||||
@keydown.enter.prevent="submit"
|
||||
/>
|
||||
</IconField>
|
||||
<label for="cr-nome">Nome *</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Ativo -->
|
||||
<div class="shrink-0 flex items-center gap-2">
|
||||
<span class="text-sm font-medium">Ativo</span>
|
||||
<ToggleSwitch v-model="form.active" :disabled="saving || isActiveLocked" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Descrição -->
|
||||
<FloatLabel variant="on">
|
||||
<Textarea
|
||||
id="cr-descricao"
|
||||
v-model="form.description"
|
||||
autoResize
|
||||
rows="2"
|
||||
class="w-full"
|
||||
variant="filled"
|
||||
:disabled="saving || isEditLocked"
|
||||
/>
|
||||
<label for="cr-descricao">Descrição</label>
|
||||
</FloatLabel>
|
||||
|
||||
<!-- Seção Cor -->
|
||||
<div class="border border-[var(--surface-border)] rounded-[6px] bg-[var(--surface-card)] p-4">
|
||||
<div class="text-[1rem] font-bold uppercase tracking-[0.06em] opacity-45 mb-3">Cor</div>
|
||||
|
||||
<!-- Paleta predefinida -->
|
||||
<div class="flex flex-wrap gap-[0.45rem]">
|
||||
<button
|
||||
v-for="p in presetColors"
|
||||
:key="p.bg"
|
||||
class="w-7 h-7 rounded-full grid place-items-center cursor-pointer relative transition-transform duration-[120ms] ease-in-out hover:scale-[1.18] hover:shadow-[0_3px_10px_rgba(0,0,0,0.2)] disabled:cursor-not-allowed"
|
||||
:class="form.bg_color === p.bg ? 'shadow-[0_0_0_2px_var(--text-color)] border-2 border-[var(--surface-0,#fff)]' : 'border-0'"
|
||||
:style="{ backgroundColor: `#${p.bg}` }"
|
||||
:title="p.name"
|
||||
:disabled="saving || isEditLocked"
|
||||
@click="applyPreset(p)"
|
||||
>
|
||||
<i v-if="form.bg_color === p.bg" class="pi pi-check !text-[13px] text-white font-black p-1" />
|
||||
</button>
|
||||
|
||||
<!-- Custom ColorPicker -->
|
||||
<div
|
||||
class="w-7 h-7 rounded-full grid place-items-center cursor-pointer overflow-hidden relative transition-transform duration-[120ms] ease-in-out hover:scale-[1.18] hover:shadow-[0_3px_10px_rgba(0,0,0,0.2)]"
|
||||
:class="isCustomColor ? 'shadow-[0_0_0_2px_var(--text-color)]' : ''"
|
||||
style="background: conic-gradient(red, yellow, lime, cyan, blue, magenta, red);"
|
||||
title="Cor personalizada"
|
||||
>
|
||||
<i
|
||||
v-if="isCustomColor"
|
||||
class="pi pi-check !text-[13px] text-white font-black absolute z-10 pointer-events-none drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]"
|
||||
/>
|
||||
<ColorPicker
|
||||
v-model="form.bg_color"
|
||||
format="hex"
|
||||
:disabled="saving || isEditLocked"
|
||||
class="absolute inset-0 [&_.p-colorpicker-preview]:w-full [&_.p-colorpicker-preview]:h-full [&_.p-colorpicker-preview]:border-0 [&_.p-colorpicker-preview]:rounded-full [&_.p-colorpicker-preview]:opacity-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Texto -->
|
||||
<div class="flex items-center gap-3 mt-2">
|
||||
<span class="text-xs font-medium opacity-60 uppercase tracking-wide">Texto</span>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="inline-flex items-center gap-[0.4rem] px-3 py-1 rounded-full border text-sm font-medium cursor-pointer transition-colors duration-[120ms] disabled:cursor-not-allowed"
|
||||
:class="
|
||||
form.text_color === '#ffffff'
|
||||
? 'bg-[var(--surface-section,var(--surface-100))] border-[var(--primary-color)] text-[var(--primary-color)] font-bold'
|
||||
: 'bg-transparent border-[var(--surface-border)] text-[var(--text-color)] hover:bg-[var(--surface-hover)]'
|
||||
"
|
||||
:disabled="saving || isEditLocked"
|
||||
@click="form.text_color = '#ffffff'"
|
||||
>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block border border-[#ccc]" style="background:#ffffff;" />
|
||||
Branco
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-[0.4rem] px-3 py-1 rounded-full border text-sm font-medium cursor-pointer transition-colors duration-[120ms] disabled:cursor-not-allowed"
|
||||
:class="
|
||||
form.text_color === '#000000'
|
||||
? 'bg-[var(--surface-section,var(--surface-100))] border-[var(--primary-color)] text-[var(--primary-color)] font-bold'
|
||||
: 'bg-transparent border-[var(--surface-border)] text-[var(--text-color)] hover:bg-[var(--surface-hover)]'
|
||||
"
|
||||
:disabled="saving || isEditLocked"
|
||||
@click="form.text_color = '#000000'"
|
||||
>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block" style="background:#000000;" />
|
||||
Preto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campos adicionais -->
|
||||
<div class="border border-[var(--surface-border)] rounded-[6px] bg-[var(--surface-card)] p-4">
|
||||
<div class="flex items-center justify-between gap-2 mb-3">
|
||||
<div class="text-[1rem] font-bold uppercase tracking-[0.06em] opacity-45">Campos adicionais</div>
|
||||
<Button
|
||||
label="Adicionar campo"
|
||||
icon="pi pi-plus"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
class="rounded-full"
|
||||
:disabled="saving || isFieldsLocked"
|
||||
@click="addField"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!form.fields.length" class="py-3 text-center text-sm opacity-50">
|
||||
Nenhum campo adicional configurado.
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="(f, idx) in form.fields"
|
||||
:key="f.key"
|
||||
class="grid grid-cols-1 gap-2 rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-0)] p-3 md:grid-cols-12"
|
||||
>
|
||||
<div class="md:col-span-6">
|
||||
<FloatLabel variant="on">
|
||||
<InputText
|
||||
:id="`cr-field-label-${idx}`"
|
||||
v-model="f.label"
|
||||
class="w-full"
|
||||
variant="filled"
|
||||
:disabled="saving || isFieldsLocked"
|
||||
@keydown.enter.prevent="submit"
|
||||
@blur="syncKey(f)"
|
||||
/>
|
||||
<label :for="`cr-field-label-${idx}`">Nome do campo</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
<Dropdown
|
||||
:id="`cr-field-type-${idx}`"
|
||||
v-model="f.type"
|
||||
:options="fieldTypeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full"
|
||||
variant="filled"
|
||||
:disabled="saving || isFieldsLocked"
|
||||
/>
|
||||
<label :for="`cr-field-type-${idx}`">Tipo</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 flex items-center justify-end">
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
text
|
||||
rounded
|
||||
:disabled="saving || isFieldsLocked"
|
||||
@click="removeField(idx)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer com botões Cancelar / Salvar -->
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-end gap-2 pt-2">
|
||||
<Button
|
||||
label="Cancelar"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="rounded-full"
|
||||
:disabled="saving"
|
||||
@click="close"
|
||||
/>
|
||||
<Button
|
||||
label="Salvar"
|
||||
icon="pi pi-check"
|
||||
class="rounded-full"
|
||||
:loading="saving"
|
||||
:disabled="!canSubmit"
|
||||
@click="submit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
|
||||
import Textarea from 'primevue/textarea'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
|
||||
import Textarea from 'primevue/textarea';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import ColorPicker from 'primevue/colorpicker';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
mode: { type: String, default: 'create' }, // 'create' | 'edit'
|
||||
saving: { type: Boolean, default: false },
|
||||
commitment: { type: Object, default: null } // quando edit
|
||||
})
|
||||
modelValue: { type: Boolean, default: false },
|
||||
mode: { type: String, default: 'create' }, // 'create' | 'edit'
|
||||
saving: { type: Boolean, default: false },
|
||||
commitment: { type: Object, default: null } // quando edit
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'delete'])
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'delete']);
|
||||
|
||||
const fieldTypeOptions = [
|
||||
{ label: 'Texto', value: 'text' },
|
||||
{ label: 'Texto longo', value: 'textarea' }
|
||||
]
|
||||
{ label: 'Texto', value: 'text' },
|
||||
{ label: 'Texto longo', value: 'textarea' }
|
||||
];
|
||||
|
||||
const presetColors = [
|
||||
{ bg: '6366f1', text: '#ffffff', name: 'Índigo' },
|
||||
{ bg: '8b5cf6', text: '#ffffff', name: 'Violeta' },
|
||||
{ bg: 'ec4899', text: '#ffffff', name: 'Rosa' },
|
||||
{ bg: 'ef4444', text: '#ffffff', name: 'Vermelho' },
|
||||
{ bg: 'f97316', text: '#ffffff', name: 'Laranja' },
|
||||
{ bg: 'eab308', text: '#000000', name: 'Amarelo' },
|
||||
{ bg: '22c55e', text: '#ffffff', name: 'Verde' },
|
||||
{ bg: '14b8a6', text: '#ffffff', name: 'Teal' },
|
||||
{ bg: '3b82f6', text: '#ffffff', name: 'Azul' },
|
||||
{ bg: '06b6d4', text: '#ffffff', name: 'Ciano' },
|
||||
{ bg: '64748b', text: '#ffffff', name: 'Ardósia' },
|
||||
{ bg: '292524', text: '#ffffff', name: 'Escuro' },
|
||||
]
|
||||
{ bg: '6366f1', text: '#ffffff', name: 'Índigo' },
|
||||
{ bg: '8b5cf6', text: '#ffffff', name: 'Violeta' },
|
||||
{ bg: 'ec4899', text: '#ffffff', name: 'Rosa' },
|
||||
{ bg: 'ef4444', text: '#ffffff', name: 'Vermelho' },
|
||||
{ bg: 'f97316', text: '#ffffff', name: 'Laranja' },
|
||||
{ bg: 'eab308', text: '#000000', name: 'Amarelo' },
|
||||
{ bg: '22c55e', text: '#ffffff', name: 'Verde' },
|
||||
{ bg: '14b8a6', text: '#ffffff', name: 'Teal' },
|
||||
{ bg: '3b82f6', text: '#ffffff', name: 'Azul' },
|
||||
{ bg: '06b6d4', text: '#ffffff', name: 'Ciano' },
|
||||
{ bg: '64748b', text: '#ffffff', name: 'Ardósia' },
|
||||
{ bg: '292524', text: '#ffffff', name: 'Escuro' }
|
||||
];
|
||||
|
||||
// bg_colors dos presets (sem #) para comparação
|
||||
const presetBgValues = presetColors.map(p => p.bg)
|
||||
const presetBgValues = presetColors.map((p) => p.bg);
|
||||
|
||||
// Verdadeiro quando a cor atual não bate com nenhum preset
|
||||
const isCustomColor = computed(() => {
|
||||
if (!form.bg_color) return false
|
||||
const clean = String(form.bg_color).replace('#', '').toLowerCase()
|
||||
return !presetBgValues.includes(clean)
|
||||
})
|
||||
if (!form.bg_color) return false;
|
||||
const clean = String(form.bg_color).replace('#', '').toLowerCase();
|
||||
return !presetBgValues.includes(clean);
|
||||
});
|
||||
|
||||
function applyPreset (p) {
|
||||
if (props.saving) return
|
||||
form.bg_color = p.bg
|
||||
form.text_color = p.text
|
||||
function applyPreset(p) {
|
||||
if (props.saving) return;
|
||||
form.bg_color = p.bg;
|
||||
form.text_color = p.text;
|
||||
}
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
native: false,
|
||||
locked: false,
|
||||
active: true,
|
||||
bg_color: '6366f1',
|
||||
text_color: '#ffffff',
|
||||
fields: []
|
||||
})
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
native: false,
|
||||
locked: false,
|
||||
active: true,
|
||||
bg_color: '6366f1',
|
||||
text_color: '#ffffff',
|
||||
fields: []
|
||||
});
|
||||
|
||||
const previewBgColor = computed(() => {
|
||||
if (!form.bg_color) return '#6366f1'
|
||||
return form.bg_color.startsWith('#') ? form.bg_color : `#${form.bg_color}`
|
||||
})
|
||||
if (!form.bg_color) return '#6366f1';
|
||||
return form.bg_color.startsWith('#') ? form.bg_color : `#${form.bg_color}`;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (!open) return
|
||||
hydrate()
|
||||
}
|
||||
)
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (!open) return;
|
||||
hydrate();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.commitment,
|
||||
() => {
|
||||
if (!props.modelValue) return
|
||||
hydrate()
|
||||
}
|
||||
)
|
||||
() => props.commitment,
|
||||
() => {
|
||||
if (!props.modelValue) return;
|
||||
hydrate();
|
||||
}
|
||||
);
|
||||
|
||||
function hydrate () {
|
||||
const c = props.commitment
|
||||
if (props.mode === 'edit' && c) {
|
||||
form.id = c.id
|
||||
form.name = c.name || ''
|
||||
form.description = c.description || ''
|
||||
form.native = !!c.native
|
||||
form.locked = !!c.locked
|
||||
form.active = !!c.active
|
||||
form.bg_color = c.bg_color || '6366f1'
|
||||
form.text_color = c.text_color || '#ffffff'
|
||||
form.fields = Array.isArray(c.fields) ? JSON.parse(JSON.stringify(c.fields)) : []
|
||||
} else {
|
||||
form.id = null
|
||||
form.name = ''
|
||||
form.description = ''
|
||||
form.native = false
|
||||
form.locked = false
|
||||
form.active = true
|
||||
form.bg_color = '6366f1'
|
||||
form.text_color = '#ffffff'
|
||||
form.fields = []
|
||||
}
|
||||
function hydrate() {
|
||||
const c = props.commitment;
|
||||
if (props.mode === 'edit' && c) {
|
||||
form.id = c.id;
|
||||
form.name = c.name || '';
|
||||
form.description = c.description || '';
|
||||
form.native = !!c.native;
|
||||
form.locked = !!c.locked;
|
||||
form.active = !!c.active;
|
||||
form.bg_color = c.bg_color || '6366f1';
|
||||
form.text_color = c.text_color || '#ffffff';
|
||||
form.fields = Array.isArray(c.fields) ? JSON.parse(JSON.stringify(c.fields)) : [];
|
||||
} else {
|
||||
form.id = null;
|
||||
form.name = '';
|
||||
form.description = '';
|
||||
form.native = false;
|
||||
form.locked = false;
|
||||
form.active = true;
|
||||
form.bg_color = '6366f1';
|
||||
form.text_color = '#ffffff';
|
||||
form.fields = [];
|
||||
}
|
||||
}
|
||||
|
||||
const isActiveLocked = computed(() => !!form.locked)
|
||||
const isEditLocked = computed(() => false)
|
||||
const isFieldsLocked = computed(() => false)
|
||||
const canDelete = computed(() => !form.native)
|
||||
const isActiveLocked = computed(() => !!form.locked);
|
||||
const isEditLocked = computed(() => false);
|
||||
const isFieldsLocked = computed(() => false);
|
||||
const canDelete = computed(() => !form.native);
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
if (props.saving) return false
|
||||
if (!String(form.name || '').trim()) return false
|
||||
return true
|
||||
})
|
||||
if (props.saving) return false;
|
||||
if (!String(form.name || '').trim()) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
function close () {
|
||||
if (props.saving) return
|
||||
visible.value = false
|
||||
function close() {
|
||||
if (props.saving) return;
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function submit () {
|
||||
if (!canSubmit.value) return
|
||||
function submit() {
|
||||
if (!canSubmit.value) return;
|
||||
|
||||
const payload = {
|
||||
id: form.id,
|
||||
name: String(form.name || '').trim(),
|
||||
description: String(form.description || '').trim(),
|
||||
active: form.locked ? true : !!form.active,
|
||||
bg_color: form.bg_color || null,
|
||||
text_color: form.text_color || null,
|
||||
fields: (form.fields || []).map(f => ({
|
||||
key: f.key,
|
||||
label: String(f.label || '').trim() || 'Campo',
|
||||
type: f.type === 'textarea' ? 'textarea' : 'text',
|
||||
required: !!f.required
|
||||
}))
|
||||
}
|
||||
const payload = {
|
||||
id: form.id,
|
||||
name: String(form.name || '').trim(),
|
||||
description: String(form.description || '').trim(),
|
||||
active: form.locked ? true : !!form.active,
|
||||
bg_color: form.bg_color || null,
|
||||
text_color: form.text_color || null,
|
||||
fields: (form.fields || []).map((f) => ({
|
||||
key: f.key,
|
||||
label: String(f.label || '').trim() || 'Campo',
|
||||
type: f.type === 'textarea' ? 'textarea' : 'text',
|
||||
required: !!f.required
|
||||
}))
|
||||
};
|
||||
|
||||
emit('save', payload)
|
||||
emit('save', payload);
|
||||
}
|
||||
|
||||
function emitDelete () {
|
||||
if (props.saving) return
|
||||
emit('delete', { id: form.id })
|
||||
visible.value = false
|
||||
function emitDelete() {
|
||||
if (props.saving) return;
|
||||
emit('delete', { id: form.id });
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function addField () {
|
||||
const base = `campo-${form.fields.length + 1}`
|
||||
form.fields.push({
|
||||
key: makeKey(base),
|
||||
label: 'Observação',
|
||||
type: 'textarea',
|
||||
required: false
|
||||
})
|
||||
function addField() {
|
||||
const base = `campo-${form.fields.length + 1}`;
|
||||
form.fields.push({
|
||||
key: makeKey(base),
|
||||
label: 'Observação',
|
||||
type: 'textarea',
|
||||
required: false
|
||||
});
|
||||
}
|
||||
|
||||
function removeField (idx) {
|
||||
form.fields.splice(idx, 1)
|
||||
function removeField(idx) {
|
||||
form.fields.splice(idx, 1);
|
||||
}
|
||||
|
||||
function syncKey (field) {
|
||||
const next = makeKey(field.label)
|
||||
field.key = next
|
||||
function syncKey(field) {
|
||||
const next = makeKey(field.label);
|
||||
field.key = next;
|
||||
}
|
||||
|
||||
function makeKey (label) {
|
||||
const k = String(label || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/(^_|_$)/g, '') || `field_${Math.random().toString(16).slice(2, 8)}`
|
||||
return k
|
||||
function makeKey(label) {
|
||||
const k =
|
||||
String(label || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/(^_|_$)/g, '') || `field_${Math.random().toString(16).slice(2, 8)}`;
|
||||
return k;
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="visible"
|
||||
modal
|
||||
:draggable="false"
|
||||
:closable="!saving"
|
||||
:dismissableMask="!saving"
|
||||
class="dc-dialog w-[96vw] max-w-2xl"
|
||||
:pt="{ content: { class: 'p-0' }, header: { class: 'pb-0' }, footer: { class: 'pt-0' } }"
|
||||
pt:mask:class="backdrop-blur-xs"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex w-full items-center justify-between gap-3 px-1">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<!-- Dot de cor -->
|
||||
<span class="shrink-0 w-3.5 h-3.5 rounded-full border-2 border-white/30 shadow-[0_0_0_3px_rgba(0,0,0,0.08)] transition-colors duration-200" :style="{ backgroundColor: previewBgColor }" />
|
||||
<div class="min-w-0">
|
||||
<div class="text-base font-semibold truncate">
|
||||
{{ form.name || (mode === 'create' ? 'Novo compromisso' : 'Editar compromisso') }}
|
||||
</div>
|
||||
<div class="text-xs opacity-50">
|
||||
{{ mode === 'create' ? 'Novo tipo de compromisso' : 'Editando tipo de compromisso' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Button v-if="mode === 'edit' && canDelete" icon="pi pi-trash" severity="danger" text rounded :disabled="saving" v-tooltip.top="'Excluir'" @click="emitDelete" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Banner de preview -->
|
||||
<div class="h-[72px] flex items-center justify-center transition-colors duration-[250ms] rounded-[6px]" :style="{ backgroundColor: previewBgColor }">
|
||||
<span class="text-base font-bold tracking-[-0.02em] px-[1.1rem] py-[0.35rem] bg-black/15 rounded-full backdrop-blur-sm transition-colors duration-200" :style="{ color: form.text_color || '#ffffff' }">
|
||||
{{ form.name || 'Nome do compromisso' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Corpo -->
|
||||
<div class="flex flex-col gap-4 mt-4">
|
||||
<!-- Nome + Ativo -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1">
|
||||
<FloatLabel variant="on">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-tag" />
|
||||
<InputText id="cr-nome" v-model="form.name" class="w-full" variant="filled" :disabled="saving || isEditLocked" @keydown.enter.prevent="submit" />
|
||||
</IconField>
|
||||
<label for="cr-nome">Nome *</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Ativo -->
|
||||
<div class="shrink-0 flex items-center gap-2">
|
||||
<span class="text-sm font-medium">Ativo</span>
|
||||
<ToggleSwitch v-model="form.active" :disabled="saving || isActiveLocked" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Descrição -->
|
||||
<FloatLabel variant="on">
|
||||
<Textarea id="cr-descricao" v-model="form.description" autoResize rows="2" class="w-full" variant="filled" :disabled="saving || isEditLocked" />
|
||||
<label for="cr-descricao">Descrição</label>
|
||||
</FloatLabel>
|
||||
|
||||
<!-- Seção Cor -->
|
||||
<div class="border border-[var(--surface-border)] rounded-[6px] bg-[var(--surface-card)] p-4">
|
||||
<div class="text-[1rem] font-bold uppercase tracking-[0.06em] opacity-45 mb-3">Cor</div>
|
||||
|
||||
<!-- Paleta predefinida -->
|
||||
<div class="flex flex-wrap gap-[0.45rem]">
|
||||
<button
|
||||
v-for="p in presetColors"
|
||||
:key="p.bg"
|
||||
class="w-7 h-7 rounded-full grid place-items-center cursor-pointer relative transition-transform duration-[120ms] ease-in-out hover:scale-[1.18] hover:shadow-[0_3px_10px_rgba(0,0,0,0.2)] disabled:cursor-not-allowed"
|
||||
:class="form.bg_color === p.bg ? 'shadow-[0_0_0_2px_var(--text-color)] border-2 border-[var(--surface-0,#fff)]' : 'border-0'"
|
||||
:style="{ backgroundColor: `#${p.bg}` }"
|
||||
:title="p.name"
|
||||
:disabled="saving || isEditLocked"
|
||||
@click="applyPreset(p)"
|
||||
>
|
||||
<i v-if="form.bg_color === p.bg" class="pi pi-check !text-[13px] text-white font-black p-1" />
|
||||
</button>
|
||||
|
||||
<!-- Custom ColorPicker -->
|
||||
<div
|
||||
class="w-7 h-7 rounded-full grid place-items-center cursor-pointer overflow-hidden relative transition-transform duration-[120ms] ease-in-out hover:scale-[1.18] hover:shadow-[0_3px_10px_rgba(0,0,0,0.2)]"
|
||||
:class="isCustomColor ? 'shadow-[0_0_0_2px_var(--text-color)]' : ''"
|
||||
style="background: conic-gradient(red, yellow, lime, cyan, blue, magenta, red)"
|
||||
title="Cor personalizada"
|
||||
>
|
||||
<i v-if="isCustomColor" class="pi pi-check !text-[13px] text-white font-black absolute z-10 pointer-events-none drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]" />
|
||||
<ColorPicker
|
||||
v-model="form.bg_color"
|
||||
format="hex"
|
||||
:disabled="saving || isEditLocked"
|
||||
class="absolute inset-0 [&_.p-colorpicker-preview]:w-full [&_.p-colorpicker-preview]:h-full [&_.p-colorpicker-preview]:border-0 [&_.p-colorpicker-preview]:rounded-full [&_.p-colorpicker-preview]:opacity-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Texto -->
|
||||
<div class="flex items-center gap-3 mt-2">
|
||||
<span class="text-xs font-medium opacity-60 uppercase tracking-wide">Texto</span>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="inline-flex items-center gap-[0.4rem] px-3 py-1 rounded-full border text-sm font-medium cursor-pointer transition-colors duration-[120ms] disabled:cursor-not-allowed"
|
||||
:class="
|
||||
form.text_color === '#ffffff'
|
||||
? 'bg-[var(--surface-section,var(--surface-100))] border-[var(--primary-color)] text-[var(--primary-color)] font-bold'
|
||||
: 'bg-transparent border-[var(--surface-border)] text-[var(--text-color)] hover:bg-[var(--surface-hover)]'
|
||||
"
|
||||
:disabled="saving || isEditLocked"
|
||||
@click="form.text_color = '#ffffff'"
|
||||
>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block border border-[#ccc]" style="background: #ffffff" />
|
||||
Branco
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-[0.4rem] px-3 py-1 rounded-full border text-sm font-medium cursor-pointer transition-colors duration-[120ms] disabled:cursor-not-allowed"
|
||||
:class="
|
||||
form.text_color === '#000000'
|
||||
? 'bg-[var(--surface-section,var(--surface-100))] border-[var(--primary-color)] text-[var(--primary-color)] font-bold'
|
||||
: 'bg-transparent border-[var(--surface-border)] text-[var(--text-color)] hover:bg-[var(--surface-hover)]'
|
||||
"
|
||||
:disabled="saving || isEditLocked"
|
||||
@click="form.text_color = '#000000'"
|
||||
>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block" style="background: #000000" />
|
||||
Preto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campos adicionais -->
|
||||
<div class="border border-[var(--surface-border)] rounded-[6px] bg-[var(--surface-card)] p-4">
|
||||
<div class="flex items-center justify-between gap-2 mb-3">
|
||||
<div class="text-[1rem] font-bold uppercase tracking-[0.06em] opacity-45">Campos adicionais</div>
|
||||
<Button label="Adicionar campo" icon="pi pi-plus" severity="secondary" outlined size="small" class="rounded-full" :disabled="saving || isFieldsLocked" @click="addField" />
|
||||
</div>
|
||||
|
||||
<div v-if="!form.fields.length" class="py-3 text-center text-sm opacity-50">Nenhum campo adicional configurado.</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<div v-for="(f, idx) in form.fields" :key="f.key" class="grid grid-cols-1 gap-2 rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-0)] p-3 md:grid-cols-12">
|
||||
<div class="md:col-span-6">
|
||||
<FloatLabel variant="on">
|
||||
<InputText :id="`cr-field-label-${idx}`" v-model="f.label" class="w-full" variant="filled" :disabled="saving || isFieldsLocked" @keydown.enter.prevent="submit" @blur="syncKey(f)" />
|
||||
<label :for="`cr-field-label-${idx}`">Nome do campo</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
<Dropdown :id="`cr-field-type-${idx}`" v-model="f.type" :options="fieldTypeOptions" optionLabel="label" optionValue="value" class="w-full" variant="filled" :disabled="saving || isFieldsLocked" />
|
||||
<label :for="`cr-field-type-${idx}`">Tipo</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 flex items-center justify-end">
|
||||
<Button icon="pi pi-trash" severity="danger" text rounded :disabled="saving || isFieldsLocked" @click="removeField(idx)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer com botões Cancelar / Salvar -->
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-end gap-2 pt-2">
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" :disabled="saving" @click="close" />
|
||||
<Button label="Salvar" icon="pi pi-check" class="rounded-full" :loading="saving" :disabled="!canSubmit" @click="submit" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user