- 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.
894 lines
35 KiB
Vue
894 lines
35 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { useToast } from 'primevue/usetoast';
|
|
import Card from 'primevue/card';
|
|
import Button from 'primevue/button';
|
|
import DataTable from 'primevue/datatable';
|
|
import Column from 'primevue/column';
|
|
import InputText from 'primevue/inputtext';
|
|
import Dropdown from 'primevue/dropdown';
|
|
import Tag from 'primevue/tag';
|
|
import Paginator from 'primevue/paginator';
|
|
import Toast from 'primevue/toast';
|
|
import ConfirmDialog from 'primevue/confirmdialog';
|
|
import Dialog from 'primevue/dialog';
|
|
import Textarea from 'primevue/textarea';
|
|
import { useRequisitionStore } from './stores/requisitionStore';
|
|
import type { Requisition } 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 toast = useToast();
|
|
const requisitionStore = useRequisitionStore();
|
|
const departmentsService = new DepartmentsService();
|
|
|
|
// Departamentos desde API
|
|
const departments = ref<Department[]>([]);
|
|
|
|
const searchQuery = ref('');
|
|
const selectedStatus = ref<string | null>(null);
|
|
const selectedPriority = ref<string | null>(null);
|
|
|
|
const pagination = ref({
|
|
first: 0,
|
|
rows: 10
|
|
});
|
|
|
|
// Cancel dialog
|
|
const showCancelDialog = ref(false);
|
|
const cancelComment = ref('');
|
|
const requisitionToCancel = ref<Requisition | null>(null);
|
|
|
|
// Approval/Rejection dialogs
|
|
const showApprovalDialog = ref(false);
|
|
const showRejectionDialog = ref(false);
|
|
const approvalType = ref<'technical' | 'financial'>('technical');
|
|
const approvalComments = ref('');
|
|
const rejectionReason = ref('');
|
|
const requisitionToApprove = ref<Requisition | null>(null);
|
|
const requisitionToReject = ref<Requisition | null>(null);
|
|
|
|
// Computed filtered requisitions
|
|
const filteredRequisitions = computed(() => {
|
|
let filtered = [...requisitionStore.requisitions];
|
|
|
|
if (searchQuery.value) {
|
|
const query = searchQuery.value.toLowerCase();
|
|
filtered = filtered.filter(r =>
|
|
r.folio.toLowerCase().includes(query) ||
|
|
r.requester.toLowerCase().includes(query)
|
|
);
|
|
}
|
|
|
|
if (selectedStatus.value) {
|
|
filtered = filtered.filter(r => r.status === selectedStatus.value);
|
|
}
|
|
|
|
if (selectedPriority.value) {
|
|
filtered = filtered.filter(r => r.priority === selectedPriority.value);
|
|
}
|
|
|
|
return filtered;
|
|
});
|
|
|
|
const paginatedRequisitions = computed(() => {
|
|
const start = pagination.value.first;
|
|
const end = start + pagination.value.rows;
|
|
return filteredRequisitions.value.slice(start, end);
|
|
});
|
|
|
|
const totalRecords = computed(() => filteredRequisitions.value.length);
|
|
|
|
const statusOptions = [
|
|
{ label: 'Todos', value: null },
|
|
{ label: 'Borrador', value: 'draft' },
|
|
{ label: 'Pendiente Técnica', value: 'pending_technical' },
|
|
{ label: 'Rechazado Técnica', value: 'rejected_technical' },
|
|
{ label: 'Pendiente Financiera', value: 'pending_financial' },
|
|
{ label: 'Rechazado Financiera', value: 'rejected_financial' },
|
|
{ label: 'Aprobado', value: 'approved' },
|
|
{ label: 'Cancelado', value: 'cancelled' }
|
|
];
|
|
|
|
const priorityOptions = [
|
|
{ label: 'Todas', value: null },
|
|
{ label: 'Baja', value: 'low' },
|
|
{ label: 'Normal', value: 'normal' },
|
|
{ label: 'Alta', value: 'high' },
|
|
{ label: 'Urgente', value: 'urgent' }
|
|
];
|
|
|
|
const getStatusSeverity = (status: string) => {
|
|
const severityMap: Record<string, string> = {
|
|
pending_technical: 'warning',
|
|
pending_financial: 'warning',
|
|
approved: 'success',
|
|
draft: 'secondary',
|
|
rejected_technical: 'danger',
|
|
rejected_financial: 'danger',
|
|
cancelled: 'contrast'
|
|
};
|
|
return severityMap[status] || 'info';
|
|
};
|
|
|
|
const getStatusLabel = (status: string) => {
|
|
const labelMap: Record<string, string> = {
|
|
pending_technical: 'Pend. Aprobación Técnica',
|
|
pending_financial: 'Pend. Aprobación Financiera',
|
|
approved: 'Aprobado',
|
|
draft: 'Borrador',
|
|
rejected_technical: 'Rechazado por Técnico',
|
|
rejected_financial: 'Rechazado por Finanzas',
|
|
cancelled: 'Cancelado'
|
|
};
|
|
return labelMap[status] || status;
|
|
};
|
|
|
|
const getPriorityColor = (priority: string) => {
|
|
const colorMap: Record<string, string> = {
|
|
low: 'bg-slate-300 dark:bg-slate-600',
|
|
normal: 'bg-blue-500',
|
|
high: 'bg-red-500',
|
|
urgent: 'bg-red-600'
|
|
};
|
|
return colorMap[priority] || 'bg-gray-400';
|
|
};
|
|
|
|
const getPriorityLabel = (priority: string) => {
|
|
const labelMap: Record<string, string> = {
|
|
low: 'Baja',
|
|
normal: 'Normal',
|
|
high: 'Alta',
|
|
urgent: 'Urgente'
|
|
};
|
|
return labelMap[priority] || priority;
|
|
};
|
|
|
|
/* const getDepartmentName = (departmentId: string) => {
|
|
const dept = departments.value.find(d => d.id.toString() === departmentId);
|
|
return dept ? dept.name : departmentId;
|
|
}; */
|
|
|
|
const onPageChange = (event: any) => {
|
|
pagination.value.first = event.first;
|
|
pagination.value.rows = event.rows;
|
|
};
|
|
|
|
const handleView = (requisition: Requisition) => {
|
|
router.push(`/requisitions/${requisition.id}`);
|
|
};
|
|
|
|
const handleEdit = (requisition: Requisition) => {
|
|
// Permitir editar si está en borrador o fue rechazada
|
|
if (requisition.status === 'draft' ||
|
|
requisition.status === 'rejected_technical' ||
|
|
requisition.status === 'rejected_financial') {
|
|
router.push(`/requisitions/edit/${requisition.id}`);
|
|
} else {
|
|
toast.add({
|
|
severity: 'warn',
|
|
summary: 'No permitido',
|
|
detail: 'Solo se pueden editar requisiciones en borrador o rechazadas',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleApproveTechnical = (requisition: Requisition) => {
|
|
requisitionToApprove.value = requisition;
|
|
approvalType.value = 'technical';
|
|
approvalComments.value = '';
|
|
showApprovalDialog.value = true;
|
|
};
|
|
|
|
const handleRejectTechnical = (requisition: Requisition) => {
|
|
requisitionToReject.value = requisition;
|
|
approvalType.value = 'technical';
|
|
rejectionReason.value = '';
|
|
showRejectionDialog.value = true;
|
|
};
|
|
|
|
const handleApproveFinancial = (requisition: Requisition) => {
|
|
requisitionToApprove.value = requisition;
|
|
approvalType.value = 'financial';
|
|
approvalComments.value = '';
|
|
showApprovalDialog.value = true;
|
|
};
|
|
|
|
const handleRejectFinancial = (requisition: Requisition) => {
|
|
requisitionToReject.value = requisition;
|
|
approvalType.value = 'financial';
|
|
rejectionReason.value = '';
|
|
showRejectionDialog.value = true;
|
|
};
|
|
|
|
const confirmApproval = async () => {
|
|
if (!requisitionToApprove.value) return;
|
|
|
|
try {
|
|
const approver = 'Usuario Actual'; // Aquí iría el usuario del sistema de autenticación
|
|
|
|
if (approvalType.value === 'technical') {
|
|
await requisitionStore.approveTechnical(
|
|
requisitionToApprove.value.id,
|
|
approver,
|
|
approvalComments.value.trim() || undefined
|
|
);
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Aprobado',
|
|
detail: 'Requisición aprobada técnicamente. Ahora pasa a finanzas.',
|
|
life: 3000
|
|
});
|
|
} else {
|
|
await requisitionStore.approveFinancial(
|
|
requisitionToApprove.value.id,
|
|
approver,
|
|
approvalComments.value.trim() || undefined
|
|
);
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Aprobado',
|
|
detail: 'Requisición aprobada. Ahora pasa a almacén.',
|
|
life: 3000
|
|
});
|
|
}
|
|
|
|
showApprovalDialog.value = false;
|
|
requisitionToApprove.value = null;
|
|
approvalComments.value = '';
|
|
} catch (error: any) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: error.message || 'Error al aprobar la requisición',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const confirmRejection = async () => {
|
|
if (!requisitionToReject.value) return;
|
|
|
|
if (!rejectionReason.value.trim() || rejectionReason.value.trim().length < 10) {
|
|
toast.add({
|
|
severity: 'warn',
|
|
summary: 'Campo requerido',
|
|
detail: 'Debe proporcionar un motivo válido (mínimo 10 caracteres)',
|
|
life: 3000
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const rejector = 'Usuario Actual'; // Aquí iría el usuario del sistema de autenticación
|
|
|
|
if (approvalType.value === 'technical') {
|
|
await requisitionStore.rejectTechnical(
|
|
requisitionToReject.value.id,
|
|
rejector,
|
|
rejectionReason.value.trim()
|
|
);
|
|
toast.add({
|
|
severity: 'info',
|
|
summary: 'Rechazado',
|
|
detail: 'Requisición rechazada. El solicitante puede editarla y reenviarla.',
|
|
life: 3000
|
|
});
|
|
} else {
|
|
await requisitionStore.rejectFinancial(
|
|
requisitionToReject.value.id,
|
|
rejector,
|
|
rejectionReason.value.trim()
|
|
);
|
|
toast.add({
|
|
severity: 'info',
|
|
summary: 'Rechazado',
|
|
detail: 'Requisición rechazada. El solicitante puede editarla y reenviarla.',
|
|
life: 3000
|
|
});
|
|
}
|
|
|
|
showRejectionDialog.value = false;
|
|
requisitionToReject.value = null;
|
|
rejectionReason.value = '';
|
|
} catch (error: any) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: error.message || 'Error al rechazar la requisición',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const closeApprovalDialog = () => {
|
|
showApprovalDialog.value = false;
|
|
requisitionToApprove.value = null;
|
|
approvalComments.value = '';
|
|
};
|
|
|
|
const closeRejectionDialog = () => {
|
|
showRejectionDialog.value = false;
|
|
requisitionToReject.value = null;
|
|
rejectionReason.value = '';
|
|
};
|
|
|
|
const handleCancel = (requisition: Requisition) => {
|
|
requisitionToCancel.value = requisition;
|
|
cancelComment.value = '';
|
|
showCancelDialog.value = true;
|
|
};
|
|
|
|
const confirmCancel = async () => {
|
|
if (!requisitionToCancel.value) return;
|
|
|
|
if (!cancelComment.value.trim() || cancelComment.value.trim().length < 10) {
|
|
toast.add({
|
|
severity: 'warn',
|
|
summary: 'Campo requerido',
|
|
detail: 'Debe proporcionar un motivo válido (mínimo 10 caracteres)',
|
|
life: 3000
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Usar el método cancelRequisition del store en lugar de deleteRequisition
|
|
await requisitionStore.cancelRequisition(
|
|
requisitionToCancel.value.id,
|
|
cancelComment.value.trim(),
|
|
requisitionToCancel.value.requester // O el usuario actual del sistema
|
|
);
|
|
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Cancelado',
|
|
detail: 'Requisición cancelada correctamente',
|
|
life: 3000
|
|
});
|
|
|
|
showCancelDialog.value = false;
|
|
requisitionToCancel.value = null;
|
|
cancelComment.value = '';
|
|
} catch (error: any) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: error.message || 'Error al cancelar la requisición',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const closeCancelDialog = () => {
|
|
showCancelDialog.value = false;
|
|
requisitionToCancel.value = null;
|
|
cancelComment.value = '';
|
|
};
|
|
|
|
const handleNewRequisition = () => {
|
|
router.push('/requisitions/create');
|
|
};
|
|
|
|
const loadDepartments = async () => {
|
|
try {
|
|
const response = await departmentsService.getDepartments();
|
|
departments.value = response.data;
|
|
} catch (error) {
|
|
console.error('Error al cargar departamentos:', error);
|
|
}
|
|
};
|
|
|
|
onMounted(async () => {
|
|
await loadDepartments();
|
|
await requisitionStore.fetchRequisitions();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
<div>
|
|
<h2 class="text-3xl font-black text-surface-900 dark:text-white tracking-tight">
|
|
Gestión de Requisiciones
|
|
</h2>
|
|
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">
|
|
Revisa y administra las requisiciones de almacén y producción.
|
|
</p>
|
|
</div>
|
|
<Button label="Nueva Requisición" icon="pi pi-plus" @click="handleNewRequisition" />
|
|
</div>
|
|
|
|
<!-- KPI Cards -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<Card>
|
|
<template #content>
|
|
<div class="space-y-2">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Total Pendientes</p>
|
|
<div class="flex items-end justify-between">
|
|
<h3 class="text-2xl font-bold">{{ requisitionStore.pendingCount }}</h3>
|
|
<Tag value="Acción requerida" severity="warning" class="text-xs" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<Card>
|
|
<template #content>
|
|
<div class="space-y-2">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Aprobados Hoy</p>
|
|
<div class="flex items-end justify-between">
|
|
<h3 class="text-2xl font-bold">{{ requisitionStore.approvedTodayCount }}</h3>
|
|
<Tag value="Actualizado" severity="success" class="text-xs" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<Card>
|
|
<template #content>
|
|
<div class="space-y-2">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Total Requisiciones</p>
|
|
<div class="flex items-end justify-between">
|
|
<h3 class="text-2xl font-bold">{{ requisitionStore.requisitions.length }}</h3>
|
|
<Tag value="Sistema" severity="info" class="text-xs" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- <Card>
|
|
<template #content>
|
|
<div class="space-y-2">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Presupuesto Mensual</p>
|
|
<div class="flex items-end justify-between">
|
|
<h3 class="text-2xl font-bold">${{ (requisitionStore.totalBudgetThisMonth / 1000).toFixed(1) }}k</h3>
|
|
<Tag value="Este mes" severity="secondary" class="text-xs" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card> -->
|
|
</div>
|
|
|
|
<!-- Filters and Table -->
|
|
<Card>
|
|
<template #content>
|
|
<!-- Filters Toolbar -->
|
|
<div class="flex flex-wrap gap-4 items-end mb-6">
|
|
<div class="flex-1 min-w-[300px]">
|
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">
|
|
Buscar
|
|
</label>
|
|
<span class="p-input-icon-left w-full">
|
|
<i class="pi pi-search" />
|
|
<InputText
|
|
v-model="searchQuery"
|
|
placeholder="Buscar por número de folio (ej. REQ-2023...)"
|
|
class="w-full"
|
|
/>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="w-48">
|
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">
|
|
Estado
|
|
</label>
|
|
<Dropdown
|
|
v-model="selectedStatus"
|
|
:options="statusOptions"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
placeholder="Todos"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div class="w-48">
|
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">
|
|
Prioridad
|
|
</label>
|
|
<Dropdown
|
|
v-model="selectedPriority"
|
|
:options="priorityOptions"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
placeholder="Todas"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<Button icon="pi pi-filter" label="Más Filtros" text />
|
|
</div>
|
|
|
|
<!-- DataTable -->
|
|
<DataTable
|
|
:value="paginatedRequisitions"
|
|
:loading="requisitionStore.loading"
|
|
stripedRows
|
|
responsiveLayout="scroll"
|
|
class="p-datatable-sm"
|
|
>
|
|
<Column field="id" header="ID" style="min-width: 80px">
|
|
<template #body="{ data }">
|
|
<span class="font-mono text-gray-500">{{ data.id }}</span>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="folio" header="Folio" style="min-width: 150px">
|
|
<template #body="{ data }">
|
|
<span class="font-bold">{{ data.folio }}</span>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="requester" header="Solicitante" style="min-width: 150px" />
|
|
|
|
<!-- <Column field="department" header="Departamento" style="min-width: 180px">
|
|
<template #body="{ data }">
|
|
<span class="text-sm">{{ getDepartmentName(data.department) }}</span>
|
|
</template>
|
|
</Column> -->
|
|
|
|
<Column field="status" header="Estado" style="min-width: 120px">
|
|
<template #body="{ data }">
|
|
<Tag :value="getStatusLabel(data.status)" :severity="getStatusSeverity(data.status)" />
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="priority" header="Prioridad" style="min-width: 120px">
|
|
<template #body="{ data }">
|
|
<span class="flex items-center gap-2">
|
|
<span :class="['w-2 h-2 rounded-full', getPriorityColor(data.priority)]"></span>
|
|
{{ getPriorityLabel(data.priority) }}
|
|
</span>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="totalAmount" header="Monto Total" style="min-width: 140px">
|
|
<template #body="{ data }">
|
|
<span class="font-semibold">${{ data.totalAmount.toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}</span>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column field="createdAt" header="Fecha de Creación" style="min-width: 140px">
|
|
<template #body="{ data }">
|
|
<span class="text-sm">{{ new Date(data.createdAt).toLocaleDateString('es-MX') }}</span>
|
|
</template>
|
|
</Column>
|
|
|
|
<Column header="Acciones" headerStyle="text-align: center" bodyStyle="text-align: center" style="min-width: 200px">
|
|
<template #body="{ data }">
|
|
<div class="flex items-center justify-center gap-2">
|
|
<Button
|
|
icon="pi pi-eye"
|
|
text
|
|
rounded
|
|
size="small"
|
|
@click="handleView(data)"
|
|
v-tooltip.top="'Ver Detalles'"
|
|
/>
|
|
|
|
<!-- Editar - Solo borrador o rechazadas -->
|
|
<Button
|
|
v-if="data.status === 'draft' || data.status === 'rejected_technical' || data.status === 'rejected_financial'"
|
|
icon="pi pi-pencil"
|
|
text
|
|
rounded
|
|
size="small"
|
|
@click="handleEdit(data)"
|
|
v-tooltip.top="'Editar'"
|
|
/>
|
|
|
|
<!-- Aprobar Técnica -->
|
|
<Button
|
|
v-if="data.status === 'pending_technical'"
|
|
icon="pi pi-check"
|
|
text
|
|
rounded
|
|
size="small"
|
|
severity="success"
|
|
@click="handleApproveTechnical(data)"
|
|
v-tooltip.top="'Aprobar (Técnico)'"
|
|
/>
|
|
|
|
<!-- Rechazar Técnica -->
|
|
<Button
|
|
v-if="data.status === 'pending_technical'"
|
|
icon="pi pi-times"
|
|
text
|
|
rounded
|
|
size="small"
|
|
severity="danger"
|
|
@click="handleRejectTechnical(data)"
|
|
v-tooltip.top="'Rechazar (Técnico)'"
|
|
/>
|
|
|
|
<!-- Aprobar Financiera -->
|
|
<Button
|
|
v-if="data.status === 'pending_financial'"
|
|
icon="pi pi-check"
|
|
text
|
|
rounded
|
|
size="small"
|
|
severity="success"
|
|
@click="handleApproveFinancial(data)"
|
|
v-tooltip.top="'Aprobar (Finanzas)'"
|
|
/>
|
|
|
|
<!-- Rechazar Financiera -->
|
|
<Button
|
|
v-if="data.status === 'pending_financial'"
|
|
icon="pi pi-times"
|
|
text
|
|
rounded
|
|
size="small"
|
|
severity="danger"
|
|
@click="handleRejectFinancial(data)"
|
|
v-tooltip.top="'Rechazar (Finanzas)'"
|
|
/>
|
|
|
|
<!-- Cancelar - Solo borrador o pendientes -->
|
|
<Button
|
|
v-if="data.status === 'draft' || data.status === 'pending_technical' || data.status === 'pending_financial'"
|
|
icon="pi pi-ban"
|
|
text
|
|
rounded
|
|
size="small"
|
|
severity="warning"
|
|
@click="handleCancel(data)"
|
|
v-tooltip.top="'Cancelar'"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
|
|
<!-- Pagination -->
|
|
<div class="mt-4">
|
|
<Paginator
|
|
:first="pagination.first"
|
|
:rows="pagination.rows"
|
|
:totalRecords="totalRecords"
|
|
:rowsPerPageOptions="[10, 20, 50]"
|
|
@page="onPageChange"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Help Section -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<Card class="bg-blue-50 dark:bg-blue-900/10 border border-blue-100 dark:border-blue-900/30">
|
|
<template #content>
|
|
<div class="flex gap-4">
|
|
<i class="pi pi-info-circle text-blue-500 text-xl"></i>
|
|
<div>
|
|
<h4 class="font-semibold text-blue-900 dark:text-blue-300 text-sm mb-1">
|
|
¿Necesitas Ayuda?
|
|
</h4>
|
|
<p class="text-blue-700 dark:text-blue-400 text-xs leading-relaxed">
|
|
Los flujos de aprobación requieren al menos una firma de supervisor para requisiciones que excedan $500.
|
|
Consulta la sección "Ver Detalles" para la jerarquía de aprobación.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<Card class="bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800">
|
|
<template #content>
|
|
<div class="flex gap-4">
|
|
<i class="pi pi-link text-slate-500 text-xl"></i>
|
|
<div>
|
|
<h4 class="font-semibold text-slate-900 dark:text-slate-200 text-sm mb-2">
|
|
Enlaces Rápidos
|
|
</h4>
|
|
<div class="flex gap-3 text-xs">
|
|
<a href="#" class="text-primary hover:underline">Descargar Reporte</a>
|
|
<span class="text-slate-300 dark:text-slate-700">|</span>
|
|
<a href="#" class="text-primary hover:underline">Política de Aprobación</a>
|
|
<span class="text-slate-300 dark:text-slate-700">|</span>
|
|
<a href="#" class="text-primary hover:underline">Estado del Sistema</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cancel Dialog -->
|
|
<Dialog
|
|
v-model:visible="showCancelDialog"
|
|
modal
|
|
header="Cancelar Requisición"
|
|
:style="{ width: '500px' }"
|
|
:closable="true"
|
|
@hide="closeCancelDialog"
|
|
>
|
|
<div class="space-y-4">
|
|
<div v-if="requisitionToCancel" class="bg-orange-50 dark:bg-orange-900/10 border border-orange-200 dark:border-orange-900/30 rounded-lg p-4">
|
|
<div class="flex items-start gap-3">
|
|
<i class="pi pi-exclamation-triangle text-orange-500 text-xl mt-0.5"></i>
|
|
<div class="flex-1">
|
|
<h4 class="font-semibold text-orange-900 dark:text-orange-300 text-sm mb-1">
|
|
¿Está seguro de cancelar esta requisición?
|
|
</h4>
|
|
<p class="text-orange-700 dark:text-orange-400 text-xs">
|
|
Folio: <span class="font-bold">{{ requisitionToCancel.folio }}</span>
|
|
</p>
|
|
<p class="text-orange-700 dark:text-orange-400 text-xs">
|
|
Solicitante: {{ requisitionToCancel.requester }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="cancelReason" class="block text-sm font-semibold text-surface-900 dark:text-white mb-2">
|
|
Motivo de cancelación <span class="text-red-500">*</span>
|
|
</label>
|
|
<Textarea
|
|
id="cancelReason"
|
|
v-model="cancelComment"
|
|
rows="4"
|
|
class="w-full"
|
|
placeholder="Describa el motivo por el cual se cancela esta requisición..."
|
|
:class="{ 'p-invalid': !cancelComment.trim() }"
|
|
/>
|
|
<small class="text-gray-500 dark:text-gray-400">Mínimo 10 caracteres requeridos</small>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button
|
|
label="Cancelar"
|
|
icon="pi pi-times"
|
|
text
|
|
@click="closeCancelDialog"
|
|
/>
|
|
<Button
|
|
label="Confirmar Cancelación"
|
|
icon="pi pi-check"
|
|
severity="danger"
|
|
@click="confirmCancel"
|
|
:disabled="!cancelComment.trim() || cancelComment.trim().length < 10"
|
|
/>
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Approval Dialog -->
|
|
<Dialog
|
|
v-model:visible="showApprovalDialog"
|
|
modal
|
|
:header="approvalType === 'technical' ? 'Aprobación Técnica' : 'Aprobación Financiera'"
|
|
:style="{ width: '500px' }"
|
|
:closable="true"
|
|
@hide="closeApprovalDialog"
|
|
>
|
|
<div class="space-y-4">
|
|
<div v-if="requisitionToApprove" class="bg-green-50 dark:bg-green-900/10 border border-green-200 dark:border-green-900/30 rounded-lg p-4">
|
|
<div class="flex items-start gap-3">
|
|
<i class="pi pi-check-circle text-green-500 text-xl mt-0.5"></i>
|
|
<div class="flex-1">
|
|
<h4 class="font-semibold text-green-900 dark:text-green-300 text-sm mb-1">
|
|
{{ approvalType === 'technical' ? '¿Aprobar requisición técnicamente?' : '¿Aprobar requisición financieramente?' }}
|
|
</h4>
|
|
<p class="text-green-700 dark:text-green-400 text-xs">
|
|
Folio: <span class="font-bold">{{ requisitionToApprove.folio }}</span>
|
|
</p>
|
|
<p class="text-green-700 dark:text-green-400 text-xs">
|
|
Solicitante: {{ requisitionToApprove.requester }}
|
|
</p>
|
|
<p class="text-green-700 dark:text-green-400 text-xs">
|
|
Monto: <span class="font-bold">${{ requisitionToApprove.totalAmount.toLocaleString('es-MX') }}</span>
|
|
</p>
|
|
<p class="text-green-700 dark:text-green-400 text-xs mt-2">
|
|
{{ approvalType === 'technical'
|
|
? 'Al aprobar, la requisición pasará automáticamente a finanzas.'
|
|
: 'Al aprobar, la requisición pasará a almacén.'
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="approvalComments" class="block text-sm font-semibold text-surface-900 dark:text-white mb-2">
|
|
Comentarios (opcional)
|
|
</label>
|
|
<Textarea
|
|
id="approvalComments"
|
|
v-model="approvalComments"
|
|
rows="3"
|
|
class="w-full"
|
|
placeholder="Agregue cualquier comentario o nota sobre esta aprobación..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button
|
|
label="Cancelar"
|
|
icon="pi pi-times"
|
|
text
|
|
@click="closeApprovalDialog"
|
|
/>
|
|
<Button
|
|
label="Aprobar"
|
|
icon="pi pi-check"
|
|
severity="success"
|
|
@click="confirmApproval"
|
|
/>
|
|
</template>
|
|
</Dialog>
|
|
|
|
<!-- Rejection Dialog -->
|
|
<Dialog
|
|
v-model:visible="showRejectionDialog"
|
|
modal
|
|
:header="approvalType === 'technical' ? 'Rechazo Técnico' : 'Rechazo Financiero'"
|
|
:style="{ width: '500px' }"
|
|
:closable="true"
|
|
@hide="closeRejectionDialog"
|
|
>
|
|
<div class="space-y-4">
|
|
<div v-if="requisitionToReject" class="bg-red-50 dark:bg-red-900/10 border border-red-200 dark:border-red-900/30 rounded-lg p-4">
|
|
<div class="flex items-start gap-3">
|
|
<i class="pi pi-times-circle text-red-500 text-xl mt-0.5"></i>
|
|
<div class="flex-1">
|
|
<h4 class="font-semibold text-red-900 dark:text-red-300 text-sm mb-1">
|
|
{{ approvalType === 'technical' ? '¿Rechazar requisición (Técnico)?' : '¿Rechazar requisición (Finanzas)?' }}
|
|
</h4>
|
|
<p class="text-red-700 dark:text-red-400 text-xs">
|
|
Folio: <span class="font-bold">{{ requisitionToReject.folio }}</span>
|
|
</p>
|
|
<p class="text-red-700 dark:text-red-400 text-xs">
|
|
Solicitante: {{ requisitionToReject.requester }}
|
|
</p>
|
|
<p class="text-red-700 dark:text-red-400 text-xs mt-2">
|
|
El solicitante podrá editar su requisición y reenviarla para aprobación.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="rejectionReason" class="block text-sm font-semibold text-surface-900 dark:text-white mb-2">
|
|
Motivo del rechazo <span class="text-red-500">*</span>
|
|
</label>
|
|
<Textarea
|
|
id="rejectionReason"
|
|
v-model="rejectionReason"
|
|
rows="4"
|
|
class="w-full"
|
|
placeholder="Describa claramente por qué se rechaza esta requisición..."
|
|
:class="{ 'p-invalid': !rejectionReason.trim() }"
|
|
/>
|
|
<small class="text-gray-500 dark:text-gray-400">Mínimo 10 caracteres requeridos</small>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button
|
|
label="Cancelar"
|
|
icon="pi pi-times"
|
|
text
|
|
@click="closeRejectionDialog"
|
|
/>
|
|
<Button
|
|
label="Rechazar"
|
|
icon="pi pi-check"
|
|
severity="danger"
|
|
@click="confirmRejection"
|
|
:disabled="!rejectionReason.trim() || rejectionReason.trim().length < 10"
|
|
/>
|
|
</template>
|
|
</Dialog>
|
|
|
|
<Toast />
|
|
<ConfirmDialog />
|
|
</template> |