feat: agregar funcionalidad para gestionar la entrega de activos y mejorar la descarga de resguardos

This commit is contained in:
Juan Felipe Zapata Moreno 2026-03-25 15:40:58 -06:00
parent 8205d4203b
commit dfbe572c79
7 changed files with 66 additions and 17 deletions

View File

@ -224,6 +224,22 @@ const isRouteActive = (to: string | undefined) => {
return item.to === route.path; return item.to === route.path;
}); });
// Verificar si existe otro item del menú con un prefijo más específico que también matchea
// Ej: si estamos en /fixed-assets/assignments/create y el menú tiene /fixed-assets/assignments,
// entonces /fixed-assets no debe activarse porque /fixed-assets/assignments es más específico
const hasMoreSpecificMatch = visibleMenuItems.value.some(item => {
const checkItem = (itemTo: string | undefined) => {
if (!itemTo || itemTo === to) return false;
return itemTo.startsWith(to + '/') && route.path.startsWith(itemTo + '/') || itemTo.startsWith(to + '/') && route.path === itemTo;
};
if (item.items) {
return item.items.some(subItem => checkItem(subItem.to));
}
return checkItem(item.to);
});
if (hasMoreSpecificMatch) return false;
// Si NO está explícitamente en el menú, entonces es una ruta hija (create, edit, etc) // Si NO está explícitamente en el menú, entonces es una ruta hija (create, edit, etc)
return !isExplicitRoute; return !isExplicitRoute;
} }

View File

@ -54,7 +54,7 @@ const depreciationOptions = [
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100"> <label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
Valor Residual (MXN) Valor Residual
</label> </label>
<InputNumber <InputNumber
v-model="form.residual_value" v-model="form.residual_value"

View File

@ -52,6 +52,18 @@ defineProps<Props>();
showClear showClear
/> />
</div> </div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Entrega (Opcional)</label>
<Select
v-model="form.deliveredById"
:options="users"
optionLabel="full_name"
optionValue="id"
placeholder="Seleccione quien entrega..."
class="w-full"
showClear
/>
</div>
<div class="space-y-2 md:col-span-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Notas o Comentarios (Opcional)</label> <label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Notas o Comentarios (Opcional)</label>
<Textarea <Textarea

View File

@ -30,6 +30,7 @@ const form = ref<FixedAssetAssignmentFormData>({
assetId: null, assetId: null,
employeeId: null, employeeId: null,
authorizedById: null, authorizedById: null,
deliveredById: null,
assignedAt: new Date().toISOString().slice(0, 10), assignedAt: new Date().toISOString().slice(0, 10),
receiptFolio: '', receiptFolio: '',
notes: '' notes: ''
@ -97,6 +98,7 @@ const save = async () => {
assigned_at: form.value.assignedAt, assigned_at: form.value.assignedAt,
receipt_folio: form.value.receiptFolio || undefined, receipt_folio: form.value.receiptFolio || undefined,
authorized_by: form.value.authorizedById ?? undefined, authorized_by: form.value.authorizedById ?? undefined,
delivered_by: form.value.deliveredById ?? undefined,
notes: form.value.notes || undefined, notes: form.value.notes || undefined,
}); });

View File

