first commit
This commit is contained in:
@@ -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, é só 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>
|
||||
Reference in New Issue
Block a user