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']
|
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']
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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(() =>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user