@ -77,9 +77,17 @@ const goToOffboarding = (assignment: AssetAssignment) => {
router.push(`/fixed-assets/assignments/${assignment.asset_id}/${assignment.id}/offboarding`); router.push(`/fixed-assets/assignments/${assignment.asset_id}/${assignment.id}/offboarding`);
}; };
const downloadResguardo = (assignment: AssetAssignment) => { const downloadingId = ref<number | null>(null);
const url = fixedAssetsService.getResguardoUrl(assignment.asset_id, assignment.id);
window.open(url, '_blank'); const downloadResguardo = async (assignment: AssetAssignment) => {
downloadingId.value = assignment.id;
try {
await fixedAssetsService.downloadResguardo(assignment.asset_id, assignment.id);
} catch {
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo generar el resguardo.', life: 4000 });
} finally {
downloadingId.value = null;
}
}; };
onMounted(loadAssignments); onMounted(loadAssignments);
@ -130,7 +138,6 @@ onMounted(loadAssignments);
<table class="min-w-full border-collapse"> <table class="min-w-full border-collapse">
<thead> <thead>
<tr class="bg-surface-50 text-left text-xs font-semibold uppercase tracking-wide text-surface-500 dark:bg-surface-800 dark:text-surface-300"> <tr class="bg-surface-50 text-left text-xs font-semibold uppercase tracking-wide text-surface-500 dark:bg-surface-800 dark:text-surface-300">
<th class="px-4 py-3">ID</th>
<th class="px-4 py-3">Activo</th> <th class="px-4 py-3">Activo</th>
<th class="px-4 py-3">Empleado</th> <th class="px-4 py-3">Empleado</th>
<th class="px-4 py-3">Fecha Entrega</th> <th class="px-4 py-3">Fecha Entrega</th>
@ -151,9 +158,6 @@ onMounted(loadAssignments);
:key="assignment.id" :key="assignment.id"
class="border-t border-surface-200 text-sm dark:border-surface-700" class="border-t border-surface-200 text-sm dark:border-surface-700"
> >
<td class="px-4 py-3 font-medium text-surface-800 dark:text-surface-100">
#{{ assignment.id }}
</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<p class="font-semibold text-surface-900 dark:text-surface-0"> <p class="font-semibold text-surface-900 dark:text-surface-0">
{{ assignment.asset?.inventory_warehouse?.product?.name ?? assignment.asset?.sku ?? '—' }} {{ assignment.asset?.inventory_warehouse?.product?.name ?? assignment.asset?.sku ?? '—' }}
@ -187,6 +191,7 @@ onMounted(loadAssignments);
size="small" size="small"
severity="secondary" severity="secondary"
v-tooltip.top="'Descargar resguardo'" v-tooltip.top="'Descargar resguardo'"
:loading="downloadingId === assignment.id"
@click="downloadResguardo(assignment)" @click="downloadResguardo(assignment)"
/> />
<Button <Button

View File

@ -117,13 +117,14 @@ export interface AssetAssignmentsPaginatedResponse {
export interface UserOption { export interface UserOption {
id: number; id: number;
name: string;
paternal: string;
maternal: string;
full_name: string; full_name: string;
email: string;
} }
export interface UsersResponse { export interface UsersResponse {
status: string; data: UserOption[];
data: { data: UserOption[] };
} }
interface AssignAssetData { interface AssignAssetData {
@ -131,6 +132,7 @@ interface AssignAssetData {
assigned_at?: string; assigned_at?: string;
receipt_folio?: string; receipt_folio?: string;
authorized_by?: number; authorized_by?: number;
delivered_by?: number;
notes?: string; notes?: string;
} }
@ -183,7 +185,7 @@ class FixedAssetsService {
if (filters.page) params.page = filters.page; if (filters.page) params.page = filters.page;
if (filters.paginate !== undefined) params.paginate = filters.paginate; if (filters.paginate !== undefined) params.paginate = filters.paginate;
const response = await api.get<AssetAssignmentsPaginatedResponse>('/api/asset-assignments', { params }); const response = await api.get<AssetAssignmentsPaginatedResponse>('/api/assets/assignments', { params });
return response.data; return response.data;
} }
@ -198,13 +200,24 @@ class FixedAssetsService {
} }
async getUsers(): Promise<UserOption[]> { async getUsers(): Promise<UserOption[]> {
const response = await api.get<UsersResponse>('/api/admin/users', { params: { paginate: false } }); const response = await api.get<UsersResponse>('/api/rh/employees', { params: { paginate: false } });
return response.data.data.data; return response.data.data.map((e: any) => ({
...e,
full_name: `${e.name} ${e.paternal} ${e.maternal ?? ''}`.trim(),
}));
} }
getResguardoUrl(assetId: number, assignmentId: number): string { async downloadResguardo(assetId: number, assignmentId: number): Promise<void> {
const base = import.meta.env.VITE_API_URL || ''; const response = await api.get(
return `${base}/api/asset-assignments-public/${assetId}/assignments/${assignmentId}/resguardo`; `/api/assets/${assetId}/assignments/${assignmentId}/resguardo`,
{ responseType: 'blob' }
);
const blobUrl = URL.createObjectURL(new Blob([response.data], { type: 'application/pdf' }));
const link = document.createElement('a');
link.href = blobUrl;
link.target = '_blank';
link.click();
URL.revokeObjectURL(blobUrl);
} }
} }

View File

@ -18,6 +18,7 @@ export interface FixedAssetAssignmentFormData {
assetId: number | null; assetId: number | null;
employeeId: number | null; employeeId: number | null;
authorizedById: number | null; authorizedById: number | null;
deliveredById: number | null;
assignedAt: string; assignedAt: string;
receiptFolio: string; receiptFolio: string;
notes: string; notes: string;