Files
don_confiao_frontend/src/components/CatalogueImagesManagement.vue

384 lines
9.4 KiB
Vue

<template>
<v-container fluid>
<v-row align="center">
<v-col cols="12" md="6">
<h1 class="text-h4">Imágenes de Catálogo</h1>
</v-col>
<v-col cols="12" md="6" class="text-md-right">
<v-btn
color="primary"
variant="elevated"
prepend-icon="mdi-plus"
@click="openCreateDialog"
>
Agregar Imagen
</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-card>
<v-data-table
:headers="headers"
:items="images"
:loading="loading"
density="compact"
item-value="id"
items-per-page="25"
:items-per-page-options="[10, 25, 50, 100]"
>
<template #item.image="{ item }">
<v-img
:src="item.image"
max-width="60"
aspect-ratio="1"
cover
class="rounded"
/>
</template>
<template #item.product="{ item }">
{{ productMap[item.product] || `ID: ${item.product}` }}
</template>
<template #item.uploaded_at="{ item }">
{{ formatDate(item.uploaded_at) }}
</template>
<template #item.actions="{ item }">
<v-btn
icon
size="small"
variant="text"
@click="openEditDialog(item)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn
icon
size="small"
variant="text"
color="error"
@click="openDeleteDialog(item)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
<template #loading>
<v-skeleton-loader type="table-row@10" />
</template>
<template #no-data>
<v-alert type="info" variant="tonal" class="my-4">
No hay imágenes de catálogo
</v-alert>
</template>
</v-data-table>
</v-card>
</v-col>
</v-row>
<v-dialog v-model="dialog.show" max-width="500" persistent>
<v-card>
<v-card-title>
{{ dialog.isEdit ? 'Editar' : 'Agregar' }} Imagen de Catálogo
</v-card-title>
<v-card-text>
<v-form ref="formRef">
<v-select
v-model="form.product"
:items="productOptions"
label="Producto"
:rules="[(v) => !!v || 'Seleccione un producto']"
item-title="name"
item-value="id"
variant="outlined"
:disabled="dialog.isEdit"
class="mb-3"
/>
<v-file-input
ref="fileInputRef"
label="Imagen"
accept="image/*"
:rules="[(v) => !!v || 'Seleccione una imagen']"
variant="outlined"
prepend-icon="mdi-camera"
class="mb-3"
@update:model-value="onFileSelected"
/>
<v-img
v-if="form.preview"
:src="form.preview"
max-height="200"
contain
class="mb-3 rounded"
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="closeDialog">Cancelar</v-btn>
<v-btn
color="primary"
variant="elevated"
:loading="submitting"
:disabled="submitting"
@click="submitForm"
>
{{ dialog.isEdit ? 'Actualizar' : 'Agregar' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="deleteDialog.show" max-width="400">
<v-card>
<v-card-title>Confirmar Eliminación</v-card-title>
<v-card-text>
¿Está seguro de eliminar esta imagen de catálogo?
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="deleteDialog.show = false">
Cancelar
</v-btn>
<v-btn
color="error"
variant="elevated"
:loading="deleting"
:disabled="deleting"
@click="confirmDelete"
>
Eliminar
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
location="top"
>
{{ snackbar.message }}
<template #actions>
<v-btn variant="text" @click="snackbar.show = false">Cerrar</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script setup>
import { ref, inject, onMounted, computed, watch } from "vue";
const api = inject("api");
const images = ref([]);
const products = ref([]);
const loading = ref(false);
const submitting = ref(false);
const deleting = ref(false);
const formRef = ref(null);
const snackbar = ref({ show: false, message: "", color: "success" });
const headers = [
{ title: "ID", key: "id", sortable: true },
{ title: "Producto", key: "product", sortable: true },
{ title: "Imagen", key: "image", sortable: false },
{ title: "Subida el", key: "uploaded_at", sortable: true },
{ title: "Acciones", key: "actions", sortable: false },
];
const dialog = ref({
show: false,
isEdit: false,
editingItem: null,
});
const fileInputRef = ref(null);
const selectedFile = ref(null);
const form = ref({
product: null,
preview: null,
});
const deleteDialog = ref({
show: false,
item: null,
});
let previewObjectUrl = null;
const productMap = computed(() => {
const map = {};
for (const p of products.value) {
map[p.id] = p.name;
}
return map;
});
const productOptions = computed(() => {
return products.value.map((p) => ({
name: p.name,
id: p.id,
}));
});
watch(
selectedFile,
(file) => {
if (previewObjectUrl) {
URL.revokeObjectURL(previewObjectUrl);
previewObjectUrl = null;
}
if (file) {
previewObjectUrl = URL.createObjectURL(file);
form.value.preview = previewObjectUrl;
} else if (dialog.value.isEdit && dialog.value.editingItem) {
form.value.preview = dialog.value.editingItem.image;
} else {
form.value.preview = null;
}
},
);
function onFileSelected(file) {
selectedFile.value = file;
}
function formatDate(dateStr) {
if (!dateStr) return "-";
const d = new Date(dateStr);
return d.toLocaleString("es-CO");
}
async function loadImages() {
loading.value = true;
try {
const data = await api.getCatalogueImages();
images.value = data;
} catch (error) {
console.error("Error al cargar imágenes:", error);
showSnackbar("Error al cargar imágenes", "error");
} finally {
loading.value = false;
}
}
async function loadProducts() {
try {
const data = await api.getProducts("all");
products.value = data;
} catch (error) {
console.error("Error al cargar productos:", error);
}
}
function openCreateDialog() {
dialog.value = { show: true, isEdit: false, editingItem: null };
selectedFile.value = null;
form.value = { product: null, preview: null };
}
function openEditDialog(item) {
dialog.value = { show: true, isEdit: true, editingItem: item };
selectedFile.value = null;
form.value = {
product: item.product,
preview: item.image,
};
}
function closeDialog() {
if (previewObjectUrl) {
URL.revokeObjectURL(previewObjectUrl);
previewObjectUrl = null;
}
selectedFile.value = null;
dialog.value.show = false;
dialog.value.isEdit = false;
dialog.value.editingItem = null;
form.value = { product: null, preview: null };
}
async function submitForm() {
const formComponent = formRef.value;
if (formComponent) {
const { valid } = await formComponent.validate();
if (!valid) return;
}
submitting.value = true;
try {
const fd = new FormData();
fd.append("product", form.value.product);
if (selectedFile.value) {
fd.append("image", selectedFile.value);
}
if (dialog.value.isEdit) {
await api.updateCatalogueImage(dialog.value.editingItem.id, fd);
showSnackbar("Imagen actualizada exitosamente", "success");
} else {
await api.createCatalogueImage(fd);
showSnackbar("Imagen agregada exitosamente", "success");
}
closeDialog();
await loadImages();
} catch (error) {
console.error("Error al guardar imagen:", error);
showSnackbar("Error al guardar imagen", "error");
} finally {
submitting.value = false;
}
}
function openDeleteDialog(item) {
deleteDialog.value = { show: true, item };
}
async function confirmDelete() {
deleting.value = true;
try {
await api.deleteCatalogueImage(deleteDialog.value.item.id);
showSnackbar("Imagen eliminada exitosamente", "success");
deleteDialog.value.show = false;
await loadImages();
} catch (error) {
console.error("Error al eliminar imagen:", error);
showSnackbar("Error al eliminar imagen", "error");
} finally {
deleting.value = false;
}
}
function showSnackbar(message, color) {
snackbar.value = { show: true, message, color };
}
onMounted(() => {
loadImages();
loadProducts();
});
</script>
<style scoped>
.text-md-right {
text-align: right;
}
@media (max-width: 960px) {
.text-md-right {
text-align: left;
}
}
</style>