feat: mejora gestión de activos fijos con edición y asignaciones

- Se agregó edición de activos fijos en FixedAssetForm.vue.
- Se mejoró el manejo de carga y errores al obtener detalles del activo.
- Se actualizaron formularios de asignación usando IDs numéricos.
- Se mejoró la gestión de asignaciones con carga dinámica de activos y empleados.
- Refactor de componentes de asignación para mejorar manejo de datos y UX.
- Se agregaron métodos API para asignación y devolución de activos.
- Se actualizaron rutas para edición y baja (offboarding) de asignaciones.
This commit is contained in:
Juan Felipe Zapata Moreno 2026-03-23 17:44:34 -06:00
parent 29e4497ff1
commit c85200ed64
13 changed files with 499 additions and 329 deletions

1
components.d.ts vendored
View File

@ -35,6 +35,7 @@ declare module 'vue' {
ProgressSpinner: typeof import('primevue/progressspinner')['default'] ProgressSpinner: typeof import('primevue/progressspinner')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
Select: typeof import('primevue/select')['default']
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default'] Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
Tag: typeof import('primevue/tag')['default'] Tag: typeof import('primevue/tag')['default']
Toast: typeof import('primevue/toast')['default'] Toast: typeof import('primevue/toast')['default']

View File

@ -102,8 +102,18 @@ const menuItems = ref<MenuItem[]>([
label: 'Activos Fijos', label: 'Activos Fijos',
icon: 'pi pi-building', icon: 'pi pi-building',
items: [ items: [
{ label: 'Registro de Activos', icon: 'pi pi-building', to: '/fixed-assets' }, {
{ label: 'Asignacion a Empleado', icon: 'pi pi-send', to: '/fixed-assets/assignments' }, label: 'Registro de Activos',
icon: 'pi pi-building',
to: '/fixed-assets',
permission: ['assets.index', 'assets.show', 'assets.store', 'assets.update', 'assets.destroy'],
},
{
label: 'Asignacion a Empleado',
icon: 'pi pi-send',
to: '/fixed-assets/assignments',
permission: ['assets.assignments.index', 'assets.assignments.store', 'assets.assignments.returnAsset'],
},
], ],
}, },
{ {
@ -126,7 +136,7 @@ const openItems = ref<string[]>([]);
const canAccessItem = (item: MenuItem): boolean => { const canAccessItem = (item: MenuItem): boolean => {
if (!item.permission) { if (!item.permission) {
return false; return true;
} }
return hasPermission(item.permission); return hasPermission(item.permission);

View File

@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import Button from 'primevue/button'; import Button from 'primevue/button';
import Card from 'primevue/card'; import Card from 'primevue/card';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
@ -8,9 +10,13 @@ import IconField from 'primevue/iconfield';
import InputIcon from 'primevue/inputicon'; import InputIcon from 'primevue/inputicon';
import Select from 'primevue/select'; import Select from 'primevue/select';
import Paginator from 'primevue/paginator'; import Paginator from 'primevue/paginator';
import Toast from 'primevue/toast';
import ConfirmDialog from 'primevue/confirmdialog';
import fixedAssetsService, { type Asset, type StatusOption } from '../services/fixedAssetsService'; import fixedAssetsService, { type Asset, type StatusOption } from '../services/fixedAssetsService';
const router = useRouter(); const router = useRouter();
const toast = useToast();
const confirm = useConfirm();
const searchQuery = ref(''); const searchQuery = ref('');
const selectedStatus = ref<number | null>(null); const selectedStatus = ref<number | null>(null);
@ -18,9 +24,7 @@ const rowsPerPage = ref(15);
const currentPage = ref(1); const currentPage = ref(1);
const totalRecords = ref(0); const totalRecords = ref(0);
const assets = ref<Asset[]>([]); const assets = ref<Asset[]>([]);
const statusOptions = ref<{ label: string; value: number | null }[]>([ const statusOptions = ref<{ label: string; value: number | null }[]>([]);
{ label: 'Todos los Estatus', value: null }
]);
const loading = ref(false); const loading = ref(false);
let searchTimeout: ReturnType<typeof setTimeout> | null = null; let searchTimeout: ReturnType<typeof setTimeout> | null = null;
@ -34,10 +38,12 @@ const fetchAssets = async () => {
page: currentPage.value, page: currentPage.value,
}); });
const pageData = response as any; const pageData = response as any;
assets.value = pageData.data ?? []; const pagination = pageData.data?.data;
totalRecords.value = pageData.total ?? 0; assets.value = pagination?.data ?? [];
totalRecords.value = pagination?.total ?? 0;
} catch (error) { } catch (error) {
console.error('Error al cargar activos:', error); console.error('Error al cargar activos:', error);
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar los activos.', life: 4000 });
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -46,10 +52,7 @@ const fetchAssets = async () => {
const fetchStatusOptions = async () => { const fetchStatusOptions = async () => {
try { try {
const options = await fixedAssetsService.getStatusOptions(); const options = await fixedAssetsService.getStatusOptions();
statusOptions.value = [ statusOptions.value = options.map((opt: StatusOption) => ({ label: opt.name, value: opt.id }));
{ label: 'Todos los Estatus', value: null },
...options.map((opt: StatusOption) => ({ label: opt.name, value: opt.id }))
];
} catch (error) { } catch (error) {
console.error('Error al cargar estatus:', error); console.error('Error al cargar estatus:', error);
} }
@ -100,21 +103,38 @@ const goToAssignment = () => {
router.push('/fixed-assets/assignments'); router.push('/fixed-assets/assignments');
}; };
const handleDelete = async (asset: Asset) => { const goToEdit = (asset: Asset) => {
if (!confirm(`¿Estás seguro de eliminar el activo ${asset.sku}?`)) return; router.push(`/fixed-assets/${asset.id}/edit`);
try { };
await fixedAssetsService.deleteAsset(asset.id);
fetchAssets(); const handleDelete = (asset: Asset) => {
} catch (error) { confirm.require({
console.error('Error al eliminar activo:', error); message: `¿Estás seguro de eliminar el activo ${asset.sku}?`,
} header: 'Confirmar eliminación',
icon: 'pi pi-exclamation-triangle',
rejectLabel: 'Cancelar',
acceptLabel: 'Eliminar',
acceptClass: 'p-button-danger',
accept: async () => {
try {
await fixedAssetsService.deleteAsset(asset.id);
toast.add({ severity: 'success', summary: 'Eliminado', detail: `Activo ${asset.sku} eliminado correctamente.`, life: 3000 });
fetchAssets();
} catch (error: any) {
const message = error.response?.data?.message || 'No se pudo eliminar el activo.';
toast.add({ severity: 'error', summary: 'Error', detail: message, life: 4000 });
}
}
});
}; };
</script> </script>
<template> <template>
<section class="space-y-6"> <section class="space-y-6">
<Toast position="bottom-right" />
<ConfirmDialog />
<!-- Header --> <!-- Header -->
<div class="flex flex-wrap justify-between gap-4 items-center"> <div class="flex flex-wrap justify-between gap-4 items-center">
<div class="flex min-w-72 flex-col gap-1"> <div class="flex min-w-72 flex-col gap-1">
<h1 class="text-surface-900 dark:text-white text-3xl md:text-4xl font-black leading-tight tracking-tight"> <h1 class="text-surface-900 dark:text-white text-3xl md:text-4xl font-black leading-tight tracking-tight">
@ -164,6 +184,8 @@ const handleDelete = async (asset: Asset) => {
:options="statusOptions" :options="statusOptions"
optionLabel="label" optionLabel="label"
optionValue="value" optionValue="value"
placeholder="Todos los Estatus"
showClear
class="min-w-44" class="min-w-44"
/> />
</div> </div>
@ -233,8 +255,7 @@ const handleDelete = async (asset: Asset) => {
</td> </td>
<td class="px-4 py-4"> <td class="px-4 py-4">
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Button icon="pi pi-pencil" text rounded size="small" /> <Button icon="pi pi-pencil" text rounded size="small" @click="goToEdit(asset)" />
<Button icon="pi pi-qrcode" text rounded size="small" />
<Button icon="pi pi-trash" text rounded size="small" severity="danger" @click="handleDelete(asset)" /> <Button icon="pi pi-trash" text rounded size="small" severity="danger" @click="handleDelete(asset)" />
</div> </div>
</td> </td>

View File

@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
import Button from 'primevue/button'; import Button from 'primevue/button';
import Card from 'primevue/card';
import FixedAssetGeneralInfoSection from './FixedAssetGeneralInfoSection.vue'; import FixedAssetGeneralInfoSection from './FixedAssetGeneralInfoSection.vue';
import FixedAssetAcquisitionSection from './FixedAssetAcquisitionSection.vue'; import FixedAssetAcquisitionSection from './FixedAssetAcquisitionSection.vue';
import FixedAssetAssignmentSection from './FixedAssetAssignmentSection.vue'; import FixedAssetAssignmentSection from './FixedAssetAssignmentSection.vue';
@ -11,8 +12,14 @@ import fixedAssetsService from '../../services/fixedAssetsService';
import type { FixedAssetFormData } from '../../types/fixedAsset'; import type { FixedAssetFormData } from '../../types/fixedAsset';
const router = useRouter(); const router = useRouter();
const route = useRoute();
const toast = useToast(); const toast = useToast();
const saving = ref(false); const saving = ref(false);
const loading = ref(false);
const isEditing = computed(() => !!route.params.id);
const assetId = computed(() => isEditing.value ? Number(route.params.id) : null);
const currentProductName = ref('');
const form = ref<FixedAssetFormData>({ const form = ref<FixedAssetFormData>({
inventory_warehouse_id: null, inventory_warehouse_id: null,
@ -24,18 +31,44 @@ const form = ref<FixedAssetFormData>({
warranty_end_date: '' warranty_end_date: ''
}); });
const loadAsset = async () => {
if (!assetId.value) return;
loading.value = true;
try {
const response = await fixedAssetsService.getAsset(assetId.value);
const asset = response.data.data;
form.value = {
inventory_warehouse_id: asset.inventory_warehouse?.id ?? null,
estimated_useful_life: asset.estimated_useful_life,
depreciation_method: asset.depreciation_method ?? 'straight_line',
residual_value: asset.residual_value ? parseFloat(asset.residual_value) : null,
asset_tag: asset.asset_tag ?? '',
warranty_days: asset.warranty_days,
warranty_end_date: asset.warranty_end_date ?? ''
};
currentProductName.value = asset.inventory_warehouse?.product?.name ?? '';
} catch {
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo cargar el activo.', life: 4000 });
} finally {
loading.value = false;
}
};
onMounted(() => {
if (isEditing.value) loadAsset();
});
const cancel = () => { const cancel = () => {
router.push('/api/fixed-assets'); router.push('/fixed-assets');
}; };
const saveAsset = async () => { const saveAsset = async () => {
if (!form.value.inventory_warehouse_id || !form.value.estimated_useful_life) { if (!isEditing.value && !form.value.inventory_warehouse_id) {
toast.add({ toast.add({ severity: 'warn', summary: 'Campo requerido', detail: 'Selecciona un producto del almacén.', life: 3000 });
severity: 'warn', return;
summary: 'Campos requeridos', }
detail: 'Selecciona un producto de almacen e indica la vida util estimada.', if (!form.value.estimated_useful_life) {
life: 3000 toast.add({ severity: 'warn', summary: 'Campo requerido', detail: 'Indica la vida útil estimada.', life: 3000 });
});
return; return;
} }
@ -52,24 +85,18 @@ const saveAsset = async () => {
if (form.value.warranty_days != null) payload.warranty_days = form.value.warranty_days; if (form.value.warranty_days != null) payload.warranty_days = form.value.warranty_days;
if (form.value.warranty_end_date) payload.warranty_end_date = form.value.warranty_end_date; if (form.value.warranty_end_date) payload.warranty_end_date = form.value.warranty_end_date;
await fixedAssetsService.createAsset(payload); if (isEditing.value && assetId.value) {
await fixedAssetsService.updateAsset(assetId.value, payload);
toast.add({ severity: 'success', summary: 'Activo actualizado', detail: 'Los cambios se guardaron correctamente.', life: 2600 });
} else {
await fixedAssetsService.createAsset(payload);
toast.add({ severity: 'success', summary: 'Activo registrado', detail: 'El activo fijo se registró correctamente.', life: 2600 });
}
toast.add({ setTimeout(() => router.push('/fixed-assets'), 2600);
severity: 'success',
summary: 'Activo registrado',
detail: 'El activo fijo se registro correctamente.',
life: 2600
});
router.push('/api/fixed-assets');
} catch (error: any) { } catch (error: any) {
const message = error.response?.data?.message || 'Error al registrar el activo.'; const message = error.response?.data?.message || 'Error al guardar el activo.';
toast.add({ toast.add({ severity: 'error', summary: 'Error', detail: message, life: 4000 });
severity: 'error',
summary: 'Error',
detail: message,
life: 4000
});
} finally { } finally {
saving.value = false; saving.value = false;
} }
@ -82,28 +109,49 @@ const saveAsset = async () => {
<div> <div>
<h1 class="text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0"> <h1 class="text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0">
Registrar Nuevo Activo Fijo {{ isEditing ? 'Editar Activo Fijo' : 'Registrar Nuevo Activo Fijo' }}
</h1> </h1>
<p class="mt-1 text-surface-500 dark:text-surface-400"> <p class="mt-1 text-surface-500 dark:text-surface-400">
Selecciona un producto del inventario de almacen para darlo de alta como activo fijo. {{ isEditing ? 'Modifica los datos del activo fijo.' : 'Selecciona un producto del inventario de almacén para darlo de alta como activo fijo.' }}
</p> </p>
</div> </div>
<FixedAssetGeneralInfoSection :form="form" /> <div v-if="loading" class="flex items-center justify-center py-12 text-surface-500">
<i class="pi pi-spin pi-spinner mr-2 text-xl"></i> Cargando activo...
<div class="grid grid-cols-1 gap-5 xl:grid-cols-2">
<FixedAssetAcquisitionSection :form="form" />
<FixedAssetAssignmentSection :form="form" />
</div> </div>
<div class="flex flex-wrap items-center justify-end gap-3"> <template v-else>
<Button label="Cancelar" text severity="secondary" @click="cancel" /> <FixedAssetGeneralInfoSection v-if="!isEditing" :form="form" />
<Button
label="Guardar Registro" <Card v-else class="shadow-sm">
icon="pi pi-save" <template #title>
:loading="saving" <div class="flex items-center gap-2 text-xl">
@click="saveAsset" <i class="pi pi-info-circle text-primary"></i>
/> <span>Información General</span>
</div> </div>
</template>
<template #content>
<div class="space-y-1">
<p class="text-sm text-surface-500">Producto</p>
<p class="font-semibold text-surface-900 dark:text-surface-0">{{ currentProductName || '—' }}</p>
</div>
</template>
</Card>
<div class="grid grid-cols-1 gap-5 xl:grid-cols-2">
<FixedAssetAcquisitionSection :form="form" />
<FixedAssetAssignmentSection :form="form" />
</div>
<div class="flex flex-wrap items-center justify-end gap-3">
<Button label="Cancelar" text severity="secondary" @click="cancel" />
<Button
:label="isEditing ? 'Guardar Cambios' : 'Guardar Registro'"
icon="pi pi-save"
:loading="saving"
@click="saveAsset"
/>
</div>
</template>
</section> </section>
</template> </template>

View File

@ -7,14 +7,14 @@ import type { AssignmentAssetOption } from '../../types/fixedAssetAssignment';
interface Props { interface Props {
assets: AssignmentAssetOption[]; assets: AssignmentAssetOption[];
searchTerm: string; searchTerm: string;
selectedAssetId: string; selectedAssetId: number | null;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:searchTerm', value: string): void; (e: 'update:searchTerm', value: string): void;
(e: 'update:selectedAssetId', value: string): void; (e: 'update:selectedAssetId', value: number): void;
}>(); }>();
const selectedAsset = computed(() => const selectedAsset = computed(() =>

View File

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Card from 'primevue/card'; import Card from 'primevue/card';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import Select from 'primevue/select';
import Textarea from 'primevue/textarea'; import Textarea from 'primevue/textarea';
import type { FixedAssetAssignmentFormData } from '../../types/fixedAssetAssignment'; import type { FixedAssetAssignmentFormData } from '../../types/fixedAssetAssignment';
@ -10,12 +9,6 @@ interface Props {
} }
defineProps<Props>(); defineProps<Props>();
const conditionOptions = [
{ label: 'Excelente', value: 'Excelente' },
{ label: 'Bueno', value: 'Bueno' },
{ label: 'Regular', value: 'Regular' }
];
</script> </script>
<template> <template>
@ -31,21 +24,11 @@ const conditionOptions = [
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Fecha de Entrega</label> <label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Fecha de Entrega</label>
<InputText <InputText
v-model="form.deliveredAt" v-model="form.assignedAt"
type="date" type="date"
class="w-full" class="w-full"
/> />
</div> </div>
<div class="space-y-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Condicion del Activo</label>
<Select
v-model="form.condition"
:options="conditionOptions"
optionLabel="label"
optionValue="value"
class="w-full"
/>
</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

@ -7,14 +7,14 @@ import type { AssignmentEmployeeOption } from '../../types/fixedAssetAssignment'
interface Props { interface Props {
employees: AssignmentEmployeeOption[]; employees: AssignmentEmployeeOption[];
searchTerm: string; searchTerm: string;
selectedEmployeeId: string; selectedEmployeeId: number | null;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:searchTerm', value: string): void; (e: 'update:searchTerm', value: string): void;
(e: 'update:selectedEmployeeId', value: string): void; (e: 'update:selectedEmployeeId', value: number): void;
}>(); }>();
const visibleEmployees = computed(() => { const visibleEmployees = computed(() => {
@ -23,7 +23,6 @@ const visibleEmployees = computed(() => {
return props.employees.filter((employee) => return props.employees.filter((employee) =>
employee.fullName.toLowerCase().includes(query) employee.fullName.toLowerCase().includes(query)
|| employee.id.toLowerCase().includes(query)
|| employee.department.toLowerCase().includes(query) || employee.department.toLowerCase().includes(query)
); );
}); });

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
@ -12,42 +12,63 @@ import type {
AssignmentEmployeeOption, AssignmentEmployeeOption,
FixedAssetAssignmentFormData FixedAssetAssignmentFormData
} from '../../types/fixedAssetAssignment'; } from '../../types/fixedAssetAssignment';
import { fixedAssetsService } from '../../services/fixedAssetsService';
import { employeesService } from '@/modules/rh/components/employees/employees.services';
const router = useRouter(); const router = useRouter();
const toast = useToast(); const toast = useToast();
const loading = ref(false); const loading = ref(false);
const loadingData = ref(false);
const assetSearch = ref(''); const assetSearch = ref('');
const employeeSearch = ref(''); const employeeSearch = ref('');
const assets = ref<AssignmentAssetOption[]>([ const assets = ref<AssignmentAssetOption[]>([]);
{ id: '1', code: 'ACT-0001', name: 'Laptop Dell XPS 15', serial: 'DELL-8829-XP', category: 'Computo' }, const employees = ref<AssignmentEmployeeOption[]>([]);
{ id: '2', code: 'ACT-0042', name: 'Montacargas Hidraulico', serial: 'MH-1029', category: 'Maquinaria' },
{ id: '3', code: 'ACT-0056', name: 'Escritorio Ergonomico', serial: 'MOB-221', category: 'Mobiliario' }
]);
const employees = ref<AssignmentEmployeeOption[]>([
{ id: 'EMP-001', initials: 'RM', fullName: 'Roberto Mendez', role: 'Operador de Almacen', department: 'Logistica' },
{ id: 'EMP-008', initials: 'AS', fullName: 'Ana Salinas', role: 'Supervisora de Piso', department: 'Operaciones' },
{ id: 'EMP-017', initials: 'CG', fullName: 'Carlos Guerrero', role: 'Tecnico de Mantenimiento', department: 'Mantenimiento' }
]);
const form = ref<FixedAssetAssignmentFormData>({ const form = ref<FixedAssetAssignmentFormData>({
assetId: '', assetId: null,
employeeId: '', employeeId: null,
deliveredAt: new Date().toISOString().slice(0, 10), assignedAt: new Date().toISOString().slice(0, 10),
condition: 'Excelente',
notes: '' notes: ''
}); });
const filteredAssets = computed(() => { onMounted(async () => {
const query = assetSearch.value.trim().toLowerCase(); loadingData.value = true;
if (!query) return assets.value; try {
const [assetsRes, employeesRes] = await Promise.all([
fixedAssetsService.getAssets({ paginate: false, status: 1 }),
employeesService.getEmployees({ paginate: false }),
]);
return assets.value.filter((asset) => const allAssets = (assetsRes as any).data?.data ?? [];
asset.name.toLowerCase().includes(query) assets.value = allAssets
|| asset.serial.toLowerCase().includes(query) .filter((a: any) => !a.active_assignment)
|| asset.code.toLowerCase().includes(query) .map((a: any): AssignmentAssetOption => ({
); id: a.id,
code: a.sku,
name: a.inventory_warehouse?.product?.name ?? a.sku,
serial: a.inventory_warehouse?.product?.serial_number ?? '—',
category: a.inventory_warehouse?.product?.category ?? '—',
}));
const allEmployees = (employeesRes as any).data ?? [];
employees.value = allEmployees.map((e: any): AssignmentEmployeeOption => ({
id: e.id,
initials: `${e.name[0]}${e.paternal[0]}`.toUpperCase(),
fullName: `${e.name} ${e.paternal} ${e.maternal}`.trim(),
role: e.job_position?.name ?? '—',
department: e.department?.name ?? '—',
}));
} catch {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudieron cargar los datos. Intente de nuevo.',
life: 4000
});
} finally {
loadingData.value = false;
}
}); });
const cancel = () => router.push('/fixed-assets/assignments'); const cancel = () => router.push('/fixed-assets/assignments');
@ -57,24 +78,34 @@ const save = async () => {
toast.add({ toast.add({
severity: 'warn', severity: 'warn',
summary: 'Campos pendientes', summary: 'Campos pendientes',
detail: 'Seleccione activo y colaborador para confirmar la asignacion.', detail: 'Seleccione un activo y un colaborador para confirmar la asignacion.',
life: 3000 life: 3000
}); });
return; return;
} }
loading.value = true; loading.value = true;
await new Promise((resolve) => setTimeout(resolve, 500)); try {
loading.value = false; await fixedAssetsService.assignAsset(form.value.assetId, {
employee_id: form.value.employeeId,
assigned_at: form.value.assignedAt,
notes: form.value.notes || undefined,
});
toast.add({ toast.add({
severity: 'success', severity: 'success',
summary: 'Asignacion registrada', summary: 'Asignacion registrada',
detail: 'La asignacion de activo al colaborador se registro correctamente.', detail: 'La asignacion del activo al colaborador se registro correctamente.',
life: 2500 life: 2500
}); });
router.push('/fixed-assets/assignments'); router.push('/fixed-assets/assignments');
} catch (error: any) {
const message = error?.response?.data?.message ?? 'Error al registrar la asignacion.';
toast.add({ severity: 'error', summary: 'Error', detail: message, life: 4000 });
} finally {
loading.value = false;
}
}; };
</script> </script>
@ -92,9 +123,10 @@ const save = async () => {
</div> </div>
<AssignmentAssetSelectorCard <AssignmentAssetSelectorCard
:assets="filteredAssets" :assets="assets"
:search-term="assetSearch" :search-term="assetSearch"
:selected-asset-id="form.assetId" :selected-asset-id="form.assetId"
:loading="loadingData"
@update:search-term="assetSearch = $event" @update:search-term="assetSearch = $event"
@update:selected-asset-id="form.assetId = $event" @update:selected-asset-id="form.assetId = $event"
/> />
@ -115,6 +147,7 @@ const save = async () => {
label="Confirmar Asignacion" label="Confirmar Asignacion"
icon="pi pi-check-circle" icon="pi pi-check-circle"
:loading="loading" :loading="loading"
:disabled="loadingData"
@click="save" @click="save"
/> />
</div> </div>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
@ -7,40 +7,23 @@ import Button from 'primevue/button';
import AssignmentOffboardingSummaryCard from './offboarding/AssignmentOffboardingSummaryCard.vue'; import AssignmentOffboardingSummaryCard from './offboarding/AssignmentOffboardingSummaryCard.vue';
import AssignmentOffboardingEventCard from './offboarding/AssignmentOffboardingEventCard.vue'; import AssignmentOffboardingEventCard from './offboarding/AssignmentOffboardingEventCard.vue';
import AssignmentOffboardingEvidenceCard from './offboarding/AssignmentOffboardingEvidenceCard.vue'; import AssignmentOffboardingEvidenceCard from './offboarding/AssignmentOffboardingEvidenceCard.vue';
import { fixedAssetsService } from '../../services/fixedAssetsService';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const toast = useToast(); const toast = useToast();
const assetId = Number(route.params.assetId);
const assignmentId = Number(route.params.assignmentId);
const loading = ref(false); const loading = ref(false);
const loadingData = ref(false);
const defaultAssignment = { const summary = ref({
id: 'AS-DEFAULT', assetCode: '...',
assetCode: 'WH-NA-000', assetName: '...',
assetName: 'Activo no encontrado', serial: '...',
serial: 'N/A', custodian: '...',
custodian: 'Sin custodio'
};
const assignmentsMock = [
{
id: 'AS-00124',
assetCode: 'WH-LAP-2023-042',
assetName: `MacBook Pro 14" M2 Max`,
serial: 'SN-48291048-X',
custodian: 'Carlos Rodriguez'
},
{
id: 'AS-00125',
assetCode: 'WH-LAP-2023-051',
assetName: 'Laptop Latitude 5420',
serial: 'SN-22345008-K',
custodian: 'Ana Salinas'
}
];
const currentAssignment = computed(() => {
const id = String(route.params.id || '').replace('#', '');
return assignmentsMock.find((item) => item.id === id) ?? assignmentsMock[0] ?? defaultAssignment;
}); });
const form = ref({ const form = ref({
@ -48,7 +31,37 @@ const form = ref({
happenedAt: new Date().toISOString().slice(0, 10), happenedAt: new Date().toISOString().slice(0, 10),
details: '', details: '',
finalStatus: '', finalStatus: '',
evidenceFileName: '' evidenceFileName: '',
});
// Mapa de finalStatus a AssetStatusEk (1=ACTIVE, 2=INACTIVE, 3=UNDER_MAINTENANCE, 4=DISPOSED)
const finalStatusToAssetStatus: Record<string, number> = {
'Devuelto a almacen': 1,
'Reasignacion pendiente': 1,
'En reparacion': 3,
'Baja definitiva': 4,
};
onMounted(async () => {
loadingData.value = true;
try {
const res = await fixedAssetsService.getAsset(assetId);
const asset = res.data.data;
const assignment = asset.active_assignment;
summary.value = {
assetCode: asset.sku,
assetName: asset.inventory_warehouse?.product?.name ?? asset.sku,
serial: asset.inventory_warehouse?.product?.serial_number ?? '—',
custodian: assignment?.employee
? `${assignment.employee.name} ${(assignment.employee as any).paternal ?? ''}`.trim()
: '—',
};
} catch {
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo cargar la informacion del activo.', life: 4000 });
} finally {
loadingData.value = false;
}
}); });
const cancel = () => router.push('/fixed-assets/assignments'); const cancel = () => router.push('/fixed-assets/assignments');
@ -59,23 +72,39 @@ const confirmOffboarding = async () => {
severity: 'warn', severity: 'warn',
summary: 'Campos obligatorios', summary: 'Campos obligatorios',
detail: 'Complete motivo, fecha, descripcion y estado final.', detail: 'Complete motivo, fecha, descripcion y estado final.',
life: 3000 life: 3000,
}); });
return; return;
} }
loading.value = true; loading.value = true;
await new Promise((resolve) => setTimeout(resolve, 600)); try {
loading.value = false; const notes = `${form.value.reason}: ${form.value.details}`;
toast.add({ await fixedAssetsService.returnAsset(assetId, assignmentId, {
severity: 'success', returned_at: form.value.happenedAt,
summary: 'Baja confirmada', notes,
detail: 'La baja de asignacion se registro correctamente.', });
life: 2500
});
router.push('/fixed-assets/assignments'); const newStatus = finalStatusToAssetStatus[form.value.finalStatus];
if (newStatus && newStatus !== 1) {
await fixedAssetsService.updateAsset(assetId, { status: newStatus });
}
toast.add({
severity: 'success',
summary: 'Devolucion registrada',
detail: 'La devolucion del activo se registro correctamente.',
life: 2500,
});
router.push('/fixed-assets/assignments');
} catch (error: any) {
const message = error?.response?.data?.message ?? 'Error al registrar la devolucion.';
toast.add({ severity: 'error', summary: 'Error', detail: message, life: 4000 });
} finally {
loading.value = false;
}
}; };
</script> </script>
@ -93,10 +122,10 @@ const confirmOffboarding = async () => {
</div> </div>
<AssignmentOffboardingSummaryCard <AssignmentOffboardingSummaryCard
:asset-code="currentAssignment.assetCode" :asset-code="summary.assetCode"
:asset-name="currentAssignment.assetName" :asset-name="summary.assetName"
:serial="currentAssignment.serial" :serial="summary.serial"
:custodian="currentAssignment.custodian" :custodian="summary.custodian"
/> />
<AssignmentOffboardingEventCard :form="form" /> <AssignmentOffboardingEventCard :form="form" />
@ -109,6 +138,7 @@ const confirmOffboarding = async () => {
icon="pi pi-times-circle" icon="pi pi-times-circle"
severity="danger" severity="danger"
:loading="loading" :loading="loading"
:disabled="loadingData"
@click="confirmOffboarding" @click="confirmOffboarding"
/> />
</div> </div>

View File

@ -1,130 +1,89 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import Toast from 'primevue/toast';
import Button from 'primevue/button'; import Button from 'primevue/button';
import Card from 'primevue/card'; import Card from 'primevue/card';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import Select from 'primevue/select'; import Select from 'primevue/select';
import Paginator from 'primevue/paginator'; import Paginator from 'primevue/paginator';
import Tag from 'primevue/tag'; import Tag from 'primevue/tag';
import { fixedAssetsService, type AssetAssignment } from '../../services/fixedAssetsService';
interface AssignmentRow {
id: string;
assetName: string;
assetSerial: string;
employeeName: string;
department: string;
deliveryDate: string;
expectedReturn: string;
condition: 'Excelente' | 'Bueno' | 'Regular';
status: 'Activo' | 'Vencido' | 'Devuelto';
}
const router = useRouter(); const router = useRouter();
const toast = useToast();
const assignments = ref<AssetAssignment[]>([]);
const loading = ref(false);
const searchTerm = ref(''); const searchTerm = ref('');
const selectedStatus = ref<'all' | AssignmentRow['status']>('all'); const selectedStatus = ref<number | 'all'>('all');
const first = ref(0); const currentPage = ref(1);
const rows = ref(5); const totalRecords = ref(0);
const rows = ref(15);
const statusOptions = [ const statusOptions = [
{ label: 'Todos', value: 'all' }, { label: 'Todos', value: 'all' },
{ label: 'Activo', value: 'Activo' }, { label: 'Activo', value: 1 },
{ label: 'Vencido', value: 'Vencido' }, { label: 'Devuelto', value: 2 },
{ label: 'Devuelto', value: 'Devuelto' }
]; ];
const assignments = ref<AssignmentRow[]>([ const statusSeverity = (statusId: number) => {
{ if (statusId === 1) return 'info';
id: '#AS-00124', return 'secondary';
assetName: 'Montacargas Electrico', };
assetSerial: 'TY-98231',
employeeName: 'Roberto Mendez', const formatDate = (dateStr: string | null) => {
department: 'Logistica', if (!dateStr) return 'N/A';
deliveryDate: '20/Oct/2023', return new Date(dateStr).toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' });
expectedReturn: 'N/A', };
condition: 'Excelente',
status: 'Activo' const employeeFullName = (assignment: AssetAssignment) => {
}, const e = assignment.employee;
{ if (!e) return '—';
id: '#AS-00125', return `${e.name} ${e.paternal} ${e.maternal ?? ''}`.trim();
assetName: 'Laptop Latitude 5420', };
assetSerial: 'DL-22345',
employeeName: 'Ana Salinas', const loadAssignments = async () => {
department: 'Administracion', loading.value = true;
deliveryDate: '15/Nov/2023', try {
expectedReturn: '15/Dic/2023', const res = await fixedAssetsService.getAssignments({
condition: 'Bueno', q: searchTerm.value || undefined,
status: 'Vencido' status: selectedStatus.value !== 'all' ? selectedStatus.value : undefined,
}, page: currentPage.value,
{ });
id: '#AS-00126', assignments.value = res.data.data.data;
assetName: 'Escaner Zebra', totalRecords.value = res.data.data.total;
assetSerial: 'ZB-77788', } catch {
employeeName: 'Carlos Ruiz', toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar las asignaciones.', life: 4000 });
department: 'Almacen', } finally {
deliveryDate: '10/Oct/2023', loading.value = false;
expectedReturn: '10/Nov/2023',
condition: 'Regular',
status: 'Devuelto'
},
{
id: '#AS-00127',
assetName: 'Tablet Samsung A8',
assetSerial: 'SM-99288',
employeeName: 'Lucia Ponce',
department: 'Operaciones',
deliveryDate: '02/Ene/2024',
expectedReturn: 'N/A',
condition: 'Excelente',
status: 'Activo'
} }
]);
const conditionSeverity = (condition: AssignmentRow['condition']) => {
if (condition === 'Excelente') return 'success';
if (condition === 'Bueno') return 'warning';
return 'secondary';
}; };
const statusSeverity = (status: AssignmentRow['status']) => { const onSearch = () => {
if (status === 'Activo') return 'info'; currentPage.value = 1;
if (status === 'Vencido') return 'danger'; loadAssignments();
return 'secondary';
}; };
const filteredAssignments = computed(() => { const onPage = (event: { page: number }) => {
const query = searchTerm.value.trim().toLowerCase(); currentPage.value = event.page + 1;
return assignments.value.filter((assignment) => { loadAssignments();
const matchesQuery = !query
|| assignment.assetName.toLowerCase().includes(query)
|| assignment.employeeName.toLowerCase().includes(query)
|| assignment.id.toLowerCase().includes(query);
const matchesStatus = selectedStatus.value === 'all' || assignment.status === selectedStatus.value;
return matchesQuery && matchesStatus;
});
});
const paginatedAssignments = computed(() =>
filteredAssignments.value.slice(first.value, first.value + rows.value)
);
const onPage = (event: { first: number; rows: number }) => {
first.value = event.first;
rows.value = event.rows;
}; };
const goToCreate = () => { const goToCreate = () => router.push('/fixed-assets/assignments/create');
router.push('/fixed-assets/assignments/create');
const goToOffboarding = (assignment: AssetAssignment) => {
router.push(`/fixed-assets/assignments/${assignment.asset_id}/${assignment.id}/offboarding`);
}; };
const goToOffboarding = (assignmentId: string) => { onMounted(loadAssignments);
const cleanId = assignmentId.replace('#', '');
router.push(`/fixed-assets/assignments/${cleanId}/offboarding`);
};
</script> </script>
<template> <template>
<section class="space-y-6"> <section class="space-y-6">
<Toast position="bottom-right" />
<div class="flex flex-wrap items-center justify-between gap-4"> <div class="flex flex-wrap items-center justify-between gap-4">
<div> <div>
<h1 class="mt-2 text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0"> <h1 class="mt-2 text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0">
@ -145,10 +104,11 @@ const goToOffboarding = (assignmentId: string) => {
v-model="searchTerm" v-model="searchTerm"
class="w-full lg:max-w-sm" class="w-full lg:max-w-sm"
placeholder="Buscar por activo o empleado..." placeholder="Buscar por activo o empleado..."
@keyup.enter="onSearch"
/> />
<div class="flex w-full flex-wrap items-center gap-2 lg:w-auto"> <div class="flex w-full flex-wrap items-center gap-2 lg:w-auto">
<span class="text-sm font-medium text-surface-600 dark:text-surface-300"> <span class="text-sm font-medium text-surface-600 dark:text-surface-300">
Estatus de Asignacion: Estatus:
</span> </span>
<Select <Select
v-model="selectedStatus" v-model="selectedStatus"
@ -156,12 +116,7 @@ const goToOffboarding = (assignmentId: string) => {
optionLabel="label" optionLabel="label"
optionValue="value" optionValue="value"
class="w-full lg:w-44" class="w-full lg:w-44"
/> @change="onSearch"
<Button
label="Filtros Avanzados"
icon="pi pi-filter"
severity="secondary"
outlined
/> />
</div> </div>
</div> </div>
@ -170,74 +125,86 @@ const goToOffboarding = (assignmentId: string) => {
<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 Asignacion</th> <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">Entrega</th> <th class="px-4 py-3">Fecha Entrega</th>
<th class="px-4 py-3">Retorno Previsto</th> <th class="px-4 py-3">Fecha Devolucion</th>
<th class="px-4 py-3">Condicion</th>
<th class="px-4 py-3">Estado</th> <th class="px-4 py-3">Estado</th>
<th class="px-4 py-3 text-right">Accion</th> <th class="px-4 py-3 text-right">Accion</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr <tr v-if="loading">
v-for="assignment in paginatedAssignments" <td colspan="7" class="px-4 py-8 text-center text-surface-500">
:key="assignment.id" Cargando...
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>
<td class="px-4 py-3"> </tr>
<p class="font-semibold text-surface-900 dark:text-surface-0">{{ assignment.assetName }}</p> <template v-else>
<p class="text-xs text-surface-500 dark:text-surface-400">SN: {{ assignment.assetSerial }}</p> <tr
</td> v-for="assignment in assignments"
<td class="px-4 py-3"> :key="assignment.id"
<p class="font-semibold text-surface-900 dark:text-surface-0">{{ assignment.employeeName }}</p> class="border-t border-surface-200 text-sm dark:border-surface-700"
<p class="text-xs text-surface-500 dark:text-surface-400">{{ assignment.department }}</p> >
</td> <td class="px-4 py-3 font-medium text-surface-800 dark:text-surface-100">
<td class="px-4 py-3">{{ assignment.deliveryDate }}</td> #{{ assignment.id }}
<td class="px-4 py-3">{{ assignment.expectedReturn }}</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<Tag :value="assignment.condition" :severity="conditionSeverity(assignment.condition)" /> <p class="font-semibold text-surface-900 dark:text-surface-0">
</td> {{ assignment.asset?.inventory_warehouse?.product?.name ?? assignment.asset?.sku ?? '—' }}
<td class="px-4 py-3"> </p>
<Tag :value="assignment.status" :severity="statusSeverity(assignment.status)" /> <p class="text-xs text-surface-500 dark:text-surface-400">
</td> {{ assignment.asset?.sku }}
<td class="px-4 py-3 text-right"> </p>
<div class="flex items-center justify-end gap-1"> </td>
<Button icon="pi pi-eye" text rounded size="small" /> <td class="px-4 py-3">
<Button <p class="font-semibold text-surface-900 dark:text-surface-0">
icon="pi pi-times-circle" {{ employeeFullName(assignment) }}
text </p>
rounded <p class="text-xs text-surface-500 dark:text-surface-400">
size="small" {{ assignment.employee?.department?.name ?? '—' }}
severity="danger" </p>
v-tooltip.top="'Dar de baja asignacion'" </td>
@click="goToOffboarding(assignment.id)" <td class="px-4 py-3">{{ formatDate(assignment.assigned_at) }}</td>
<td class="px-4 py-3">{{ formatDate(assignment.returned_at) }}</td>
<td class="px-4 py-3">
<Tag
:value="assignment.status.name"
:severity="statusSeverity(assignment.status.id)"
/> />
</div> </td>
</td> <td class="px-4 py-3 text-right">
</tr> <div class="flex items-center justify-end gap-1">
<tr v-if="paginatedAssignments.length === 0"> <Button
<td colspan="8" class="px-4 py-8 text-center text-surface-500 dark:text-surface-400"> v-if="assignment.status.id === 1"
No hay asignaciones para mostrar. icon="pi pi-times-circle"
</td> text
</tr> rounded
size="small"
severity="danger"
v-tooltip.top="'Registrar devolucion'"
@click="goToOffboarding(assignment)"
/>
</div>
</td>
</tr>
<tr v-if="assignments.length === 0">
<td colspan="7" class="px-4 py-8 text-center text-surface-500 dark:text-surface-400">
No hay asignaciones para mostrar.
</td>
</tr>
</template>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between"> <div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<p class="text-sm text-surface-500 dark:text-surface-400"> <p class="text-sm text-surface-500 dark:text-surface-400">
Mostrando {{ paginatedAssignments.length }} de {{ filteredAssignments.length }} asignaciones {{ totalRecords }} asignacion(es) en total
</p> </p>
<Paginator <Paginator
:first="first"
:rows="rows" :rows="rows"
:totalRecords="filteredAssignments.length" :totalRecords="totalRecords"
:rowsPerPageOptions="[5, 10, 20]"
template="PrevPageLink PageLinks NextPageLink" template="PrevPageLink PageLinks NextPageLink"
@page="onPage" @page="onPage"
/> />

View File

@ -78,6 +78,55 @@ interface AssetFilters {
paginate?: boolean; paginate?: boolean;
} }
export interface AssetAssignment {
id: number;
asset_id: number;
employee_id: number;
assigned_at: string;
returned_at: string | null;
receipt_folio: string | null;
notes: string | null;
status: { id: number; name: string };
asset: Asset | null;
employee: {
id: number;
name: string;
paternal: string;
maternal: string;
department: { id: number; name: string } | null;
} | null;
}
export interface AssetAssignmentResponse {
status: string;
data: { data: AssetAssignment };
}
export interface AssetAssignmentsPaginatedResponse {
status: string;
data: {
data: {
current_page: number;
data: AssetAssignment[];
last_page: number;
per_page: number;
total: number;
};
};
}
interface AssignAssetData {
employee_id: number;
assigned_at?: string;
receipt_folio?: string;
notes?: string;
}
interface ReturnAssetData {
returned_at?: string;
notes?: string;
}
class FixedAssetsService { class FixedAssetsService {
async getAssets(filters: AssetFilters = {}): Promise<AssetsPaginatedResponse> { async getAssets(filters: AssetFilters = {}): Promise<AssetsPaginatedResponse> {
const params: Record<string, string | number | boolean> = {}; const params: Record<string, string | number | boolean> = {};
@ -114,6 +163,27 @@ class FixedAssetsService {
const response = await api.get<StatusOptionsResponse>('/api/assets/options/status'); const response = await api.get<StatusOptionsResponse>('/api/assets/options/status');
return response.data.data.data; return response.data.data.data;
} }
async getAssignments(filters: { q?: string; status?: number; page?: number; paginate?: boolean } = {}): Promise<AssetAssignmentsPaginatedResponse> {
const params: Record<string, string | number | boolean> = {};
if (filters.q) params.q = filters.q;
if (filters.status !== undefined) params.status = filters.status;
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 });
return response.data;
}
async assignAsset(assetId: number, data: AssignAssetData): Promise<AssetAssignmentResponse> {
const response = await api.post<AssetAssignmentResponse>(`/api/assets/${assetId}/assignments`, data);
return response.data;
}
async returnAsset(assetId: number, assignmentId: number, data: ReturnAssetData): Promise<AssetAssignmentResponse> {
const response = await api.put<AssetAssignmentResponse>(`/api/assets/${assetId}/assignments/${assignmentId}/return`, data);
return response.data;
}
} }
export const fixedAssetsService = new FixedAssetsService(); export const fixedAssetsService = new FixedAssetsService();

View File

@ -1,5 +1,5 @@
export interface AssignmentAssetOption { export interface AssignmentAssetOption {
id: string; id: number;
code: string; code: string;
name: string; name: string;
serial: string; serial: string;
@ -7,7 +7,7 @@ export interface AssignmentAssetOption {
} }
export interface AssignmentEmployeeOption { export interface AssignmentEmployeeOption {
id: string; id: number;
initials: string; initials: string;
fullName: string; fullName: string;
role: string; role: string;
@ -15,9 +15,8 @@ export interface AssignmentEmployeeOption {
} }
export interface FixedAssetAssignmentFormData { export interface FixedAssetAssignmentFormData {
assetId: string; assetId: number | null;
employeeId: string; employeeId: number | null;
deliveredAt: string; assignedAt: string;
condition: string;
notes: string; notes: string;
} }

View File

@ -284,6 +284,15 @@ const routes: RouteRecordRaw[] = [
requiresAuth: true requiresAuth: true
} }
}, },
{
path: ':id/edit',
name: 'FixedAssetEdit',
component: FixedAssetForm,
meta: {
title: 'Editar Activo Fijo',
requiresAuth: true
}
},
{ {
path: 'assignments', path: 'assignments',
name: 'FixedAssetAssignmentsModule', name: 'FixedAssetAssignmentsModule',
@ -311,7 +320,7 @@ const routes: RouteRecordRaw[] = [
} }
}, },
{ {
path: ':id/offboarding', path: ':assetId/:assignmentId/offboarding',
name: 'FixedAssetAssignmentOffboarding', name: 'FixedAssetAssignmentOffboarding',
component: FixedAssetAssignmentOffboardingForm, component: FixedAssetAssignmentOffboardingForm,
meta: { meta: {