edgar.mendez 8abf849306 feat: implement technical and financial approval workflows for requisitions
- 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.
2026-03-04 16:44:25 -06:00

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>