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;
});
// 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)
return !isExplicitRoute;
}

View File

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

View File

@ -52,6 +52,18 @@ defineProps<Props>();
showClear
/>
</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">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Notas o Comentarios (Opcional)</label>
<Textarea

View File

@ -30,6 +30,7 @@ const form = ref<FixedAssetAssignmentFormData>({
assetId: null,
employeeId: null,
authorizedById: null,
deliveredById: null,
assignedAt: new Date().toISOString().slice(0, 10),
receiptFolio: '',
notes: ''
@ -97,6 +98,7 @@ const save = async () => {
assigned_at: form.value.assignedAt,
receipt_folio: form.value.receiptFolio || undefined,
authorized_by: form.value.authorizedById ?? undefined,
delivered_by: form.value.deliveredById ?? 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`);
};
const downloadResguardo = (assignment: AssetAssignment) => {
const url = fixedAssetsService.getResguardoUrl(assignment.asset_id, assignment.id);
window.open(url, '_blank');
const downloadingId = ref<number | null>(null);
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);
@ -130,7 +138,6 @@ onMounted(loadAssignments);
<table class="min-w-full border-collapse">
<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">
<th class="px-4 py-3">ID</th>
<th class="px-4 py-3">Activo</th>
<th class="px-4 py-3">Empleado</th>
<th class="px-4 py-3">Fecha Entrega</th>
@ -151,9 +158,6 @@ onMounted(loadAssignments);
:key="assignment.id"
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">
<p class="font-semibold text-surface-900 dark:text-surface-0">
{{ assignment.asset?.inventory_warehouse?.product?.name ?? assignment.asset?.sku ?? '—' }}
@ -187,6 +191,7 @@ onMounted(loadAssignments);
size="small"
severity="secondary"
v-tooltip.top="'Descargar resguardo'"
:loading="downloadingId === assignment.id"
@click="downloadResguardo(assignment)"
/>
<Button

View File

@ -117,13 +117,14 @@ export interface AssetAssignmentsPaginatedResponse {
export interface UserOption {
id: number;
name: string;
paternal: string;
maternal: string;
full_name: string;
email: string;
}
export interface UsersResponse {
status: string;
data: { data: UserOption[] };
data: UserOption[];
}
interface AssignAssetData {
@ -131,6 +132,7 @@ interface AssignAssetData {
assigned_at?: string;
receipt_folio?: string;
authorized_by?: number;
delivered_by?: number;
notes?: string;
}
@ -183,7 +185,7 @@ class FixedAssetsService {
if (filters.page) params.page = filters.page;
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;
}
@ -198,13 +200,24 @@ class FixedAssetsService {
}
async getUsers(): Promise<UserOption[]> {
const response = await api.get<UsersResponse>('/api/admin/users', { params: { paginate: false } });
return response.data.data.data;
const response = await api.get<UsersResponse>('/api/rh/employees', { params: { paginate: false } });
return response.data.data.map((e: any) => ({
...e,
full_name: `${e.name} ${e.paternal} ${e.maternal ?? ''}`.trim(),
}));
}
getResguardoUrl(assetId: number, assignmentId: number): string {
const base = import.meta.env.VITE_API_URL || '';
return `${base}/api/asset-assignments-public/${assetId}/assignments/${assignmentId}/resguardo`;
async downloadResguardo(assetId: number, assignmentId: number): Promise<void> {
const response = await api.get(
`/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;
employeeId: number | null;
authorizedById: number | null;
deliveredById: number | null;
assignedAt: string;
receiptFolio: string;
notes: string;