Added dismissed card styling and reorganized error/empty state rendering in moderation. Added fixed width for catalog action buttons.
454 lines
16 KiB
Vue
454 lines
16 KiB
Vue
<script setup lang="ts">
|
|
import type { Category, Team } from '~/types/api'
|
|
|
|
definePageMeta({
|
|
middleware: 'auth',
|
|
auth: { role: 'ADMIN' }
|
|
})
|
|
|
|
useHead({ title: 'Categorie e Team - Admin' })
|
|
|
|
const { createCategory, updateCategory, deleteCategory, getTeams, createTeam, updateTeam, deleteTeam, getAreas } = useAdmin()
|
|
const { getCategories } = useReports()
|
|
|
|
const { data: categoriesRes, refresh: refreshCategories } = await useAsyncData('report-categories-admin-view', () => getCategories())
|
|
const { data: teamsRes, refresh: refreshTeams } = await useAsyncData('admin-teams', () => getTeams())
|
|
const { data: areasRes } = await useAsyncData('admin-team-areas', () => getAreas())
|
|
|
|
const categoryForm = reactive({
|
|
name: '',
|
|
description: '',
|
|
})
|
|
|
|
const editingCategoryId = ref<number | null>(null)
|
|
|
|
const teamForm = reactive({
|
|
name: '',
|
|
description: '',
|
|
team_category_id: null as number | null,
|
|
area_id: null as number | null,
|
|
})
|
|
|
|
const editingTeamId = ref<number | null>(null)
|
|
|
|
const isSavingCategory = ref(false)
|
|
const isSavingTeam = ref(false)
|
|
const categoryError = ref('')
|
|
const teamError = ref('')
|
|
const deletingCategoryId = ref<number | null>(null)
|
|
const deletingTeamId = ref<number | null>(null)
|
|
const categoryPendingDelete = ref<Category | null>(null)
|
|
const teamPendingDelete = ref<Team | null>(null)
|
|
|
|
const categories = computed(() => categoriesRes.value?.categories || [])
|
|
const teams = computed(() => teamsRes.value?.teams || [])
|
|
const areas = computed(() => areasRes.value?.areas || [])
|
|
const categoryDeleteMessage = computed(() =>
|
|
categoryPendingDelete.value ? `Vuoi eliminare categoria "${categoryPendingDelete.value.name}"? Operazione irreversibile.` : ''
|
|
)
|
|
const teamDeleteMessage = computed(() =>
|
|
teamPendingDelete.value ? `Vuoi eliminare team "${teamPendingDelete.value.name}"? Operazione irreversibile.` : ''
|
|
)
|
|
|
|
const resetCategoryForm = () => {
|
|
editingCategoryId.value = null
|
|
categoryForm.name = ''
|
|
categoryForm.description = ''
|
|
}
|
|
|
|
const resetTeamForm = () => {
|
|
editingTeamId.value = null
|
|
teamForm.name = ''
|
|
teamForm.description = ''
|
|
teamForm.team_category_id = null
|
|
teamForm.area_id = null
|
|
}
|
|
|
|
const editCategory = (category: Category) => {
|
|
editingCategoryId.value = category.id
|
|
categoryForm.name = category.name
|
|
categoryForm.description = category.description || ''
|
|
}
|
|
|
|
const editTeam = (team: Team) => {
|
|
editingTeamId.value = team.id
|
|
teamForm.name = team.name
|
|
teamForm.description = team.description || ''
|
|
teamForm.team_category_id = team.team_category_id
|
|
teamForm.area_id = team.area_id ?? null
|
|
}
|
|
|
|
const submitCategory = async () => {
|
|
categoryError.value = ''
|
|
isSavingCategory.value = true
|
|
|
|
try {
|
|
const payload = {
|
|
name: categoryForm.name.trim(),
|
|
description: categoryForm.description.trim() || undefined,
|
|
}
|
|
if (editingCategoryId.value) {
|
|
await updateCategory(editingCategoryId.value, payload)
|
|
} else {
|
|
await createCategory(payload)
|
|
}
|
|
resetCategoryForm()
|
|
await refreshCategories()
|
|
} catch (error: any) {
|
|
categoryError.value = error?.data?.message || 'Creazione categoria non riuscita.'
|
|
} finally {
|
|
isSavingCategory.value = false
|
|
}
|
|
}
|
|
|
|
const submitTeam = async () => {
|
|
if (!teamForm.team_category_id) return
|
|
|
|
teamError.value = ''
|
|
isSavingTeam.value = true
|
|
|
|
try {
|
|
const payload = {
|
|
name: teamForm.name.trim(),
|
|
description: teamForm.description.trim() || undefined,
|
|
team_category_id: teamForm.team_category_id,
|
|
area_id: teamForm.area_id || undefined,
|
|
}
|
|
if (editingTeamId.value) {
|
|
await updateTeam(editingTeamId.value, payload)
|
|
} else {
|
|
await createTeam(payload)
|
|
}
|
|
resetTeamForm()
|
|
await refreshTeams()
|
|
} catch (error: any) {
|
|
teamError.value = error?.data?.message || 'Creazione team non riuscita.'
|
|
} finally {
|
|
isSavingTeam.value = false
|
|
}
|
|
}
|
|
|
|
const toggleCategoryActive = async (category: Category) => {
|
|
try {
|
|
await updateCategory(category.id, { active: category.active === false })
|
|
await refreshCategories()
|
|
} catch (error: any) {
|
|
categoryError.value = error?.data?.message || 'Aggiornamento categoria non riuscito.'
|
|
}
|
|
}
|
|
|
|
const askDeleteCategory = (category: Category) => {
|
|
categoryPendingDelete.value = category
|
|
}
|
|
|
|
const closeDeleteCategoryModal = () => {
|
|
if (deletingCategoryId.value) return
|
|
categoryPendingDelete.value = null
|
|
}
|
|
|
|
const handleDeleteCategory = async () => {
|
|
if (!categoryPendingDelete.value) return
|
|
|
|
const category = categoryPendingDelete.value
|
|
|
|
deletingCategoryId.value = category.id
|
|
categoryError.value = ''
|
|
|
|
try {
|
|
await deleteCategory(category.id)
|
|
if (editingCategoryId.value === category.id) {
|
|
resetCategoryForm()
|
|
}
|
|
await refreshCategories()
|
|
categoryPendingDelete.value = null
|
|
} catch (error: any) {
|
|
categoryError.value = error?.data?.message || 'Eliminazione categoria non riuscita.'
|
|
} finally {
|
|
deletingCategoryId.value = null
|
|
}
|
|
}
|
|
|
|
const toggleTeamActive = async (team: Team) => {
|
|
try {
|
|
await updateTeam(team.id, { active: team.active === false })
|
|
await refreshTeams()
|
|
} catch (error: any) {
|
|
teamError.value = error?.data?.message || 'Aggiornamento team non riuscito.'
|
|
}
|
|
}
|
|
|
|
const askDeleteTeam = (team: Team) => {
|
|
teamPendingDelete.value = team
|
|
}
|
|
|
|
const closeDeleteTeamModal = () => {
|
|
if (deletingTeamId.value) return
|
|
teamPendingDelete.value = null
|
|
}
|
|
|
|
const handleDeleteTeam = async () => {
|
|
if (!teamPendingDelete.value) return
|
|
|
|
const team = teamPendingDelete.value
|
|
|
|
deletingTeamId.value = team.id
|
|
teamError.value = ''
|
|
|
|
try {
|
|
await deleteTeam(team.id)
|
|
if (editingTeamId.value === team.id) {
|
|
resetTeamForm()
|
|
}
|
|
await refreshTeams()
|
|
teamPendingDelete.value = null
|
|
} catch (error: any) {
|
|
teamError.value = error?.data?.message || 'Eliminazione team non riuscita.'
|
|
} finally {
|
|
deletingTeamId.value = null
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="container my-5">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
|
|
<div>
|
|
<h1 class="h3 font-weight-bold mb-1">Categorie e Team</h1>
|
|
<p class="text-muted mb-0">Gestione dati di riferimento per segnalazioni e interventi.</p>
|
|
</div>
|
|
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm">Torna al pannello admin</NuxtLink>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<div class="col-12 col-xl-5">
|
|
<div class="card card-bg border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
<h2 class="h5 mb-0">Categorie</h2>
|
|
<span class="badge bg-light text-dark border">{{ categories.length }}</span>
|
|
</div>
|
|
|
|
<div v-if="categoryError" class="alert alert-danger py-2" role="alert">
|
|
{{ categoryError }}
|
|
</div>
|
|
|
|
<form class="mb-4" @submit.prevent="submitCategory">
|
|
<div class="mb-3">
|
|
<label class="form-label">Nome categoria</label>
|
|
<input v-model="categoryForm.name" type="text" class="form-control" required />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Descrizione</label>
|
|
<textarea v-model="categoryForm.description" class="form-control" rows="3"></textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary" :disabled="isSavingCategory || !categoryForm.name.trim()">
|
|
<span v-if="isSavingCategory" class="spinner-border spinner-border-sm me-2"></span>
|
|
{{ editingCategoryId ? 'Salva categoria' : 'Crea categoria' }}
|
|
</button>
|
|
<button
|
|
v-if="editingCategoryId"
|
|
type="button"
|
|
class="btn btn-outline-secondary ms-2"
|
|
@click="resetCategoryForm"
|
|
>
|
|
Annulla
|
|
</button>
|
|
</form>
|
|
|
|
<div class="table-responsive report-table-wrapper">
|
|
<table class="table table-hover report-table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Nome</th>
|
|
<th>Stato</th>
|
|
<th class="text-end">Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="category in categories" :key="category.id">
|
|
<td class="report-table-id">#{{ category.id }}</td>
|
|
<td>
|
|
<strong>{{ category.name }}</strong>
|
|
<div v-if="category.description" class="small text-muted mt-1">{{ category.description }}</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge" :class="category.active === false ? 'bg-secondary' : 'bg-success'">
|
|
{{ category.active === false ? 'Inattiva' : 'Attiva' }}
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-primary me-2" @click="editCategory(category)">
|
|
Modifica
|
|
</button>
|
|
<button
|
|
class="btn btn-sm"
|
|
:class="category.active === false ? 'btn-outline-success' : 'btn-outline-secondary'"
|
|
@click="toggleCategoryActive(category)"
|
|
>
|
|
{{ category.active === false ? 'Attiva' : 'Disattiva' }}
|
|
</button>
|
|
<button
|
|
class="btn btn-sm btn-outline-danger ms-2"
|
|
:disabled="deletingCategoryId === category.id"
|
|
@click="askDeleteCategory(category)"
|
|
>
|
|
<span v-if="deletingCategoryId === category.id" class="spinner-border spinner-border-sm me-1"></span>
|
|
Elimina
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="categories.length === 0">
|
|
<td colspan="4" class="text-muted">Nessuna categoria configurata.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-xl-7">
|
|
<div class="card card-bg border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
<h2 class="h5 mb-0">Team di intervento</h2>
|
|
<span class="badge bg-light text-dark border">{{ teams.length }}</span>
|
|
</div>
|
|
|
|
<div v-if="teamError" class="alert alert-danger py-2" role="alert">
|
|
{{ teamError }}
|
|
</div>
|
|
|
|
<form class="row g-3 mb-4" @submit.prevent="submitTeam">
|
|
<div class="col-12 col-md-6">
|
|
<label class="form-label">Nome team</label>
|
|
<input v-model="teamForm.name" type="text" class="form-control" required />
|
|
</div>
|
|
<div class="col-12 col-md-6">
|
|
<label class="form-label">Categoria coperta</label>
|
|
<select v-model.number="teamForm.team_category_id" class="form-select" required>
|
|
<option :value="null">Seleziona categoria</option>
|
|
<option v-for="category in categories" :key="category.id" :value="category.id">
|
|
{{ category.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-12 col-md-6">
|
|
<label class="form-label">Area</label>
|
|
<select v-model.number="teamForm.area_id" class="form-select">
|
|
<option :value="null">Tutte le aree</option>
|
|
<option v-for="area in areas" :key="area.id" :value="area.id">
|
|
{{ area.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-12 col-md-6">
|
|
<label class="form-label">Descrizione</label>
|
|
<input v-model="teamForm.description" type="text" class="form-control" />
|
|
</div>
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary" :disabled="isSavingTeam || !teamForm.name.trim() || !teamForm.team_category_id">
|
|
<span v-if="isSavingTeam" class="spinner-border spinner-border-sm me-2"></span>
|
|
{{ editingTeamId ? 'Salva team' : 'Crea team' }}
|
|
</button>
|
|
<button
|
|
v-if="editingTeamId"
|
|
type="button"
|
|
class="btn btn-outline-secondary ms-2"
|
|
@click="resetTeamForm"
|
|
>
|
|
Annulla
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="table-responsive report-table-wrapper">
|
|
<table class="table table-hover report-table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Team</th>
|
|
<th>Categoria</th>
|
|
<th>Area</th>
|
|
<th>Stato</th>
|
|
<th class="text-end">Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="team in teams" :key="team.id">
|
|
<td class="report-table-id">#{{ team.id }}</td>
|
|
<td>
|
|
<strong>{{ team.name }}</strong>
|
|
<div v-if="team.description" class="small text-muted mt-1">{{ team.description }}</div>
|
|
</td>
|
|
<td>{{ team.category_name || '-' }}</td>
|
|
<td>{{ team.area_name || 'Tutte' }}</td>
|
|
<td>
|
|
<span class="badge" :class="team.active === false ? 'bg-secondary' : 'bg-success'">
|
|
{{ team.active === false ? 'Inattivo' : 'Attivo' }}
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-primary me-2" @click="editTeam(team)">
|
|
Modifica
|
|
</button>
|
|
<button
|
|
class="btn btn-sm"
|
|
:class="team.active === false ? 'btn-outline-success' : 'btn-outline-secondary'"
|
|
@click="toggleTeamActive(team)"
|
|
>
|
|
{{ team.active === false ? 'Attiva' : 'Disattiva' }}
|
|
</button>
|
|
<button
|
|
class="btn btn-sm btn-outline-danger ms-2"
|
|
:disabled="deletingTeamId === team.id"
|
|
@click="askDeleteTeam(team)"
|
|
>
|
|
<span v-if="deletingTeamId === team.id" class="spinner-border spinner-border-sm me-1"></span>
|
|
Elimina
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="teams.length === 0">
|
|
<td colspan="6" class="text-muted">Nessun team configurato.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<ConfirmModal
|
|
:open="categoryPendingDelete !== null"
|
|
title="Elimina categoria"
|
|
:message="categoryDeleteMessage"
|
|
confirm-label="Elimina categoria"
|
|
confirm-class="btn-danger"
|
|
:pending="deletingCategoryId !== null"
|
|
@cancel="closeDeleteCategoryModal"
|
|
@confirm="handleDeleteCategory"
|
|
/>
|
|
|
|
<ConfirmModal
|
|
:open="teamPendingDelete !== null"
|
|
title="Elimina team"
|
|
:message="teamDeleteMessage"
|
|
confirm-label="Elimina team"
|
|
confirm-class="btn-danger"
|
|
:pending="deletingTeamId !== null"
|
|
@cancel="closeDeleteTeamModal"
|
|
@confirm="handleDeleteTeam"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
.text-end .btn {
|
|
margin: 0 !important;
|
|
width: 120px;
|
|
}
|
|
</style>
|