first commit

This commit is contained in:
Leonardo
2026-02-18 22:36:45 -03:00
parent ec6b6ef53a
commit 676042268b
122 changed files with 26354 additions and 1615 deletions
+271
View File
@@ -0,0 +1,271 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { supabase } from '@/lib/supabase/client'
import Toolbar from 'primevue/toolbar'
import Button from 'primevue/button'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
import Dialog from 'primevue/dialog'
import InputText from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import Toast from 'primevue/toast'
import ConfirmDialog from 'primevue/confirmdialog'
import { useToast } from 'primevue/usetoast'
import { useConfirm } from 'primevue/useconfirm'
const toast = useToast()
const confirm = useConfirm()
const loading = ref(false)
const rows = ref([])
const showDlg = ref(false)
const saving = ref(false)
const isEdit = ref(false)
const form = ref({
id: null,
key: '',
descricao: ''
})
const hasCreatedAt = computed(() => rows.value?.length && 'created_at' in rows.value[0])
async function fetchAll () {
loading.value = true
const { data, error } = await supabase
.from('features')
.select('*')
.order('key', { ascending: true })
loading.value = false
if (error) {
toast.add({ severity: 'error', summary: 'Erro', detail: error.message, life: 4500 })
return
}
rows.value = data || []
}
function openCreate () {
isEdit.value = false
form.value = { id: null, key: '', descricao: '' }
showDlg.value = true
}
function openEdit (row) {
isEdit.value = true
form.value = {
id: row.id,
key: row.key ?? '',
descricao: row.descricao ?? ''
}
showDlg.value = true
}
function validate () {
const k = String(form.value.key || '').trim()
if (!k) {
toast.add({
severity: 'warn',
summary: 'Atenção',
detail: 'Informe a key da feature.',
life: 3000
})
return false
}
const d = String(form.value.descricao || '').trim()
// 🔒 evita duplicidade no frontend (exceto no próprio registro em edição)
const exists = rows.value.some(r =>
String(r.key).trim() === k && r.id !== form.value.id
)
if (exists) {
toast.add({
severity: 'warn',
summary: 'Key já existente',
detail: 'Já existe uma feature com essa key. Use outra.',
life: 3000
})
return false
}
form.value.key = k
form.value.descricao = d
return true
}
function isUniqueViolation (err) {
if (!err) return false
if (err.code === '23505') return true
const msg = String(err.message || '')
return msg.includes('duplicate key value') || msg.includes('unique constraint')
}
async function save () {
if (!validate()) return
saving.value = true
try {
if (isEdit.value) {
const { error } = await supabase
.from('features')
.update({
key: form.value.key,
descricao: form.value.descricao
})
.eq('id', form.value.id)
if (error) throw error
toast.add({ severity: 'success', summary: 'Ok', detail: 'Feature atualizada.', life: 2500 })
} else {
const { error } = await supabase
.from('features')
.insert({
key: form.value.key,
descricao: form.value.descricao
})
if (error) throw error
toast.add({ severity: 'success', summary: 'Ok', detail: 'Feature criada.', life: 2500 })
}
showDlg.value = false
await fetchAll()
} catch (e) {
if (isUniqueViolation(e)) {
toast.add({
severity: 'warn',
summary: 'Key já existente',
detail: 'Já existe uma feature com essa key. Use outra.',
life: 3500
})
} else {
toast.add({
severity: 'error',
summary: 'Erro',
detail: e.message || String(e),
life: 4500
})
}
} finally {
saving.value = false
}
}
function askDelete (row) {
confirm.require({
message: `Excluir a feature "${row.key}"?`,
header: 'Confirmar exclusão',
icon: 'pi pi-exclamation-triangle',
acceptClass: 'p-button-danger',
accept: () => doDelete(row)
})
}
async function doDelete (row) {
const { error } = await supabase
.from('features')
.delete()
.eq('id', row.id)
if (error) {
toast.add({ severity: 'error', summary: 'Erro', detail: error.message, life: 4500 })
return
}
toast.add({ severity: 'success', summary: 'Ok', detail: 'Feature excluída.', life: 2500 })
await fetchAll()
}
onMounted(fetchAll)
</script>
<template>
<Toast />
<ConfirmDialog />
<div class="p-4">
<Toolbar class="mb-4">
<template #start>
<div class="flex flex-col">
<div class="text-xl font-semibold leading-none">Features</div>
<small class="text-color-secondary mt-1">
Funcionalidades que podem ser ativadas por plano.
</small>
</div>
</template>
<template #end>
<div class="flex items-center gap-2">
<Button label="Atualizar" icon="pi pi-refresh" severity="secondary" outlined :loading="loading" @click="fetchAll" />
<Button label="Adicionar" icon="pi pi-plus" @click="openCreate" />
</div>
</template>
</Toolbar>
<DataTable :value="rows" dataKey="id" :loading="loading" stripedRows responsiveLayout="scroll">
<Column field="key" header="Key" sortable />
<Column field="descricao" header="Descrição" sortable style="min-width: 22rem">
<template #body="{ data }">
<!-- Tooltip opcional: se v-tooltip estiver habilitado no app, vai ficar ótimo.
Se não estiver, é remover o v-tooltip que segue normal. -->
<div
class="max-w-[520px] whitespace-nowrap overflow-hidden text-ellipsis text-color-secondary"
v-tooltip.top="data.descricao || ''"
>
{{ data.descricao || '-' }}
</div>
</template>
</Column>
<Column v-if="hasCreatedAt" field="created_at" header="Criado em" sortable />
<Column header="Ações" style="width: 12rem">
<template #body="{ data }">
<div class="flex gap-2">
<Button icon="pi pi-pencil" severity="secondary" outlined @click="openEdit(data)" />
<Button icon="pi pi-trash" severity="danger" outlined @click="askDelete(data)" />
</div>
</template>
</Column>
</DataTable>
<Dialog v-model:visible="showDlg" modal :header="isEdit ? 'Editar feature' : 'Nova feature'" :style="{ width: '620px' }">
<div class="flex flex-col gap-4">
<div>
<label class="block mb-2">Key</label>
<InputText v-model="form.key" class="w-full" placeholder="ex: online_scheduling.manage" />
</div>
<div>
<label class="block mb-2">Descrição</label>
<Textarea
v-model="form.descricao"
class="w-full"
:autoResize="true"
rows="3"
placeholder="Explique em linguagem humana o que essa feature habilita..."
/>
<small class="text-color-secondary block mt-2">
Dica: isso aparece no admin e ajuda a documentar o que cada feature faz.
</small>
</div>
</div>
<template #footer>
<Button label="Cancelar" severity="secondary" outlined @click="showDlg = false" />
<Button :label="isEdit ? 'Salvar' : 'Criar'" icon="pi pi-check" :loading="saving" @click="save" />
</template>
</Dialog>
</div>
</template>