- Added approval and rejection dialogs for technical and financial requisitions in Requisitions.vue. - Updated requisition statuses to include 'pending_technical', 'rejected_technical', 'pending_financial', and 'rejected_financial'. - Enhanced requisition store to handle approval and rejection logic, including saving to localStorage. - Modified requisition interface to include approval and rejection records. - Updated initial requisition data to reflect new approval statuses and added comments field for requisition items.
773 lines
33 KiB
Vue
773 lines
33 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useRouter, useRoute } from 'vue-router';
|
|
import { useToast } from 'primevue/usetoast';
|
|
import Card from 'primevue/card';
|
|
import Button from 'primevue/button';
|
|
import InputText from 'primevue/inputtext';
|
|
import Dropdown from 'primevue/dropdown';
|
|
import Textarea from 'primevue/textarea';
|
|
import InputNumber from 'primevue/inputnumber';
|
|
import DataTable from 'primevue/datatable';
|
|
import Column from 'primevue/column';
|
|
import Dialog from 'primevue/dialog';
|
|
import Toast from 'primevue/toast';
|
|
import { useRequisitionStore } from './stores/requisitionStore';
|
|
import type { RequisitionItem } from './types/requisition.interfaces';
|
|
import { DepartmentsService } from '@/modules/rh/services/departments.services';
|
|
import type { Department } from '@/modules/rh/types/departments.interface';
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
const toast = useToast();
|
|
const requisitionStore = useRequisitionStore();
|
|
const departmentsService = new DepartmentsService();
|
|
|
|
const isEditMode = ref(false);
|
|
const requisitionId = ref<number | null>(null);
|
|
|
|
const form = ref({
|
|
folio: '',
|
|
requester: 'Edgar Mendoza',
|
|
status: 'Borrador',
|
|
priority: 'medium',
|
|
department: '',
|
|
justification: ''
|
|
});
|
|
|
|
const items = ref<RequisitionItem[]>([]);
|
|
const isSaving = ref(false);
|
|
|
|
const priorityOptions = [
|
|
{ label: 'Baja', value: 'low' },
|
|
{ label: 'Media', value: 'medium' },
|
|
{ label: 'Alta', value: 'high' },
|
|
{ label: 'Urgente', value: 'urgent' }
|
|
];
|
|
|
|
// Cargar departamentos desde API
|
|
const departments = ref<Department[]>([]);
|
|
const loadingDepartments = ref(false);
|
|
|
|
const departmentOptions = computed(() => {
|
|
return departments.value.map(dept => ({
|
|
label: dept.name,
|
|
value: dept.id.toString()
|
|
}));
|
|
});
|
|
|
|
const commentsDialogVisible = ref(false);
|
|
const selectedItemIndex = ref<number | null>(null);
|
|
const tempComments = ref('');
|
|
|
|
const addItem = () => {
|
|
items.value.push({
|
|
id: items.value.length + 1,
|
|
product: '',
|
|
quantity: 0,
|
|
unit: '',
|
|
unitPrice: 0,
|
|
comments: ''
|
|
});
|
|
};
|
|
|
|
const openCommentsDialog = (index: number) => {
|
|
selectedItemIndex.value = index;
|
|
const item = items.value[index];
|
|
if (item) {
|
|
tempComments.value = item.comments || '';
|
|
}
|
|
commentsDialogVisible.value = true;
|
|
};
|
|
|
|
const saveComments = () => {
|
|
if (selectedItemIndex.value !== null) {
|
|
const item = items.value[selectedItemIndex.value];
|
|
if (item) {
|
|
item.comments = tempComments.value;
|
|
}
|
|
}
|
|
commentsDialogVisible.value = false;
|
|
tempComments.value = '';
|
|
selectedItemIndex.value = null;
|
|
};
|
|
|
|
const closeCommentsDialog = () => {
|
|
commentsDialogVisible.value = false;
|
|
tempComments.value = '';
|
|
selectedItemIndex.value = null;
|
|
};
|
|
|
|
const calculateSubtotal = (item: RequisitionItem): number => {
|
|
return item.quantity * item.unitPrice;
|
|
};
|
|
|
|
const calculateTotal = (): number => {
|
|
return items.value.reduce((total: number, item: RequisitionItem) => total + calculateSubtotal(item), 0);
|
|
};
|
|
|
|
const formatCurrency = (value: number): string => {
|
|
return new Intl.NumberFormat('es-MX', {
|
|
style: 'currency',
|
|
currency: 'MXN'
|
|
}).format(value);
|
|
};
|
|
|
|
const removeItem = (index: number) => {
|
|
items.value.splice(index, 1);
|
|
};
|
|
|
|
/* const removeFile = (index: number) => {
|
|
uploadedFiles.value.splice(index, 1);
|
|
};
|
|
|
|
const onFileUpload = (event: any) => {
|
|
const files = event.files;
|
|
files.forEach((file: File) => {
|
|
uploadedFiles.value.push({
|
|
name: file.name,
|
|
size: (file.size / 1024 / 1024).toFixed(2) + ' MB',
|
|
type: file.type.includes('image') ? 'image' : 'pdf'
|
|
});
|
|
});
|
|
}; */
|
|
|
|
const loadRequisition = async () => {
|
|
const id = route.params.id;
|
|
if (id && typeof id === 'string') {
|
|
isEditMode.value = true;
|
|
requisitionId.value = parseInt(id);
|
|
|
|
const requisition = requisitionStore.getRequisitionById(requisitionId.value);
|
|
if (requisition) {
|
|
form.value = {
|
|
folio: requisition.folio,
|
|
requester: requisition.requester,
|
|
status: requisition.status,
|
|
priority: requisition.priority,
|
|
department: requisition.department,
|
|
justification: requisition.justification
|
|
};
|
|
items.value = [...requisition.items];
|
|
} else {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: 'Requisición no encontrada',
|
|
life: 3000
|
|
});
|
|
router.push('/requisitions');
|
|
}
|
|
} else {
|
|
// Modo creación - generar nuevo folio
|
|
const nextNumber = requisitionStore.requisitions.length + 1;
|
|
form.value.folio = `REQ-2024-${String(nextNumber).padStart(3, '0')}`;
|
|
}
|
|
};
|
|
|
|
const validateForm = (): boolean => {
|
|
if (!form.value.priority) {
|
|
toast.add({
|
|
severity: 'warn',
|
|
summary: 'Validación',
|
|
detail: 'La prioridad es requerida',
|
|
life: 3000
|
|
});
|
|
return false;
|
|
}
|
|
|
|
if (!form.value.department) {
|
|
toast.add({
|
|
severity: 'warn',
|
|
summary: 'Validación',
|
|
detail: 'El departamento es requerido',
|
|
life: 3000
|
|
});
|
|
return false;
|
|
}
|
|
|
|
if (items.value.length === 0) {
|
|
toast.add({
|
|
severity: 'warn',
|
|
summary: 'Validación',
|
|
detail: 'Debe agregar al menos un item',
|
|
life: 3000
|
|
});
|
|
return false;
|
|
}
|
|
|
|
const hasInvalidItems = items.value.some((item: RequisitionItem) =>
|
|
!item.product || item.quantity <= 0 || !item.unit || item.unitPrice <= 0
|
|
);
|
|
|
|
if (hasInvalidItems) {
|
|
toast.add({
|
|
severity: 'warn',
|
|
summary: 'Validación',
|
|
detail: 'Todos los items deben tener producto, cantidad, unidad y precio válidos',
|
|
life: 3000
|
|
});
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
router.push('/requisitions');
|
|
};
|
|
|
|
const handleSaveDraft = async () => {
|
|
if (!validateForm()) return;
|
|
|
|
isSaving.value = true;
|
|
try {
|
|
if (isEditMode.value && requisitionId.value) {
|
|
await requisitionStore.updateRequisition(requisitionId.value, form.value, items.value);
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Guardado',
|
|
detail: 'Requisición actualizada correctamente',
|
|
life: 3000
|
|
});
|
|
} else {
|
|
await requisitionStore.createRequisition(form.value, items.value);
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Guardado',
|
|
detail: 'Requisición guardada como borrador',
|
|
life: 3000
|
|
});
|
|
}
|
|
router.push('/requisitions');
|
|
} catch (error: any) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: error.message || 'Error al guardar la requisición',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
isSaving.value = false;
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!validateForm()) return;
|
|
|
|
isSaving.value = true;
|
|
try {
|
|
let reqId: number;
|
|
|
|
if (isEditMode.value && requisitionId.value) {
|
|
await requisitionStore.updateRequisition(requisitionId.value, form.value, items.value);
|
|
reqId = requisitionId.value;
|
|
} else {
|
|
const newReq = await requisitionStore.createRequisition(form.value, items.value);
|
|
reqId = newReq.id;
|
|
}
|
|
|
|
// Enviar a aprobación
|
|
await requisitionStore.submitForApproval(reqId);
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Enviado',
|
|
detail: 'Requisición enviada a aprobación correctamente',
|
|
life: 3000
|
|
});
|
|
router.push('/requisitions');
|
|
} catch (error: any) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: error.message || 'Error al enviar la requisición',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
isSaving.value = false;
|
|
}
|
|
};
|
|
|
|
const loadDepartments = async () => {
|
|
loadingDepartments.value = true;
|
|
try {
|
|
const response = await departmentsService.getDepartments();
|
|
departments.value = response.data;
|
|
} catch (error) {
|
|
console.error('Error al cargar departamentos:', error);
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: 'No se pudieron cargar los departamentos',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
loadingDepartments.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
loadDepartments();
|
|
loadRequisition();
|
|
});
|
|
|
|
const totalItems = () => {
|
|
return items.value.filter((item: RequisitionItem) => item.quantity > 0).length;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="space-y-6 py-4">
|
|
<!-- Header -->
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<Button icon="pi pi-arrow-left" text rounded @click="$router.back()" class="shrink-0" size="small" />
|
|
<h2 class="text-xl md:text-2xl lg:text-3xl font-black text-surface-900 dark:text-white tracking-tight">
|
|
Crear Requisición de Material
|
|
</h2>
|
|
</div>
|
|
<div class="flex items-center gap-3 flex-wrap">
|
|
<Button icon="pi pi-print" label="Imprimir" text size="small" />
|
|
<Button icon="pi pi-share-alt" label="Compartir" text size="small" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Información General -->
|
|
<Card>
|
|
<template #header>
|
|
<div class="px-4 md:px-6 py-3 border-b border-surface-200 dark:border-surface-700 flex flex-col md:flex-row md:items-center justify-between gap-2">
|
|
<div class="flex items-center gap-2">
|
|
<i class="pi pi-info-circle text-primary text-lg"></i>
|
|
<h3 class="font-bold text-base">Información General</h3>
|
|
</div>
|
|
<span class="text-xs text-gray-500">Campos obligatorios marcados con *</span>
|
|
</div>
|
|
</template>
|
|
|
|
<template #content>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">
|
|
Folio
|
|
</label>
|
|
<InputText v-model="form.folio" class="w-full bg-gray-50 dark:bg-gray-900" size="small" />
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">
|
|
Solicitante
|
|
</label>
|
|
<InputText v-model="form.requester" readonly class="w-full bg-gray-50 dark:bg-gray-900" size="small" />
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">
|
|
Estatus
|
|
</label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-blue-400"></span>
|
|
<InputText v-model="form.status" readonly class="w-full bg-gray-50 dark:bg-gray-900 pl-8" size="small" />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">
|
|
Prioridad <span class="text-red-500">*</span>
|
|
</label>
|
|
<Dropdown
|
|
v-model="form.priority"
|
|
:options="priorityOptions"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div class="sm:col-span-2 lg:col-span-4">
|
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">
|
|
Departamento / Centro de Costos <span class="text-red-500">*</span>
|
|
</label>
|
|
<Dropdown
|
|
v-model="form.department"
|
|
:options="departmentOptions"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
placeholder="Seleccionar departamento"
|
|
:loading="loadingDepartments"
|
|
:disabled="loadingDepartments"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div class="sm:col-span-2 lg:col-span-4">
|
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">
|
|
Justificación de la Requisición
|
|
</label>
|
|
<Textarea
|
|
v-model="form.justification"
|
|
placeholder="Describe el motivo y la necesidad de esta requisición..."
|
|
rows="3"
|
|
autoResize
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Items de la Requisición -->
|
|
<Card>
|
|
<template #header>
|
|
<div class="px-4 md:px-6 py-3 border-b border-surface-200 dark:border-surface-700 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
<div class="flex items-center gap-2">
|
|
<i class="pi pi-list text-primary text-lg"></i>
|
|
<h3 class="font-bold text-base">Items de la Requisición</h3>
|
|
</div>
|
|
<Button label="Agregar" icon="pi pi-plus-circle" @click="addItem" size="small" class="w-full sm:w-auto" />
|
|
</div>
|
|
</template>
|
|
|
|
<template #content>
|
|
<!-- Mobile View - Cards -->
|
|
<div class="lg:hidden space-y-4">
|
|
<div v-for="(item, index) in items" :key="item.id"
|
|
class="border border-gray-200 dark:border-gray-800 rounded-lg p-4 space-y-3">
|
|
<div class="flex items-center justify-between border-b pb-2 mb-2">
|
|
<span class="text-sm font-bold text-gray-500">
|
|
Item #{{ String(index + 1).padStart(2, '0') }}
|
|
</span>
|
|
<Button
|
|
icon="pi pi-trash"
|
|
text
|
|
rounded
|
|
severity="danger"
|
|
size="small"
|
|
@click="removeItem(index)"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-500 mb-1">Producto / Servicio</label>
|
|
<InputText
|
|
v-model="item.product"
|
|
placeholder="Nombre del producto o servicio..."
|
|
class="w-full bg-gray-50 dark:bg-gray-900"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-500 mb-1">Cantidad</label>
|
|
<InputNumber v-model="item.quantity" :min="0" class="w-full" size="small" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-500 mb-1">Unidad</label>
|
|
<InputText
|
|
v-model="item.unit"
|
|
placeholder="Pz, Kg, Lt, etc."
|
|
class="w-full bg-gray-50 dark:bg-gray-900"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-500 mb-1">Precio Unitario</label>
|
|
<InputNumber
|
|
v-model="item.unitPrice"
|
|
:min="0"
|
|
:minFractionDigits="2"
|
|
:maxFractionDigits="2"
|
|
mode="currency"
|
|
currency="MXN"
|
|
locale="es-MX"
|
|
class="w-full"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
|
|
<div class="pt-2 border-t border-gray-200 dark:border-gray-700">
|
|
<Button
|
|
:label="item.comments ? 'Editar Comentarios' : 'Agregar Comentarios'"
|
|
:icon="item.comments ? 'pi pi-pencil' : 'pi pi-comment'"
|
|
size="small"
|
|
outlined
|
|
class="w-full"
|
|
@click="openCommentsDialog(index)"
|
|
/>
|
|
</div>
|
|
|
|
<div class="pt-2 border-t border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-xs font-bold text-gray-500 uppercase">Subtotal</span>
|
|
<span class="text-lg font-bold text-primary">{{ formatCurrency(calculateSubtotal(item)) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Desktop View - DataTable -->
|
|
<div class="hidden lg:block">
|
|
<DataTable :value="items" class="text-sm" stripedRows>
|
|
<Column field="id" header="#" class="text-center" style="width: 60px">
|
|
<template #body="{ index }">
|
|
<span class="text-gray-400 font-medium text-xs">{{ String(index + 1).padStart(2, '0') }}</span>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column header="Cantidad" style="width: 130px">
|
|
<template #body="{ data }">
|
|
<InputNumber v-model="data.quantity" :min="0" class="w-full" size="small" />
|
|
</template>
|
|
</Column>
|
|
|
|
<Column header="Unidad" style="width: 120px">
|
|
<template #body="{ data }">
|
|
<InputText
|
|
v-model="data.unit"
|
|
placeholder="Pz, Kg, Lt..."
|
|
class="w-full text-sm"
|
|
size="small"
|
|
/>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column header="Producto / Servicio" style="min-width: 300px">
|
|
<template #body="{ data }">
|
|
<InputText
|
|
v-model="data.product"
|
|
placeholder="Nombre del producto o servicio..."
|
|
class="w-full border-none p-0 text-sm"
|
|
unstyled
|
|
/>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column header="Precio Unitario" style="width: 180px">
|
|
<template #body="{ data }">
|
|
<InputNumber
|
|
v-model="data.unitPrice"
|
|
:min="0"
|
|
:minFractionDigits="2"
|
|
:maxFractionDigits="2"
|
|
mode="currency"
|
|
currency="MXN"
|
|
locale="es-MX"
|
|
class="w-full"
|
|
size="small"
|
|
/>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column header="Subtotal" headerClass="text-right" style="width: 180px">
|
|
<template #body="{ data }">
|
|
<div class="text-right">
|
|
<span class="font-semibold text-gray-700 dark:text-gray-300">{{ formatCurrency(calculateSubtotal(data)) }}</span>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column header="Comentarios" style="width: 140px" class="text-center">
|
|
<template #body="{ data, index }">
|
|
<Button
|
|
:icon="data.comments ? 'pi pi-comment' : 'pi pi-comment'"
|
|
:severity="data.comments ? 'success' : 'secondary'"
|
|
text
|
|
rounded
|
|
size="small"
|
|
@click="openCommentsDialog(index)"
|
|
v-tooltip.top="data.comments ? 'Editar Comentarios' : 'Agregar Comentarios'"
|
|
/>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column header="Acciones" class="text-center" style="width: 80px">
|
|
<template #body="{ index }">
|
|
<Button
|
|
icon="pi pi-trash"
|
|
text
|
|
rounded
|
|
severity="danger"
|
|
size="small"
|
|
@click="removeItem(index)"
|
|
/>
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
</div>
|
|
|
|
<div class="mt-3 p-4 bg-gray-50 dark:bg-gray-800/30 flex flex-col sm:flex-row items-end justify-between gap-3">
|
|
<div class="text-left">
|
|
<p class="text-xs text-gray-500 uppercase font-semibold">Total Items</p>
|
|
<p class="text-lg font-bold">{{ totalItems() }}</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-xs text-gray-500 uppercase font-semibold mb-1">Total General</p>
|
|
<p class="text-2xl font-black text-primary">{{ formatCurrency(calculateTotal()) }}</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Documentos y Fotos -->
|
|
<!-- <Card>
|
|
<template #header>
|
|
<div class="px-4 md:px-6 py-3 border-b border-surface-200 dark:border-surface-700">
|
|
<div class="flex items-center gap-2">
|
|
<i class="pi pi-paperclip text-primary text-lg"></i>
|
|
<h3 class="font-bold text-base">Documentos y Fotos</h3>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #content>
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div>
|
|
<FileUpload
|
|
mode="basic"
|
|
accept="image/*,application/pdf"
|
|
:maxFileSize="5000000"
|
|
chooseLabel="Seleccionar archivos"
|
|
@select="onFileUpload"
|
|
:auto="true"
|
|
customUpload
|
|
>
|
|
<template #empty>
|
|
<div class="border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-xl p-6 flex flex-col items-center justify-center text-center hover:border-primary hover:bg-primary/5 transition-all cursor-pointer">
|
|
<div class="bg-primary/10 w-12 h-12 rounded-full flex items-center justify-center mb-3">
|
|
<i class="pi pi-cloud-upload text-primary text-xl"></i>
|
|
</div>
|
|
<p class="font-semibold text-gray-700 dark:text-gray-300 text-sm">Suelte archivos aquí</p>
|
|
<p class="text-xs text-gray-400 mt-1">
|
|
O haga clic para explorar. JPG, PNG, PDF (Máx. 5MB)
|
|
</p>
|
|
</div>
|
|
</template>
|
|
</FileUpload>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<p class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3">Archivos Subidos</p>
|
|
<div v-for="(file, index) in uploadedFiles" :key="index"
|
|
class="flex items-center justify-between p-2.5 border border-gray-200 dark:border-gray-800 rounded-lg bg-white dark:bg-gray-900 shadow-sm">
|
|
<div class="flex items-center gap-2.5 min-w-0 flex-1">
|
|
<div class="p-1.5 rounded shrink-0" :class="file.type === 'image' ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600' : 'bg-red-50 dark:bg-red-900/20 text-red-600'">
|
|
<i :class="file.type === 'image' ? 'pi pi-image' : 'pi pi-file-pdf'" class="text-base"></i>
|
|
</div>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium truncate">{{ file.name }}</p>
|
|
<p class="text-[10px] text-gray-400 uppercase font-bold">{{ file.size }}</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
icon="pi pi-times"
|
|
text
|
|
rounded
|
|
severity="danger"
|
|
size="small"
|
|
@click="removeFile(index)"
|
|
class="shrink-0"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card> -->
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 pt-4 border-t border-gray-200 dark:border-gray-800 pb-8">
|
|
<Button
|
|
label="Cancelar Registro"
|
|
icon="pi pi-times"
|
|
severity="danger"
|
|
text
|
|
size="small"
|
|
@click="handleCancel"
|
|
class="w-full sm:w-auto order-3 sm:order-1"
|
|
/>
|
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2.5 order-1 sm:order-2">
|
|
<Button
|
|
label="Guardar como Borrador"
|
|
severity="secondary"
|
|
outlined
|
|
size="small"
|
|
:loading="isSaving"
|
|
:disabled="isSaving"
|
|
@click="handleSaveDraft"
|
|
class="w-full sm:w-auto"
|
|
/>
|
|
<Button
|
|
label="Enviar a Aprobación"
|
|
icon="pi pi-send"
|
|
size="small"
|
|
:loading="isSaving"
|
|
:disabled="isSaving"
|
|
@click="handleSubmit"
|
|
class="w-full sm:w-auto"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comments Dialog -->
|
|
<Dialog
|
|
v-model:visible="commentsDialogVisible"
|
|
modal
|
|
:header="selectedItemIndex !== null && items[selectedItemIndex]?.comments ? 'Editar Comentarios del Item' : 'Agregar Comentarios al Item'"
|
|
:style="{ width: '600px' }"
|
|
:dismissableMask="true"
|
|
>
|
|
<div class="space-y-4 py-4">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
|
<i class="pi pi-comment mr-2 text-primary"></i>
|
|
Comentarios o notas adicionales
|
|
</label>
|
|
<Textarea
|
|
v-model="tempComments"
|
|
rows="8"
|
|
placeholder="Agregue cualquier información adicional sobre este item: especificaciones técnicas, marcas preferidas, URLs de productos, notas especiales, etc."
|
|
class="w-full"
|
|
autoResize
|
|
autofocus
|
|
/>
|
|
<small class="text-gray-500 mt-2 block">
|
|
Puede incluir enlaces de productos, especificaciones, marcas, modelos o cualquier detalle relevante.
|
|
</small>
|
|
</div>
|
|
|
|
<div v-if="tempComments" class="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
<div class="flex items-start gap-2">
|
|
<i class="pi pi-info-circle text-blue-600 mt-0.5"></i>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-xs font-semibold text-blue-900 dark:text-blue-100 mb-1">Vista previa:</p>
|
|
<p class="text-xs text-blue-700 dark:text-blue-300 whitespace-pre-wrap">{{ tempComments }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex items-center justify-end gap-2">
|
|
<Button
|
|
label="Cancelar"
|
|
severity="secondary"
|
|
text
|
|
@click="closeCommentsDialog"
|
|
/>
|
|
<Button
|
|
label="Guardar"
|
|
icon="pi pi-check"
|
|
@click="saveComments"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Dialog>
|
|
|
|
<Toast />
|
|
</template>
|
|
|