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:
parent
29e4497ff1
commit
c85200ed64
1
components.d.ts
vendored
1
components.d.ts
vendored
@ -35,6 +35,7 @@ declare module 'vue' {
|
||||
ProgressSpinner: typeof import('primevue/progressspinner')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Select: typeof import('primevue/select')['default']
|
||||
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
|
||||
Tag: typeof import('primevue/tag')['default']
|
||||
Toast: typeof import('primevue/toast')['default']
|
||||
|
||||
@ -102,8 +102,18 @@ const menuItems = ref<MenuItem[]>([
|
||||
label: 'Activos Fijos',
|
||||
icon: 'pi pi-building',
|
||||
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 => {
|
||||
if (!item.permission) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return hasPermission(item.permission);
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import Button from 'primevue/button';
|
||||
import Card from 'primevue/card';
|
||||
import InputText from 'primevue/inputtext';
|
||||
@ -8,9 +10,13 @@ import IconField from 'primevue/iconfield';
|
||||
import InputIcon from 'primevue/inputicon';
|
||||
import Select from 'primevue/select';
|
||||
import Paginator from 'primevue/paginator';
|
||||
import Toast from 'primevue/toast';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import fixedAssetsService, { type Asset, type StatusOption } from '../services/fixedAssetsService';
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const searchQuery = ref('');
|
||||
const selectedStatus = ref<number | null>(null);
|
||||
@ -18,9 +24,7 @@ const rowsPerPage = ref(15);
|
||||
const currentPage = ref(1);
|
||||
const totalRecords = ref(0);
|
||||
const assets = ref<Asset[]>([]);
|
||||
const statusOptions = ref<{ label: string; value: number | null }[]>([
|
||||
{ label: 'Todos los Estatus', value: null }
|
||||
]);
|
||||
const statusOptions = ref<{ label: string; value: number | null }[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
@ -34,10 +38,12 @@ const fetchAssets = async () => {
|
||||
page: currentPage.value,
|
||||
});
|
||||
const pageData = response as any;
|
||||
assets.value = pageData.data ?? [];
|
||||
totalRecords.value = pageData.total ?? 0;
|
||||
const pagination = pageData.data?.data;
|
||||
assets.value = pagination?.data ?? [];
|
||||
totalRecords.value = pagination?.total ?? 0;
|
||||
} catch (error) {
|
||||
console.error('Error al cargar activos:', error);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar los activos.', life: 4000 });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -46,10 +52,7 @@ const fetchAssets = async () => {
|
||||
const fetchStatusOptions = async () => {
|
||||
try {
|
||||
const options = await fixedAssetsService.getStatusOptions();
|
||||
statusOptions.value = [
|
||||
{ label: 'Todos los Estatus', value: null },
|
||||
...options.map((opt: StatusOption) => ({ label: opt.name, value: opt.id }))
|
||||
];
|
||||
statusOptions.value = options.map((opt: StatusOption) => ({ label: opt.name, value: opt.id }));
|
||||
} catch (error) {
|
||||
console.error('Error al cargar estatus:', error);
|
||||
}
|
||||
@ -100,19 +103,36 @@ const goToAssignment = () => {
|
||||
router.push('/fixed-assets/assignments');
|
||||
};
|
||||
|
||||
const handleDelete = async (asset: Asset) => {
|
||||
if (!confirm(`¿Estás seguro de eliminar el activo ${asset.sku}?`)) return;
|
||||
const goToEdit = (asset: Asset) => {
|
||||
router.push(`/fixed-assets/${asset.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDelete = (asset: Asset) => {
|
||||
confirm.require({
|
||||
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) {
|
||||
console.error('Error al eliminar activo:', error);
|
||||
} 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>
|
||||
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<Toast position="bottom-right" />
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-wrap justify-between gap-4 items-center">
|
||||
@ -164,6 +184,8 @@ const handleDelete = async (asset: Asset) => {
|
||||
:options="statusOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Todos los Estatus"
|
||||
showClear
|
||||
class="min-w-44"
|
||||
/>
|
||||
</div>
|
||||
@ -233,8 +255,7 @@ const handleDelete = async (asset: Asset) => {
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<Button icon="pi pi-pencil" text rounded size="small" />
|
||||
<Button icon="pi pi-qrcode" text rounded size="small" />
|
||||
<Button icon="pi pi-pencil" text rounded size="small" @click="goToEdit(asset)" />
|
||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger" @click="handleDelete(asset)" />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import Toast from 'primevue/toast';
|
||||
import Button from 'primevue/button';
|
||||
import Card from 'primevue/card';
|
||||
import FixedAssetGeneralInfoSection from './FixedAssetGeneralInfoSection.vue';
|
||||
import FixedAssetAcquisitionSection from './FixedAssetAcquisitionSection.vue';
|
||||
import FixedAssetAssignmentSection from './FixedAssetAssignmentSection.vue';
|
||||
@ -11,8 +12,14 @@ import fixedAssetsService from '../../services/fixedAssetsService';
|
||||
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
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>({
|
||||
inventory_warehouse_id: null,
|
||||
@ -24,18 +31,44 @@ const form = ref<FixedAssetFormData>({
|
||||
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 = () => {
|
||||
router.push('/api/fixed-assets');
|
||||
router.push('/fixed-assets');
|
||||
};
|
||||
|
||||
const saveAsset = async () => {
|
||||
if (!form.value.inventory_warehouse_id || !form.value.estimated_useful_life) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Campos requeridos',
|
||||
detail: 'Selecciona un producto de almacen e indica la vida util estimada.',
|
||||
life: 3000
|
||||
});
|
||||
if (!isEditing.value && !form.value.inventory_warehouse_id) {
|
||||
toast.add({ severity: 'warn', summary: 'Campo requerido', detail: 'Selecciona un producto del almacén.', life: 3000 });
|
||||
return;
|
||||
}
|
||||
if (!form.value.estimated_useful_life) {
|
||||
toast.add({ severity: 'warn', summary: 'Campo requerido', detail: 'Indica la vida útil estimada.', life: 3000 });
|
||||
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_end_date) payload.warranty_end_date = form.value.warranty_end_date;
|
||||
|
||||
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({
|
||||
severity: 'success',
|
||||
summary: 'Activo registrado',
|
||||
detail: 'El activo fijo se registro correctamente.',
|
||||
life: 2600
|
||||
});
|
||||
|
||||
router.push('/api/fixed-assets');
|
||||
setTimeout(() => router.push('/fixed-assets'), 2600);
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || 'Error al registrar el activo.';
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: message,
|
||||
life: 4000
|
||||
});
|
||||
const message = error.response?.data?.message || 'Error al guardar el activo.';
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: message, life: 4000 });
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
@ -82,14 +109,34 @@ const saveAsset = async () => {
|
||||
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<template v-else>
|
||||
<FixedAssetGeneralInfoSection v-if="!isEditing" :form="form" />
|
||||
|
||||
<Card v-else class="shadow-sm">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2 text-xl">
|
||||
<i class="pi pi-info-circle text-primary"></i>
|
||||
<span>Información General</span>
|
||||
</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" />
|
||||
@ -99,11 +146,12 @@ const saveAsset = async () => {
|
||||
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||
<Button label="Cancelar" text severity="secondary" @click="cancel" />
|
||||
<Button
|
||||
label="Guardar Registro"
|
||||
:label="isEditing ? 'Guardar Cambios' : 'Guardar Registro'"
|
||||
icon="pi pi-save"
|
||||
:loading="saving"
|
||||
@click="saveAsset"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@ -7,14 +7,14 @@ import type { AssignmentAssetOption } from '../../types/fixedAssetAssignment';
|
||||
interface Props {
|
||||
assets: AssignmentAssetOption[];
|
||||
searchTerm: string;
|
||||
selectedAssetId: string;
|
||||
selectedAssetId: number | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:searchTerm', value: string): void;
|
||||
(e: 'update:selectedAssetId', value: string): void;
|
||||
(e: 'update:selectedAssetId', value: number): void;
|
||||
}>();
|
||||
|
||||
const selectedAsset = computed(() =>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import Card from 'primevue/card';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Select from 'primevue/select';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import type { FixedAssetAssignmentFormData } from '../../types/fixedAssetAssignment';
|
||||
|
||||
@ -10,12 +9,6 @@ interface Props {
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const conditionOptions = [
|
||||
{ label: 'Excelente', value: 'Excelente' },
|
||||
{ label: 'Bueno', value: 'Bueno' },
|
||||
{ label: 'Regular', value: 'Regular' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -31,21 +24,11 @@ const conditionOptions = [
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Fecha de Entrega</label>
|
||||
<InputText
|
||||
v-model="form.deliveredAt"
|
||||
v-model="form.assignedAt"
|
||||
type="date"
|
||||
class="w-full"
|
||||
/>
|
||||
</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">
|
||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Notas o Comentarios (Opcional)</label>
|
||||
<Textarea
|
||||
|
||||
@ -7,14 +7,14 @@ import type { AssignmentEmployeeOption } from '../../types/fixedAssetAssignment'
|
||||
interface Props {
|
||||
employees: AssignmentEmployeeOption[];
|
||||
searchTerm: string;
|
||||
selectedEmployeeId: string;
|
||||
selectedEmployeeId: number | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:searchTerm', value: string): void;
|
||||
(e: 'update:selectedEmployeeId', value: string): void;
|
||||
(e: 'update:selectedEmployeeId', value: number): void;
|
||||
}>();
|
||||
|
||||
const visibleEmployees = computed(() => {
|
||||
@ -23,7 +23,6 @@ const visibleEmployees = computed(() => {
|
||||
|
||||
return props.employees.filter((employee) =>
|
||||
employee.fullName.toLowerCase().includes(query)
|
||||
|| employee.id.toLowerCase().includes(query)
|
||||
|| employee.department.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import Toast from 'primevue/toast';
|
||||
@ -12,42 +12,63 @@ import type {
|
||||
AssignmentEmployeeOption,
|
||||
FixedAssetAssignmentFormData
|
||||
} from '../../types/fixedAssetAssignment';
|
||||
import { fixedAssetsService } from '../../services/fixedAssetsService';
|
||||
import { employeesService } from '@/modules/rh/components/employees/employees.services';
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const loading = ref(false);
|
||||
const loadingData = ref(false);
|
||||
const assetSearch = ref('');
|
||||
const employeeSearch = ref('');
|
||||
|
||||
const assets = ref<AssignmentAssetOption[]>([
|
||||
{ id: '1', code: 'ACT-0001', name: 'Laptop Dell XPS 15', serial: 'DELL-8829-XP', category: 'Computo' },
|
||||
{ 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 assets = ref<AssignmentAssetOption[]>([]);
|
||||
const employees = ref<AssignmentEmployeeOption[]>([]);
|
||||
|
||||
const form = ref<FixedAssetAssignmentFormData>({
|
||||
assetId: '',
|
||||
employeeId: '',
|
||||
deliveredAt: new Date().toISOString().slice(0, 10),
|
||||
condition: 'Excelente',
|
||||
assetId: null,
|
||||
employeeId: null,
|
||||
assignedAt: new Date().toISOString().slice(0, 10),
|
||||
notes: ''
|
||||
});
|
||||
|
||||
const filteredAssets = computed(() => {
|
||||
const query = assetSearch.value.trim().toLowerCase();
|
||||
if (!query) return assets.value;
|
||||
onMounted(async () => {
|
||||
loadingData.value = true;
|
||||
try {
|
||||
const [assetsRes, employeesRes] = await Promise.all([
|
||||
fixedAssetsService.getAssets({ paginate: false, status: 1 }),
|
||||
employeesService.getEmployees({ paginate: false }),
|
||||
]);
|
||||
|
||||
return assets.value.filter((asset) =>
|
||||
asset.name.toLowerCase().includes(query)
|
||||
|| asset.serial.toLowerCase().includes(query)
|
||||
|| asset.code.toLowerCase().includes(query)
|
||||
);
|
||||
const allAssets = (assetsRes as any).data?.data ?? [];
|
||||
assets.value = allAssets
|
||||
.filter((a: any) => !a.active_assignment)
|
||||
.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');
|
||||
@ -57,24 +78,34 @@ const save = async () => {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
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
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
loading.value = false;
|
||||
try {
|
||||
await fixedAssetsService.assignAsset(form.value.assetId, {
|
||||
employee_id: form.value.employeeId,
|
||||
assigned_at: form.value.assignedAt,
|
||||
notes: form.value.notes || undefined,
|
||||
});
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
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
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
@ -92,9 +123,10 @@ const save = async () => {
|
||||
</div>
|
||||
|
||||
<AssignmentAssetSelectorCard
|
||||
:assets="filteredAssets"
|
||||
:assets="assets"
|
||||
:search-term="assetSearch"
|
||||
:selected-asset-id="form.assetId"
|
||||
:loading="loadingData"
|
||||
@update:search-term="assetSearch = $event"
|
||||
@update:selected-asset-id="form.assetId = $event"
|
||||
/>
|
||||
@ -115,6 +147,7 @@ const save = async () => {
|
||||
label="Confirmar Asignacion"
|
||||
icon="pi pi-check-circle"
|
||||
:loading="loading"
|
||||
:disabled="loadingData"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import Toast from 'primevue/toast';
|
||||
@ -7,40 +7,23 @@ import Button from 'primevue/button';
|
||||
import AssignmentOffboardingSummaryCard from './offboarding/AssignmentOffboardingSummaryCard.vue';
|
||||
import AssignmentOffboardingEventCard from './offboarding/AssignmentOffboardingEventCard.vue';
|
||||
import AssignmentOffboardingEvidenceCard from './offboarding/AssignmentOffboardingEvidenceCard.vue';
|
||||
import { fixedAssetsService } from '../../services/fixedAssetsService';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const assetId = Number(route.params.assetId);
|
||||
const assignmentId = Number(route.params.assignmentId);
|
||||
|
||||
const loading = ref(false);
|
||||
const loadingData = ref(false);
|
||||
|
||||
const defaultAssignment = {
|
||||
id: 'AS-DEFAULT',
|
||||
assetCode: 'WH-NA-000',
|
||||
assetName: 'Activo no encontrado',
|
||||
serial: 'N/A',
|
||||
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 summary = ref({
|
||||
assetCode: '...',
|
||||
assetName: '...',
|
||||
serial: '...',
|
||||
custodian: '...',
|
||||
});
|
||||
|
||||
const form = ref({
|
||||
@ -48,7 +31,37 @@ const form = ref({
|
||||
happenedAt: new Date().toISOString().slice(0, 10),
|
||||
details: '',
|
||||
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');
|
||||
@ -59,23 +72,39 @@ const confirmOffboarding = async () => {
|
||||
severity: 'warn',
|
||||
summary: 'Campos obligatorios',
|
||||
detail: 'Complete motivo, fecha, descripcion y estado final.',
|
||||
life: 3000
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
loading.value = false;
|
||||
try {
|
||||
const notes = `${form.value.reason}: ${form.value.details}`;
|
||||
|
||||
await fixedAssetsService.returnAsset(assetId, assignmentId, {
|
||||
returned_at: form.value.happenedAt,
|
||||
notes,
|
||||
});
|
||||
|
||||
const newStatus = finalStatusToAssetStatus[form.value.finalStatus];
|
||||
if (newStatus && newStatus !== 1) {
|
||||
await fixedAssetsService.updateAsset(assetId, { status: newStatus });
|
||||
}
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Baja confirmada',
|
||||
detail: 'La baja de asignacion se registro correctamente.',
|
||||
life: 2500
|
||||
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>
|
||||
|
||||
@ -93,10 +122,10 @@ const confirmOffboarding = async () => {
|
||||
</div>
|
||||
|
||||
<AssignmentOffboardingSummaryCard
|
||||
:asset-code="currentAssignment.assetCode"
|
||||
:asset-name="currentAssignment.assetName"
|
||||
:serial="currentAssignment.serial"
|
||||
:custodian="currentAssignment.custodian"
|
||||
:asset-code="summary.assetCode"
|
||||
:asset-name="summary.assetName"
|
||||
:serial="summary.serial"
|
||||
:custodian="summary.custodian"
|
||||
/>
|
||||
|
||||
<AssignmentOffboardingEventCard :form="form" />
|
||||
@ -109,6 +138,7 @@ const confirmOffboarding = async () => {
|
||||
icon="pi pi-times-circle"
|
||||
severity="danger"
|
||||
:loading="loading"
|
||||
:disabled="loadingData"
|
||||
@click="confirmOffboarding"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,130 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import Toast from 'primevue/toast';
|
||||
import Button from 'primevue/button';
|
||||
import Card from 'primevue/card';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Select from 'primevue/select';
|
||||
import Paginator from 'primevue/paginator';
|
||||
import Tag from 'primevue/tag';
|
||||
|
||||
interface AssignmentRow {
|
||||
id: string;
|
||||
assetName: string;
|
||||
assetSerial: string;
|
||||
employeeName: string;
|
||||
department: string;
|
||||
deliveryDate: string;
|
||||
expectedReturn: string;
|
||||
condition: 'Excelente' | 'Bueno' | 'Regular';
|
||||
status: 'Activo' | 'Vencido' | 'Devuelto';
|
||||
}
|
||||
import { fixedAssetsService, type AssetAssignment } from '../../services/fixedAssetsService';
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const assignments = ref<AssetAssignment[]>([]);
|
||||
const loading = ref(false);
|
||||
const searchTerm = ref('');
|
||||
const selectedStatus = ref<'all' | AssignmentRow['status']>('all');
|
||||
const first = ref(0);
|
||||
const rows = ref(5);
|
||||
const selectedStatus = ref<number | 'all'>('all');
|
||||
const currentPage = ref(1);
|
||||
const totalRecords = ref(0);
|
||||
const rows = ref(15);
|
||||
|
||||
const statusOptions = [
|
||||
{ label: 'Todos', value: 'all' },
|
||||
{ label: 'Activo', value: 'Activo' },
|
||||
{ label: 'Vencido', value: 'Vencido' },
|
||||
{ label: 'Devuelto', value: 'Devuelto' }
|
||||
{ label: 'Activo', value: 1 },
|
||||
{ label: 'Devuelto', value: 2 },
|
||||
];
|
||||
|
||||
const assignments = ref<AssignmentRow[]>([
|
||||
{
|
||||
id: '#AS-00124',
|
||||
assetName: 'Montacargas Electrico',
|
||||
assetSerial: 'TY-98231',
|
||||
employeeName: 'Roberto Mendez',
|
||||
department: 'Logistica',
|
||||
deliveryDate: '20/Oct/2023',
|
||||
expectedReturn: 'N/A',
|
||||
condition: 'Excelente',
|
||||
status: 'Activo'
|
||||
},
|
||||
{
|
||||
id: '#AS-00125',
|
||||
assetName: 'Laptop Latitude 5420',
|
||||
assetSerial: 'DL-22345',
|
||||
employeeName: 'Ana Salinas',
|
||||
department: 'Administracion',
|
||||
deliveryDate: '15/Nov/2023',
|
||||
expectedReturn: '15/Dic/2023',
|
||||
condition: 'Bueno',
|
||||
status: 'Vencido'
|
||||
},
|
||||
{
|
||||
id: '#AS-00126',
|
||||
assetName: 'Escaner Zebra',
|
||||
assetSerial: 'ZB-77788',
|
||||
employeeName: 'Carlos Ruiz',
|
||||
department: 'Almacen',
|
||||
deliveryDate: '10/Oct/2023',
|
||||
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 statusSeverity = (statusId: number) => {
|
||||
if (statusId === 1) return 'info';
|
||||
return 'secondary';
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return 'N/A';
|
||||
return new Date(dateStr).toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
};
|
||||
|
||||
const employeeFullName = (assignment: AssetAssignment) => {
|
||||
const e = assignment.employee;
|
||||
if (!e) return '—';
|
||||
return `${e.name} ${e.paternal} ${e.maternal ?? ''}`.trim();
|
||||
};
|
||||
|
||||
const loadAssignments = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await fixedAssetsService.getAssignments({
|
||||
q: searchTerm.value || undefined,
|
||||
status: selectedStatus.value !== 'all' ? selectedStatus.value : undefined,
|
||||
page: currentPage.value,
|
||||
});
|
||||
assignments.value = res.data.data.data;
|
||||
totalRecords.value = res.data.data.total;
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar las asignaciones.', life: 4000 });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
]);
|
||||
|
||||
const conditionSeverity = (condition: AssignmentRow['condition']) => {
|
||||
if (condition === 'Excelente') return 'success';
|
||||
if (condition === 'Bueno') return 'warning';
|
||||
return 'secondary';
|
||||
};
|
||||
|
||||
const statusSeverity = (status: AssignmentRow['status']) => {
|
||||
if (status === 'Activo') return 'info';
|
||||
if (status === 'Vencido') return 'danger';
|
||||
return 'secondary';
|
||||
const onSearch = () => {
|
||||
currentPage.value = 1;
|
||||
loadAssignments();
|
||||
};
|
||||
|
||||
const filteredAssignments = computed(() => {
|
||||
const query = searchTerm.value.trim().toLowerCase();
|
||||
return assignments.value.filter((assignment) => {
|
||||
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 onPage = (event: { page: number }) => {
|
||||
currentPage.value = event.page + 1;
|
||||
loadAssignments();
|
||||
};
|
||||
|
||||
const goToCreate = () => {
|
||||
router.push('/fixed-assets/assignments/create');
|
||||
const goToCreate = () => 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) => {
|
||||
const cleanId = assignmentId.replace('#', '');
|
||||
router.push(`/fixed-assets/assignments/${cleanId}/offboarding`);
|
||||
};
|
||||
onMounted(loadAssignments);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<Toast position="bottom-right" />
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<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"
|
||||
class="w-full lg:max-w-sm"
|
||||
placeholder="Buscar por activo o empleado..."
|
||||
@keyup.enter="onSearch"
|
||||
/>
|
||||
<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">
|
||||
Estatus de Asignacion:
|
||||
Estatus:
|
||||
</span>
|
||||
<Select
|
||||
v-model="selectedStatus"
|
||||
@ -156,12 +116,7 @@ const goToOffboarding = (assignmentId: string) => {
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full lg:w-44"
|
||||
/>
|
||||
<Button
|
||||
label="Filtros Avanzados"
|
||||
icon="pi pi-filter"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@change="onSearch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -170,74 +125,86 @@ const goToOffboarding = (assignmentId: string) => {
|
||||
<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 Asignacion</th>
|
||||
<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">Entrega</th>
|
||||
<th class="px-4 py-3">Retorno Previsto</th>
|
||||
<th class="px-4 py-3">Condicion</th>
|
||||
<th class="px-4 py-3">Fecha Entrega</th>
|
||||
<th class="px-4 py-3">Fecha Devolucion</th>
|
||||
<th class="px-4 py-3">Estado</th>
|
||||
<th class="px-4 py-3 text-right">Accion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="loading">
|
||||
<td colspan="7" class="px-4 py-8 text-center text-surface-500">
|
||||
Cargando...
|
||||
</td>
|
||||
</tr>
|
||||
<template v-else>
|
||||
<tr
|
||||
v-for="assignment in paginatedAssignments"
|
||||
v-for="assignment in assignments"
|
||||
: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 }}
|
||||
#{{ assignment.id }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<p class="font-semibold text-surface-900 dark:text-surface-0">{{ assignment.assetName }}</p>
|
||||
<p class="text-xs text-surface-500 dark:text-surface-400">SN: {{ assignment.assetSerial }}</p>
|
||||
<p class="font-semibold text-surface-900 dark:text-surface-0">
|
||||
{{ assignment.asset?.inventory_warehouse?.product?.name ?? assignment.asset?.sku ?? '—' }}
|
||||
</p>
|
||||
<p class="text-xs text-surface-500 dark:text-surface-400">
|
||||
{{ assignment.asset?.sku }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<p class="font-semibold text-surface-900 dark:text-surface-0">{{ assignment.employeeName }}</p>
|
||||
<p class="text-xs text-surface-500 dark:text-surface-400">{{ assignment.department }}</p>
|
||||
<p class="font-semibold text-surface-900 dark:text-surface-0">
|
||||
{{ employeeFullName(assignment) }}
|
||||
</p>
|
||||
<p class="text-xs text-surface-500 dark:text-surface-400">
|
||||
{{ assignment.employee?.department?.name ?? '—' }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ assignment.deliveryDate }}</td>
|
||||
<td class="px-4 py-3">{{ assignment.expectedReturn }}</td>
|
||||
<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.condition" :severity="conditionSeverity(assignment.condition)" />
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<Tag :value="assignment.status" :severity="statusSeverity(assignment.status)" />
|
||||
<Tag
|
||||
:value="assignment.status.name"
|
||||
:severity="statusSeverity(assignment.status.id)"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<Button icon="pi pi-eye" text rounded size="small" />
|
||||
<Button
|
||||
v-if="assignment.status.id === 1"
|
||||
icon="pi pi-times-circle"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="danger"
|
||||
v-tooltip.top="'Dar de baja asignacion'"
|
||||
@click="goToOffboarding(assignment.id)"
|
||||
v-tooltip.top="'Registrar devolucion'"
|
||||
@click="goToOffboarding(assignment)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="paginatedAssignments.length === 0">
|
||||
<td colspan="8" class="px-4 py-8 text-center text-surface-500 dark:text-surface-400">
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
Mostrando {{ paginatedAssignments.length }} de {{ filteredAssignments.length }} asignaciones
|
||||
{{ totalRecords }} asignacion(es) en total
|
||||
</p>
|
||||
<Paginator
|
||||
:first="first"
|
||||
:rows="rows"
|
||||
:totalRecords="filteredAssignments.length"
|
||||
:rowsPerPageOptions="[5, 10, 20]"
|
||||
:totalRecords="totalRecords"
|
||||
template="PrevPageLink PageLinks NextPageLink"
|
||||
@page="onPage"
|
||||
/>
|
||||
|
||||
@ -78,6 +78,55 @@ interface AssetFilters {
|
||||
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 {
|
||||
async getAssets(filters: AssetFilters = {}): Promise<AssetsPaginatedResponse> {
|
||||
const params: Record<string, string | number | boolean> = {};
|
||||
@ -114,6 +163,27 @@ class FixedAssetsService {
|
||||
const response = await api.get<StatusOptionsResponse>('/api/assets/options/status');
|
||||
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();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export interface AssignmentAssetOption {
|
||||
id: string;
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
serial: string;
|
||||
@ -7,7 +7,7 @@ export interface AssignmentAssetOption {
|
||||
}
|
||||
|
||||
export interface AssignmentEmployeeOption {
|
||||
id: string;
|
||||
id: number;
|
||||
initials: string;
|
||||
fullName: string;
|
||||
role: string;
|
||||
@ -15,9 +15,8 @@ export interface AssignmentEmployeeOption {
|
||||
}
|
||||
|
||||
export interface FixedAssetAssignmentFormData {
|
||||
assetId: string;
|
||||
employeeId: string;
|
||||
deliveredAt: string;
|
||||
condition: string;
|
||||
assetId: number | null;
|
||||
employeeId: number | null;
|
||||
assignedAt: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
@ -284,6 +284,15 @@ const routes: RouteRecordRaw[] = [
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id/edit',
|
||||
name: 'FixedAssetEdit',
|
||||
component: FixedAssetForm,
|
||||
meta: {
|
||||
title: 'Editar Activo Fijo',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'assignments',
|
||||
name: 'FixedAssetAssignmentsModule',
|
||||
@ -311,7 +320,7 @@ const routes: RouteRecordRaw[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id/offboarding',
|
||||
path: ':assetId/:assignmentId/offboarding',
|
||||
name: 'FixedAssetAssignmentOffboarding',
|
||||
component: FixedAssetAssignmentOffboardingForm,
|
||||
meta: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user