Files
matosbox/frontend/pages/categories/index.vue

213 lines
5.7 KiB
Vue

<template>
<main class="container">
<h1>{{ t('nav.categories') }}</h1>
<p v-if="errorMessage">{{ errorMessage }}</p>
<section class="card" style="margin-bottom: 16px;">
<h2>{{ t('filters.name') }}</h2>
<div style="display: grid; gap: 8px;">
<input v-model="filterNom" :placeholder="t('placeholders.name')" />
<label>
{{ t('filters.limit') }}
<select v-model.number="limit">
<option :value="10">10</option>
<option :value="25">25</option>
<option :value="50">50</option>
</select>
</label>
<div style="display: flex; gap: 8px;">
<button class="card" type="button" @click="resetFilters">
{{ t('filters.reset') }}
</button>
</div>
</div>
</section>
<CategorieForm
v-model="form"
:saving="saving"
:message="message"
:mode="editingId ? 'edit' : 'create'"
@save="saveCategorie"
@cancel="resetForm"
/>
<p v-if="pending">{{ t('states.loading') }}</p>
<p v-else-if="items.length === 0">{{ t('states.empty') }}</p>
<section v-else class="grid">
<article v-for="item in items" :key="item.id" class="card">
<h3>{{ item.nom }}</h3>
<p v-if="item.slug">{{ t('labels.slug') }}: {{ item.slug }}</p>
<p v-if="item.icone">{{ t('labels.icone') }}: {{ item.icone }}</p>
<small v-if="item.parent_id">{{ t('labels.parent') }}: {{ item.parent_id }}</small>
<div style="margin-top: 8px; display: flex; gap: 8px;">
<button class="card" type="button" @click="editCategorie(item)">{{ t('actions.edit') }}</button>
<button class="card" type="button" @click="confirmDelete(item.id)">{{ t('actions.delete') }}</button>
</div>
</article>
</section>
<section v-if="items.length" class="card" style="margin-top: 16px;">
<h2>{{ t('tree.title') }}</h2>
<TreeList :items="items" />
</section>
<section class="card" style="margin-top: 16px;">
<p>{{ t('pagination.page') }} {{ page }} / {{ totalPages }}</p>
<div style="display: flex; gap: 8px;">
<button class="card" type="button" :disabled="page <= 1" @click="page--">
{{ t('pagination.prev') }}
</button>
<button class="card" type="button" :disabled="page >= totalPages" @click="page++">
{{ t('pagination.next') }}
</button>
</div>
</section>
<ConfirmDialog
:open="confirmOpen"
:title="t('confirm.deleteCategorieTitle')"
:message="t('confirm.deleteCategorieMessage')"
@confirm="runConfirm"
@cancel="closeConfirm"
/>
</main>
</template>
<script setup lang="ts">
type Categorie = {
id: string
nom: string
parent_id?: string | null
slug?: string | null
icone?: string | null
}
const { apiBase, getErrorMessage } = useApi()
const { t } = useI18n()
const page = ref(1)
const limit = ref(50)
const filterNom = ref('')
const { data, pending, error, refresh } = await useFetch<{
items: Categorie[]
meta?: { total: number; page: number; limit: number }
}>(`${apiBase}/categories`, {
query: {
page,
limit,
nom: filterNom
},
watch: [page, limit, filterNom]
})
const items = computed(() => data.value?.items ?? [])
const total = computed(() => data.value?.meta?.total ?? items.value.length)
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / limit.value)))
const errorMessage = computed(() =>
error.value ? getErrorMessage(error.value, t('messages.loadError')) : ''
)
const saving = ref(false)
const message = ref('')
const editingId = ref<string | null>(null)
const confirmOpen = ref(false)
const confirmAction = ref<null | (() => Promise<void>)>(null)
const form = ref({
nom: '',
parent_id: '',
slug: '',
icone: ''
})
watch([filterNom, limit], () => {
page.value = 1
})
const resetFilters = () => {
filterNom.value = ''
limit.value = 50
}
const resetForm = () => {
editingId.value = null
form.value = { nom: '', parent_id: '', slug: '', icone: '' }
}
const editCategorie = (item: Categorie) => {
editingId.value = item.id
form.value = {
nom: item.nom,
parent_id: item.parent_id || '',
slug: item.slug || '',
icone: item.icone || ''
}
}
const saveCategorie = async () => {
message.value = ''
if (!form.value.nom) {
message.value = t('messages.requiredName')
return
}
saving.value = true
try {
const payload = {
nom: form.value.nom,
parent_id: form.value.parent_id || undefined,
slug: form.value.slug || undefined,
icone: form.value.icone || undefined
}
if (editingId.value) {
await $fetch(`${apiBase}/categories/${editingId.value}`, {
method: 'PUT',
body: payload
})
message.value = t('messages.updated')
} else {
await $fetch(`${apiBase}/categories`, {
method: 'POST',
body: payload
})
message.value = t('messages.created')
}
resetForm()
await refresh()
} catch (err) {
message.value = getErrorMessage(err, t('messages.saveError'))
} finally {
saving.value = false
}
}
const deleteCategorie = async (id: string) => {
message.value = ''
try {
await $fetch(`${apiBase}/categories/${id}`, { method: 'DELETE' })
message.value = t('messages.deleted')
await refresh()
} catch (err) {
message.value = getErrorMessage(err, t('messages.deleteError'))
}
}
const confirmDelete = (id: string) => {
confirmAction.value = () => deleteCategorie(id)
confirmOpen.value = true
}
const closeConfirm = () => {
confirmOpen.value = false
confirmAction.value = null
}
const runConfirm = async () => {
if (confirmAction.value) {
await confirmAction.value()
}
closeConfirm()
}
</script>