activos-fijos #18
34
colors.css
Normal file
34
colors.css
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
@theme {
|
||||||
|
--color-page: #fff;
|
||||||
|
--color-page-t: #000;
|
||||||
|
--color-page-d: #292524;
|
||||||
|
--color-page-dt: #fff;
|
||||||
|
--color-primary: #374151;
|
||||||
|
--color-primary-t: #fff;
|
||||||
|
--color-primary-d: #1c1917;
|
||||||
|
--color-primary-dt: #fff;
|
||||||
|
--color-secondary: #3b82f6;
|
||||||
|
--color-secondary-t: #fff;
|
||||||
|
--color-secondary-d: #312e81;
|
||||||
|
--color-secondary-dt: #fff;
|
||||||
|
--color-primary-info: #06b6d4;
|
||||||
|
--color-primary-info-t: #fff;
|
||||||
|
--color-primary-info-d: #06b6d4;
|
||||||
|
--color-primary-info-dt: #fff;
|
||||||
|
--color-secondary-info: #06b6d4;
|
||||||
|
--color-secondary-info-t: #fff;
|
||||||
|
--color-secondary-info-d: #06b6d4;
|
||||||
|
--color-secondary-info-dt: #fff;
|
||||||
|
--color-success: #22c55e;
|
||||||
|
--color-success-t: #fff;
|
||||||
|
--color-success-d: #22c55e;
|
||||||
|
--color-success-dt: #fff;
|
||||||
|
--color-danger: #ef4444;
|
||||||
|
--color-danger-t: #fff;
|
||||||
|
--color-danger-d: #ef4444;
|
||||||
|
--color-danger-dt: #fecaca;
|
||||||
|
--color-warning: #eab308;
|
||||||
|
--color-warning-t: #fff;
|
||||||
|
--color-warning-d: #eab308;
|
||||||
|
--color-warning-dt: #fff;
|
||||||
|
}
|
||||||
@ -71,6 +71,19 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
icon: 'pi pi-cog',
|
icon: 'pi pi-cog',
|
||||||
to: '/stores'
|
to: '/stores'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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: 'Estructura de Activos', icon: 'pi pi-sitemap', to: '/fixed-assets/structures' },
|
||||||
|
// { label: 'Marcas', icon: 'pi pi-building', to: '/fixed-assets/brands' },
|
||||||
|
// { label: 'Modelos', icon: 'pi pi-building', to: '/fixed-assets/models' },
|
||||||
|
// { label: 'Estados', icon: 'pi pi-building', to: '/fixed-assets/states' },
|
||||||
|
// { label: 'Ubicaciones', icon: 'pi pi-building', to: '/fixed-assets/locations' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Configuración',
|
label: 'Configuración',
|
||||||
icon: 'pi pi-cog',
|
icon: 'pi pi-cog',
|
||||||
|
|||||||
322
src/modules/fixed-assets/components/FixedAssetsIndex.vue
Normal file
322
src/modules/fixed-assets/components/FixedAssetsIndex.vue
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import IconField from 'primevue/iconfield';
|
||||||
|
import InputIcon from 'primevue/inputicon';
|
||||||
|
import Select from 'primevue/select';
|
||||||
|
import Paginator from 'primevue/paginator';
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
|
interface FixedAsset {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
serial: string;
|
||||||
|
category: string;
|
||||||
|
location: string;
|
||||||
|
status: 'ASIGNADO' | 'MANTENIMIENTO' | 'DISPONIBLE';
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchQuery = ref('');
|
||||||
|
const selectedCategory = ref('all');
|
||||||
|
const selectedStatus = ref('all');
|
||||||
|
const rowsPerPage = ref(4);
|
||||||
|
const first = ref(0);
|
||||||
|
|
||||||
|
const categoryOptions = [
|
||||||
|
{ label: 'Todas las Categorías', value: 'all' },
|
||||||
|
{ label: 'Cómputo', value: 'Cómputo' },
|
||||||
|
{ label: 'Maquinaria', value: 'Maquinaria' },
|
||||||
|
{ label: 'Mobiliario', value: 'Mobiliario' },
|
||||||
|
{ label: 'TI Infra', value: 'TI Infra' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: 'Todos los Estatus', value: 'all' },
|
||||||
|
{ label: 'Asignado', value: 'ASIGNADO' },
|
||||||
|
{ label: 'Mantenimiento', value: 'MANTENIMIENTO' },
|
||||||
|
{ label: 'Disponible', value: 'DISPONIBLE' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const fixedAssets = ref<FixedAsset[]>([
|
||||||
|
{
|
||||||
|
id: 'ACT-0001',
|
||||||
|
name: 'MacBook Pro 14"',
|
||||||
|
serial: 'SN-928374',
|
||||||
|
category: 'Cómputo',
|
||||||
|
location: 'Oficina Central - Piso 2',
|
||||||
|
status: 'ASIGNADO',
|
||||||
|
value: 45000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-0042',
|
||||||
|
name: 'Montacargas Hidráulico',
|
||||||
|
serial: 'MH-1029',
|
||||||
|
category: 'Maquinaria',
|
||||||
|
location: 'Almacén General - Pasillo A',
|
||||||
|
status: 'MANTENIMIENTO',
|
||||||
|
value: 185200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-0056',
|
||||||
|
name: 'Escritorio Ergonómico',
|
||||||
|
serial: 'N/A',
|
||||||
|
category: 'Mobiliario',
|
||||||
|
location: 'Sucursal Norte',
|
||||||
|
status: 'DISPONIBLE',
|
||||||
|
value: 8400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-0099',
|
||||||
|
name: 'Servidor Dell PowerEdge',
|
||||||
|
serial: 'SV-3301',
|
||||||
|
category: 'TI Infra',
|
||||||
|
location: 'Data Center',
|
||||||
|
status: 'ASIGNADO',
|
||||||
|
value: 120000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-0103',
|
||||||
|
name: 'Impresora Industrial ZT610',
|
||||||
|
serial: 'ZT-7745',
|
||||||
|
category: 'Maquinaria',
|
||||||
|
location: 'Planta Producción',
|
||||||
|
status: 'DISPONIBLE',
|
||||||
|
value: 76250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-0114',
|
||||||
|
name: 'Cámara de Seguridad PTZ',
|
||||||
|
serial: 'CAM-445',
|
||||||
|
category: 'TI Infra',
|
||||||
|
location: 'Oficina Central - Lobby',
|
||||||
|
status: 'ASIGNADO',
|
||||||
|
value: 12890
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filteredAssets = computed(() => {
|
||||||
|
return fixedAssets.value.filter((asset) => {
|
||||||
|
const normalizedSearch = searchQuery.value.trim().toLowerCase();
|
||||||
|
const matchesSearch = !normalizedSearch
|
||||||
|
|| asset.id.toLowerCase().includes(normalizedSearch)
|
||||||
|
|| asset.name.toLowerCase().includes(normalizedSearch)
|
||||||
|
|| asset.serial.toLowerCase().includes(normalizedSearch);
|
||||||
|
const matchesCategory = selectedCategory.value === 'all' || asset.category === selectedCategory.value;
|
||||||
|
const matchesStatus = selectedStatus.value === 'all' || asset.status === selectedStatus.value;
|
||||||
|
|
||||||
|
return matchesSearch && matchesCategory && matchesStatus;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginatedAssets = computed(() => {
|
||||||
|
return filteredAssets.value.slice(first.value, first.value + rowsPerPage.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalRecords = computed(() => filteredAssets.value.length);
|
||||||
|
|
||||||
|
const onPageChange = (event: { first: number; rows: number }) => {
|
||||||
|
first.value = event.first;
|
||||||
|
rowsPerPage.value = event.rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return `$${value.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusClasses: Record<FixedAsset['status'], string> = {
|
||||||
|
ASIGNADO: 'bg-blue-100 text-blue-700',
|
||||||
|
MANTENIMIENTO: 'bg-amber-100 text-amber-700',
|
||||||
|
DISPONIBLE: 'bg-emerald-100 text-emerald-700'
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToStructures = () => {
|
||||||
|
router.push('/fixed-assets/structures');
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToCreateAsset = () => {
|
||||||
|
router.push('/fixed-assets/create');
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToAssignment = () => {
|
||||||
|
router.push('/fixed-assets/assignments');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="space-y-6">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex flex-wrap justify-between gap-4 items-center">
|
||||||
|
<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">
|
||||||
|
Activos Fijos
|
||||||
|
</h1>
|
||||||
|
<p class="text-surface-500 dark:text-surface-400 text-base font-normal leading-normal">
|
||||||
|
Control detallado y trazabilidad de bienes corporativos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<Button
|
||||||
|
label="Exportar"
|
||||||
|
icon="pi pi-file-export"
|
||||||
|
outlined
|
||||||
|
severity="secondary"
|
||||||
|
class="min-w-[200px]"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Registrar Activo"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
class="min-w-[200px]"
|
||||||
|
@click="goToCreateAsset"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Estructuras"
|
||||||
|
icon="pi pi-sitemap"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
class="min-w-40"
|
||||||
|
@click="goToStructures"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Asignar Activo"
|
||||||
|
icon="pi pi-send"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
class="min-w-44"
|
||||||
|
@click="goToAssignment"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Card -->
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||||
|
<div class="flex w-full items-center gap-3 xl:max-w-xl">
|
||||||
|
<IconField iconPosition="left" class="w-full">
|
||||||
|
<InputIcon class="pi pi-search" />
|
||||||
|
<InputText
|
||||||
|
v-model="searchQuery"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Buscar por código, serie o descripción..."
|
||||||
|
/>
|
||||||
|
</IconField>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-sliders-h"
|
||||||
|
outlined
|
||||||
|
severity="secondary"
|
||||||
|
aria-label="Filtros avanzados"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Select
|
||||||
|
v-model="selectedCategory"
|
||||||
|
:options="categoryOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
|
||||||
|
class="min-w-48"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
v-model="selectedStatus"
|
||||||
|
:options="statusOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="min-w-44"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl border border-surface-200 dark:border-surface-700">
|
||||||
|
<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-4">ID</th>
|
||||||
|
<th class="px-4 py-4">Descripción / Activo</th>
|
||||||
|
<th class="px-4 py-4">Categoría</th>
|
||||||
|
<th class="px-4 py-4">Ubicación</th>
|
||||||
|
<th class="px-4 py-4">Estatus</th>
|
||||||
|
<th class="px-4 py-4">Valor MXN</th>
|
||||||
|
<th class="px-4 py-4 text-right">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="asset in paginatedAssets"
|
||||||
|
:key="asset.id"
|
||||||
|
class="border-t border-surface-200 text-sm text-surface-700 dark:border-surface-700 dark:text-surface-200"
|
||||||
|
>
|
||||||
|
<td class="px-4 py-4 font-medium text-surface-500 dark:text-surface-400">
|
||||||
|
{{ asset.id }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-surface-100 dark:bg-surface-800">
|
||||||
|
<i class="pi pi-image text-surface-400"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-surface-900 dark:text-surface-0">
|
||||||
|
{{ asset.name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-surface-500 dark:text-surface-400">
|
||||||
|
Serie: {{ asset.serial }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4">{{ asset.category }}</td>
|
||||||
|
<td class="px-4 py-4">{{ asset.location }}</td>
|
||||||
|
<td class="px-4 py-4">
|
||||||
|
<span
|
||||||
|
class="inline-flex rounded-full px-3 py-1 text-xs font-semibold"
|
||||||
|
:class="statusClasses[asset.status]"
|
||||||
|
>
|
||||||
|
{{ asset.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4 text-lg font-semibold text-surface-900 dark:text-surface-0">
|
||||||
|
{{ formatCurrency(asset.value) }}
|
||||||
|
</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-trash" text rounded size="small" severity="danger" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="paginatedAssets.length === 0">
|
||||||
|
<td colspan="7" class="px-4 py-10 text-center text-surface-500 dark:text-surface-400">
|
||||||
|
No se encontraron activos con los filtros seleccionados.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 pt-1 md:flex-row md:items-center md:justify-between">
|
||||||
|
<p class="text-sm text-surface-500 dark:text-surface-400">
|
||||||
|
Mostrando {{ paginatedAssets.length }} de {{ totalRecords }} activos
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Paginator
|
||||||
|
:first="first"
|
||||||
|
:rows="rowsPerPage"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:rowsPerPageOptions="[4, 8, 12]"
|
||||||
|
template="PrevPageLink PageLinks NextPageLink"
|
||||||
|
@page="onPageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import InputNumber from 'primevue/inputnumber';
|
||||||
|
import Select from 'primevue/select';
|
||||||
|
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
form: FixedAssetFormData;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const locationOptions = [
|
||||||
|
{ label: 'Seleccione ubicacion de almacen', value: '' },
|
||||||
|
{ label: 'Almacen General', value: 'Almacen General' },
|
||||||
|
{ label: 'Sucursal Norte', value: 'Sucursal Norte' },
|
||||||
|
{ label: 'Oficina Central - Piso 2', value: 'Oficina Central - Piso 2' },
|
||||||
|
{ label: 'Data Center', value: 'Data Center' }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-wallet text-primary"></i>
|
||||||
|
<span>Adquisicion y Ubicacion</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||||
|
Fecha de Compra
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="form.purchaseDate"
|
||||||
|
type="date"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||||
|
Precio de Compra (MXN)
|
||||||
|
</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model="form.purchasePrice"
|
||||||
|
mode="currency"
|
||||||
|
currency="MXN"
|
||||||
|
locale="es-MX"
|
||||||
|
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">
|
||||||
|
Ubicacion Inicial
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
v-model="form.initialLocation"
|
||||||
|
:options="locationOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Select from 'primevue/select';
|
||||||
|
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
form: FixedAssetFormData;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: 'Disponible', value: 'Disponible' },
|
||||||
|
{ label: 'Asignado', value: 'Asignado' },
|
||||||
|
{ label: 'Mantenimiento', value: 'Mantenimiento' }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-user-plus text-primary"></i>
|
||||||
|
<span>Asignacion Inicial</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||||
|
Estatus Inicial
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
v-model="form.initialStatus"
|
||||||
|
:options="statusOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||||
|
Empleado Responsable
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="form.responsibleEmployee"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Buscar empleado por nombre o ID"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import Toast from 'primevue/toast';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import FixedAssetGeneralInfoSection from './FixedAssetGeneralInfoSection.vue';
|
||||||
|
import FixedAssetAcquisitionSection from './FixedAssetAcquisitionSection.vue';
|
||||||
|
import FixedAssetAssignmentSection from './FixedAssetAssignmentSection.vue';
|
||||||
|
import FixedAssetImageCard from './FixedAssetImageCard.vue';
|
||||||
|
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const toast = useToast();
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
const form = ref<FixedAssetFormData>({
|
||||||
|
name: '',
|
||||||
|
serial: '',
|
||||||
|
category: '',
|
||||||
|
brand: '',
|
||||||
|
model: '',
|
||||||
|
purchaseDate: '',
|
||||||
|
purchasePrice: null,
|
||||||
|
initialLocation: '',
|
||||||
|
initialStatus: 'Disponible',
|
||||||
|
responsibleEmployee: '',
|
||||||
|
imageFileName: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
router.push('/fixed-assets');
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAsset = async () => {
|
||||||
|
if (!form.value.name || !form.value.serial) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Campos requeridos',
|
||||||
|
detail: 'Completa nombre y numero de serie del activo.',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
saving.value = false;
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Activo registrado',
|
||||||
|
detail: `El activo "${form.value.name}" se registro correctamente.`,
|
||||||
|
life: 2600
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push('/fixed-assets');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="space-y-6">
|
||||||
|
<Toast position="bottom-right" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0">
|
||||||
|
Registrar Nuevo Activo Fijo
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-surface-500 dark:text-surface-400">
|
||||||
|
Complete los detalles para dar de alta un nuevo activo en el inventario global del almacen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FixedAssetGeneralInfoSection :form="form" />
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-5 xl:grid-cols-12">
|
||||||
|
<div class="space-y-5 xl:col-span-8">
|
||||||
|
<FixedAssetAcquisitionSection :form="form" />
|
||||||
|
<FixedAssetAssignmentSection :form="form" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="xl:col-span-4">
|
||||||
|
<FixedAssetImageCard :form="form" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||||
|
<Button label="Cancelar" text severity="secondary" @click="cancel" />
|
||||||
|
<Button
|
||||||
|
label="Guardar Registro"
|
||||||
|
icon="pi pi-save"
|
||||||
|
:loading="saving"
|
||||||
|
@click="saveAsset"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Select from 'primevue/select';
|
||||||
|
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
form: FixedAssetFormData;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const categoryOptions = [
|
||||||
|
{ label: 'Seleccione una categoria', value: '' },
|
||||||
|
{ label: 'Computo', value: 'Computo' },
|
||||||
|
{ label: 'Maquinaria', value: 'Maquinaria' },
|
||||||
|
{ label: 'Mobiliario', value: 'Mobiliario' },
|
||||||
|
{ label: 'Infraestructura TI', value: 'Infraestructura TI' }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-info-circle text-primary"></i>
|
||||||
|
<span>Informacion General</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div class="space-y-2 md:col-span-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||||
|
Nombre del Activo *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="form.name"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Ej: Montacargas Electrico Toyota"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||||
|
Numero de Serie / Serial *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="form.serial"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="SN-123456789"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||||
|
Categoria
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
v-model="form.category"
|
||||||
|
:options="categoryOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||||
|
Marca
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="form.brand"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Ej: Toyota, Dell, Bosch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||||
|
Modelo
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="form.model"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Ej: Series-X 2023"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import FileUpload from 'primevue/fileupload';
|
||||||
|
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
form: FixedAssetFormData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const onSelectFile = (event: { files: File[] }) => {
|
||||||
|
const file = event.files?.[0];
|
||||||
|
props.form.imageFileName = file ? file.name : '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm h-full">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-image text-primary"></i>
|
||||||
|
<span>Imagen del Activo</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<FileUpload
|
||||||
|
mode="basic"
|
||||||
|
name="fixed-asset-image"
|
||||||
|
accept="image/png,image/jpeg"
|
||||||
|
:maxFileSize="5000000"
|
||||||
|
chooseLabel="Subir Imagen"
|
||||||
|
class="w-full"
|
||||||
|
@select="onSelectFile"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="upload-dropzone">
|
||||||
|
<i class="pi pi-upload text-3xl text-surface-400"></i>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-surface-700 dark:text-surface-200">Subir Imagen</p>
|
||||||
|
<p class="mt-1 text-xs text-surface-500 dark:text-surface-400">PNG, JPG hasta 5MB</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-surface-100 px-3 py-2 text-xs text-surface-500 dark:bg-surface-800 dark:text-surface-400">
|
||||||
|
{{ form.imageFileName || 'No seleccionado' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.upload-dropzone {
|
||||||
|
border: 1px dashed var(--p-surface-300);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
min-height: 160px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Select from 'primevue/select';
|
||||||
|
import type { AssignmentAssetOption } from '../../types/fixedAssetAssignment';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
assets: AssignmentAssetOption[];
|
||||||
|
searchTerm: string;
|
||||||
|
selectedAssetId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:searchTerm', value: string): void;
|
||||||
|
(e: 'update:selectedAssetId', value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedAsset = computed(() =>
|
||||||
|
props.assets.find((asset) => asset.id === props.selectedAssetId) ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectOptions = computed(() =>
|
||||||
|
props.assets.map((asset) => ({
|
||||||
|
label: `${asset.name} - ${asset.serial}`,
|
||||||
|
value: asset.id
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-folder-open text-primary"></i>
|
||||||
|
<span>Seleccionar Activo</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Select
|
||||||
|
:model-value="selectedAssetId"
|
||||||
|
:options="selectOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Busque o seleccione un activo por nombre o serie..."
|
||||||
|
filter
|
||||||
|
@update:model-value="emit('update:selectedAssetId', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-blue-200 bg-blue-50 p-4 dark:border-blue-900/40 dark:bg-blue-900/15">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="mt-1 flex h-10 w-10 items-center justify-center rounded-lg bg-white dark:bg-surface-900">
|
||||||
|
<i class="pi pi-image text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedAsset">
|
||||||
|
<p class="font-semibold text-surface-900 dark:text-surface-0">
|
||||||
|
{{ selectedAsset.name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-surface-600 dark:text-surface-300">
|
||||||
|
{{ selectedAsset.code }} - {{ selectedAsset.serial }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-surface-500 dark:text-surface-400">
|
||||||
|
{{ selectedAsset.category }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p class="font-semibold text-surface-900 dark:text-surface-0">Activo seleccionado</p>
|
||||||
|
<p class="text-sm text-surface-600 dark:text-surface-300">
|
||||||
|
Seleccione un activo arriba para ver los detalles aqui.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
<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';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
form: FixedAssetAssignmentFormData;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const conditionOptions = [
|
||||||
|
{ label: 'Excelente', value: 'Excelente' },
|
||||||
|
{ label: 'Bueno', value: 'Bueno' },
|
||||||
|
{ label: 'Regular', value: 'Regular' }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-clipboard text-primary"></i>
|
||||||
|
<span>Detalles de la Asignacion</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<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"
|
||||||
|
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
|
||||||
|
v-model="form.notes"
|
||||||
|
rows="4"
|
||||||
|
class="w-full"
|
||||||
|
autoResize
|
||||||
|
placeholder="Anade detalles relevantes sobre el estado de entrega o terminos especificos..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import type { AssignmentEmployeeOption } from '../../types/fixedAssetAssignment';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
employees: AssignmentEmployeeOption[];
|
||||||
|
searchTerm: string;
|
||||||
|
selectedEmployeeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:searchTerm', value: string): void;
|
||||||
|
(e: 'update:selectedEmployeeId', value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const visibleEmployees = computed(() => {
|
||||||
|
const query = props.searchTerm.trim().toLowerCase();
|
||||||
|
if (!query) return props.employees;
|
||||||
|
|
||||||
|
return props.employees.filter((employee) =>
|
||||||
|
employee.fullName.toLowerCase().includes(query)
|
||||||
|
|| employee.id.toLowerCase().includes(query)
|
||||||
|
|| employee.department.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-users text-primary"></i>
|
||||||
|
<span>Seleccionar Colaborador</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<InputText
|
||||||
|
:model-value="searchTerm"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Buscar por nombre, ID o departamento..."
|
||||||
|
@update:model-value="emit('update:searchTerm', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
|
<button
|
||||||
|
v-for="employee in visibleEmployees"
|
||||||
|
:key="employee.id"
|
||||||
|
type="button"
|
||||||
|
class="rounded-xl border p-4 text-left transition-all"
|
||||||
|
:class="selectedEmployeeId === employee.id
|
||||||
|
? 'border-primary bg-primary-50 dark:bg-primary-950/20'
|
||||||
|
: 'border-surface-200 hover:border-primary/50 dark:border-surface-700'"
|
||||||
|
@click="emit('update:selectedEmployeeId', employee.id)"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-sm font-semibold text-white">
|
||||||
|
{{ employee.initials }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-surface-900 dark:text-surface-0">{{ employee.fullName }}</p>
|
||||||
|
<p class="text-sm text-surface-600 dark:text-surface-300">{{ employee.role }}</p>
|
||||||
|
<p class="text-xs text-surface-500 dark:text-surface-400">{{ employee.department }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
class="pi pi-check-circle text-primary"
|
||||||
|
:class="selectedEmployeeId === employee.id ? 'opacity-100' : 'opacity-0'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import Toast from 'primevue/toast';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import AssignmentAssetSelectorCard from './AssignmentAssetSelectorCard.vue';
|
||||||
|
import AssignmentEmployeeSelectorCard from './AssignmentEmployeeSelectorCard.vue';
|
||||||
|
import AssignmentDetailsCard from './AssignmentDetailsCard.vue';
|
||||||
|
import type {
|
||||||
|
AssignmentAssetOption,
|
||||||
|
AssignmentEmployeeOption,
|
||||||
|
FixedAssetAssignmentFormData
|
||||||
|
} from '../../types/fixedAssetAssignment';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const toast = useToast();
|
||||||
|
const loading = 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 form = ref<FixedAssetAssignmentFormData>({
|
||||||
|
assetId: '',
|
||||||
|
employeeId: '',
|
||||||
|
deliveredAt: new Date().toISOString().slice(0, 10),
|
||||||
|
condition: 'Excelente',
|
||||||
|
notes: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredAssets = computed(() => {
|
||||||
|
const query = assetSearch.value.trim().toLowerCase();
|
||||||
|
if (!query) return assets.value;
|
||||||
|
|
||||||
|
return assets.value.filter((asset) =>
|
||||||
|
asset.name.toLowerCase().includes(query)
|
||||||
|
|| asset.serial.toLowerCase().includes(query)
|
||||||
|
|| asset.code.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancel = () => router.push('/fixed-assets/assignments');
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
if (!form.value.assetId || !form.value.employeeId) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Campos pendientes',
|
||||||
|
detail: 'Seleccione activo y colaborador para confirmar la asignacion.',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Asignacion registrada',
|
||||||
|
detail: 'La asignacion de activo al colaborador se registro correctamente.',
|
||||||
|
life: 2500
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push('/fixed-assets/assignments');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="space-y-6">
|
||||||
|
<Toast position="bottom-right" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0">
|
||||||
|
Asignacion de Activo a Empleado
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-surface-500 dark:text-surface-400">
|
||||||
|
Registre la entrega de una herramienta o equipo a un colaborador del almacen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AssignmentAssetSelectorCard
|
||||||
|
:assets="filteredAssets"
|
||||||
|
:search-term="assetSearch"
|
||||||
|
:selected-asset-id="form.assetId"
|
||||||
|
@update:search-term="assetSearch = $event"
|
||||||
|
@update:selected-asset-id="form.assetId = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AssignmentEmployeeSelectorCard
|
||||||
|
:employees="employees"
|
||||||
|
:search-term="employeeSearch"
|
||||||
|
:selected-employee-id="form.employeeId"
|
||||||
|
@update:search-term="employeeSearch = $event"
|
||||||
|
@update:selected-employee-id="form.employeeId = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AssignmentDetailsCard :form="form" />
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||||
|
<Button label="Cancelar" text severity="secondary" @click="cancel" />
|
||||||
|
<Button
|
||||||
|
label="Confirmar Asignacion"
|
||||||
|
icon="pi pi-check-circle"
|
||||||
|
:loading="loading"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import Toast from 'primevue/toast';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import AssignmentOffboardingSummaryCard from './offboarding/AssignmentOffboardingSummaryCard.vue';
|
||||||
|
import AssignmentOffboardingEventCard from './offboarding/AssignmentOffboardingEventCard.vue';
|
||||||
|
import AssignmentOffboardingEvidenceCard from './offboarding/AssignmentOffboardingEvidenceCard.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const toast = useToast();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
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];
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
reason: '',
|
||||||
|
happenedAt: new Date().toISOString().slice(0, 10),
|
||||||
|
details: '',
|
||||||
|
finalStatus: '',
|
||||||
|
evidenceFileName: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancel = () => router.push('/fixed-assets/assignments');
|
||||||
|
|
||||||
|
const confirmOffboarding = async () => {
|
||||||
|
if (!form.value.reason || !form.value.happenedAt || !form.value.details || !form.value.finalStatus) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Campos obligatorios',
|
||||||
|
detail: 'Complete motivo, fecha, descripcion y estado final.',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Baja confirmada',
|
||||||
|
detail: 'La baja de asignacion se registro correctamente.',
|
||||||
|
life: 2500
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push('/fixed-assets/assignments');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="space-y-6">
|
||||||
|
<Toast position="bottom-right" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0">
|
||||||
|
Baja de Asignacion de Activo
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-surface-500 dark:text-surface-400">
|
||||||
|
Complete el formulario para desvincular el activo del custodio actual y registrar el motivo de la baja.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AssignmentOffboardingSummaryCard
|
||||||
|
:asset-code="currentAssignment.assetCode"
|
||||||
|
:asset-name="currentAssignment.assetName"
|
||||||
|
:serial="currentAssignment.serial"
|
||||||
|
:custodian="currentAssignment.custodian"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AssignmentOffboardingEventCard :form="form" />
|
||||||
|
<AssignmentOffboardingEvidenceCard :form="form" />
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||||
|
<Button label="Cancelar" text severity="secondary" @click="cancel" />
|
||||||
|
<Button
|
||||||
|
label="Confirmar Baja"
|
||||||
|
icon="pi pi-times-circle"
|
||||||
|
severity="danger"
|
||||||
|
:loading="loading"
|
||||||
|
@click="confirmOffboarding"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,249 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const searchTerm = ref('');
|
||||||
|
const selectedStatus = ref<'all' | AssignmentRow['status']>('all');
|
||||||
|
const first = ref(0);
|
||||||
|
const rows = ref(5);
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: 'Todos', value: 'all' },
|
||||||
|
{ label: 'Activo', value: 'Activo' },
|
||||||
|
{ label: 'Vencido', value: 'Vencido' },
|
||||||
|
{ label: 'Devuelto', value: 'Devuelto' }
|
||||||
|
];
|
||||||
|
|
||||||
|
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 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 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 goToCreate = () => {
|
||||||
|
router.push('/fixed-assets/assignments/create');
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToOffboarding = (assignmentId: string) => {
|
||||||
|
const cleanId = assignmentId.replace('#', '');
|
||||||
|
router.push(`/fixed-assets/assignments/${cleanId}/offboarding`);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="space-y-6">
|
||||||
|
<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">
|
||||||
|
Listado de Asignaciones
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-surface-500 dark:text-surface-400">
|
||||||
|
Gestione y rastree las entregas de equipos al personal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button label="Nueva Asignacion" icon="pi pi-plus" @click="goToCreate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
<InputText
|
||||||
|
v-model="searchTerm"
|
||||||
|
class="w-full lg:max-w-sm"
|
||||||
|
placeholder="Buscar por activo o empleado..."
|
||||||
|
/>
|
||||||
|
<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:
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
v-model="selectedStatus"
|
||||||
|
:options="statusOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full lg:w-44"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Filtros Avanzados"
|
||||||
|
icon="pi pi-filter"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl border border-surface-200 dark:border-surface-700">
|
||||||
|
<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">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">Estado</th>
|
||||||
|
<th class="px-4 py-3 text-right">Accion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="assignment in paginatedAssignments"
|
||||||
|
:key="assignment.id"
|
||||||
|
class="border-t border-surface-200 text-sm dark:border-surface-700"
|
||||||
|
>
|
||||||
|
<td class="px-4 py-3 font-medium text-surface-800 dark:text-surface-100">
|
||||||
|
{{ assignment.id }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<p class="font-semibold text-surface-900 dark:text-surface-0">{{ assignment.assetName }}</p>
|
||||||
|
<p class="text-xs text-surface-500 dark:text-surface-400">SN: {{ assignment.assetSerial }}</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>
|
||||||
|
</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">
|
||||||
|
<Tag :value="assignment.condition" :severity="conditionSeverity(assignment.condition)" />
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<Tag :value="assignment.status" :severity="statusSeverity(assignment.status)" />
|
||||||
|
</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
|
||||||
|
icon="pi pi-times-circle"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
size="small"
|
||||||
|
severity="danger"
|
||||||
|
v-tooltip.top="'Dar de baja asignacion'"
|
||||||
|
@click="goToOffboarding(assignment.id)"
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
|
No hay asignaciones para mostrar.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</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
|
||||||
|
</p>
|
||||||
|
<Paginator
|
||||||
|
:first="first"
|
||||||
|
:rows="rows"
|
||||||
|
:totalRecords="filteredAssignments.length"
|
||||||
|
:rowsPerPageOptions="[5, 10, 20]"
|
||||||
|
template="PrevPageLink PageLinks NextPageLink"
|
||||||
|
@page="onPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import Select from 'primevue/select';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Textarea from 'primevue/textarea';
|
||||||
|
|
||||||
|
interface OffboardingEventForm {
|
||||||
|
reason: string;
|
||||||
|
happenedAt: string;
|
||||||
|
details: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
form: OffboardingEventForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const reasonOptions = [
|
||||||
|
{ label: 'Seleccione un motivo', value: '' },
|
||||||
|
{ label: 'Cambio de colaborador', value: 'Cambio de colaborador' },
|
||||||
|
{ label: 'Baja por danio', value: 'Baja por danio' },
|
||||||
|
{ label: 'Robo o extravio', value: 'Robo o extravio' },
|
||||||
|
{ label: 'Fin de proyecto', value: 'Fin de proyecto' }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-file-edit text-red-500"></i>
|
||||||
|
<span>2. Detalles del Evento</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Motivo de la Baja *</label>
|
||||||
|
<Select
|
||||||
|
v-model="form.reason"
|
||||||
|
:options="reasonOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Fecha del Suceso *</label>
|
||||||
|
<InputText v-model="form.happenedAt" type="date" 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">Descripcion Detallada *</label>
|
||||||
|
<Textarea
|
||||||
|
v-model="form.details"
|
||||||
|
rows="4"
|
||||||
|
class="w-full"
|
||||||
|
autoResize
|
||||||
|
placeholder="Proporcione detalles especificos sobre el fallo o la razon de la desvinculacion..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import FileUpload from 'primevue/fileupload';
|
||||||
|
import Select from 'primevue/select';
|
||||||
|
|
||||||
|
interface OffboardingEvidenceForm {
|
||||||
|
finalStatus: string;
|
||||||
|
evidenceFileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
form: OffboardingEvidenceForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const finalStatusOptions = [
|
||||||
|
{ label: 'Seleccione el destino del activo', value: '' },
|
||||||
|
{ label: 'Devuelto a almacen', value: 'Devuelto a almacen' },
|
||||||
|
{ label: 'En reparacion', value: 'En reparacion' },
|
||||||
|
{ label: 'Baja definitiva', value: 'Baja definitiva' },
|
||||||
|
{ label: 'Reasignacion pendiente', value: 'Reasignacion pendiente' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const onSelectEvidence = (event: { files: File[] }) => {
|
||||||
|
const file = event.files?.[0];
|
||||||
|
props.form.evidenceFileName = file ? file.name : '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-file-plus text-red-500"></i>
|
||||||
|
<span>3. Evidencia y Estado Final</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||||
|
Archivos de Evidencia (Fotos, Reportes, PDFs)
|
||||||
|
</p>
|
||||||
|
<div class="mt-2 rounded-xl border border-dashed border-surface-300 p-6 text-center dark:border-surface-700">
|
||||||
|
<FileUpload
|
||||||
|
mode="basic"
|
||||||
|
name="offboarding-evidence"
|
||||||
|
accept="image/png,image/jpeg,application/pdf"
|
||||||
|
:maxFileSize="10000000"
|
||||||
|
chooseLabel="Subir evidencia"
|
||||||
|
class="w-full"
|
||||||
|
@select="onSelectEvidence"
|
||||||
|
/>
|
||||||
|
<p class="mt-3 text-xs text-surface-500 dark:text-surface-400">
|
||||||
|
Soportado: JPG, PNG, PDF (Max. 10MB)
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-xs font-medium text-surface-600 dark:text-surface-300">
|
||||||
|
{{ form.evidenceFileName || 'Sin archivo seleccionado' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">Estado Final del Activo *</label>
|
||||||
|
<Select
|
||||||
|
v-model="form.finalStatus"
|
||||||
|
:options="finalStatusOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full md:w-80"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
assetCode: string;
|
||||||
|
assetName: string;
|
||||||
|
serial: string;
|
||||||
|
custodian: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-exclamation-circle text-red-500"></i>
|
||||||
|
<span>1. Resumen de Asignacion Actual</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-surface-500">ID de Activo</p>
|
||||||
|
<p class="mt-1 font-medium text-surface-900 dark:text-surface-0">{{ assetCode }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-surface-500">Descripcion</p>
|
||||||
|
<p class="mt-1 font-medium text-surface-900 dark:text-surface-0">{{ assetName }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-surface-500">Numero de Serie</p>
|
||||||
|
<p class="mt-1 font-medium text-surface-900 dark:text-surface-0">{{ serial }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-surface-500">Custodio Actual</p>
|
||||||
|
<p class="mt-1 font-medium text-surface-900 dark:text-surface-0">{{ custodian }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import Breadcrumb from 'primevue/breadcrumb';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import StructureParentInfoCard from './StructureParentInfoCard.vue';
|
||||||
|
import StructureContentsTable from './StructureContentsTable.vue';
|
||||||
|
import StructureValuationSummary from './StructureValuationSummary.vue';
|
||||||
|
import StructureControlInfoCard from './StructureControlInfoCard.vue';
|
||||||
|
import { fixedAssetStructuresService } from '../../services/fixedAssetStructuresService';
|
||||||
|
import type { FixedAssetStructure } from '../../types/fixedAssetStructure';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(true);
|
||||||
|
const structure = ref<FixedAssetStructure | null>(null);
|
||||||
|
|
||||||
|
const breadcrumbItems = computed(() => [
|
||||||
|
{ label: 'Inicio', route: '/' },
|
||||||
|
{ label: 'Estructuras de Activos', route: '/fixed-assets/structures' },
|
||||||
|
{ label: structure.value?.code ?? 'Detalle' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const home = { icon: 'pi pi-home', route: '/' };
|
||||||
|
|
||||||
|
const loadDetails = async () => {
|
||||||
|
const id = String(route.params.id);
|
||||||
|
structure.value = await fixedAssetStructuresService.getStructureById(id);
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToEdit = () => {
|
||||||
|
if (!structure.value) return;
|
||||||
|
router.push(`/fixed-assets/structures/${structure.value.id}/edit`);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadDetails);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="space-y-5">
|
||||||
|
<Breadcrumb :home="home" :model="breadcrumbItems" />
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0">
|
||||||
|
Detalle de Estructura de Activo
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-surface-500 dark:text-surface-400">
|
||||||
|
Consulta el arbol de relacion y datos de control del activo compuesto.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button label="Editar estructura" icon="pi pi-pencil" :disabled="!structure" @click="goToEdit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="rounded-xl border border-surface-200 bg-surface-0 p-8 text-center text-surface-500 dark:border-surface-700 dark:bg-surface-900">
|
||||||
|
Cargando informacion de la estructura...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="structure" class="grid grid-cols-1 gap-5 xl:grid-cols-12">
|
||||||
|
<div class="space-y-5 xl:col-span-8">
|
||||||
|
<StructureParentInfoCard :structure="structure" readOnly />
|
||||||
|
<StructureContentsTable :model-value="structure.contents" readOnly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-5 xl:col-span-4">
|
||||||
|
<StructureValuationSummary :structure="structure" />
|
||||||
|
<StructureControlInfoCard :structure="structure" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="rounded-xl border border-surface-200 bg-surface-0 p-8 text-center text-surface-500 dark:border-surface-700 dark:bg-surface-900">
|
||||||
|
No se encontro la estructura solicitada.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import Breadcrumb from 'primevue/breadcrumb';
|
||||||
|
import InputNumber from 'primevue/inputnumber';
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import StructureParentInfoCard from './StructureParentInfoCard.vue';
|
||||||
|
import StructureValuationSummary from './StructureValuationSummary.vue';
|
||||||
|
import StructureControlInfoCard from './StructureControlInfoCard.vue';
|
||||||
|
import StructureContentsTable from './StructureContentsTable.vue';
|
||||||
|
import { fixedAssetStructuresService } from '../../services/fixedAssetStructuresService';
|
||||||
|
import type { FixedAssetStructure } from '../../types/fixedAssetStructure';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const emptyStructure = (): FixedAssetStructure => ({
|
||||||
|
id: '',
|
||||||
|
code: 'NUEVA',
|
||||||
|
name: '',
|
||||||
|
containerCategory: 'Oficina Movil',
|
||||||
|
containerSerial: '',
|
||||||
|
location: '',
|
||||||
|
containerValue: 0,
|
||||||
|
status: 'BORRADOR',
|
||||||
|
controlInfo: {
|
||||||
|
statusLabel: 'Borrador / En proceso',
|
||||||
|
createdAt: new Date().toLocaleDateString('es-MX', { day: '2-digit', month: 'long', year: 'numeric' }),
|
||||||
|
owner: 'Usuario actual'
|
||||||
|
},
|
||||||
|
contents: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const structure = ref<FixedAssetStructure>(emptyStructure());
|
||||||
|
const isEditing = computed(() => Boolean(route.params.id));
|
||||||
|
|
||||||
|
const title = computed(() =>
|
||||||
|
isEditing.value ? 'Editar Estructura de Activo Compuesto' : 'Crear Estructura de Activo Compuesto'
|
||||||
|
);
|
||||||
|
|
||||||
|
const breadcrumbItems = computed(() => [
|
||||||
|
{ label: 'Inicio', route: '/' },
|
||||||
|
{ label: 'Gestion de Activos', route: '/fixed-assets' },
|
||||||
|
{ label: isEditing.value ? 'Editar Estructura' : 'Crear Estructura' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const home = { icon: 'pi pi-home', route: '/' };
|
||||||
|
|
||||||
|
const loadStructure = async () => {
|
||||||
|
if (!route.params.id) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
const found = await fixedAssetStructuresService.getStructureById(String(route.params.id));
|
||||||
|
if (found) {
|
||||||
|
structure.value = JSON.parse(JSON.stringify(found)) as FixedAssetStructure;
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveStructure = () => {
|
||||||
|
router.push('/fixed-assets/structures');
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadStructure);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="space-y-5">
|
||||||
|
<Breadcrumb :home="home" :model="breadcrumbItems" />
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-surface-500 dark:text-surface-400">
|
||||||
|
Configure la jerarquia de activos y asigne los elementos contenidos en el contenedor principal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button label="Cancelar" outlined severity="secondary" @click="cancel" />
|
||||||
|
<Button label="Guardar estructura" icon="pi pi-check" :loading="loading" @click="saveStructure" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-5 xl:grid-cols-12">
|
||||||
|
<div class="space-y-5 xl:col-span-8">
|
||||||
|
<StructureParentInfoCard :structure="structure" />
|
||||||
|
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-wallet text-primary"></i>
|
||||||
|
<span>Valor del Activo Padre</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="max-w-sm space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wide text-surface-500">
|
||||||
|
Valor de contenedor
|
||||||
|
</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model="structure.containerValue"
|
||||||
|
mode="currency"
|
||||||
|
currency="MXN"
|
||||||
|
locale="es-MX"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<StructureContentsTable v-model="structure.contents" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-5 xl:col-span-4">
|
||||||
|
<StructureValuationSummary :structure="structure" />
|
||||||
|
<StructureControlInfoCard :structure="structure" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,182 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import IconField from 'primevue/iconfield';
|
||||||
|
import InputIcon from 'primevue/inputicon';
|
||||||
|
import Select from 'primevue/select';
|
||||||
|
import Paginator from 'primevue/paginator';
|
||||||
|
import { fixedAssetStructuresService } from '../../services/fixedAssetStructuresService';
|
||||||
|
import type { FixedAssetStructure, StructureStatus } from '../../types/fixedAssetStructure';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const first = ref(0);
|
||||||
|
const rows = ref(6);
|
||||||
|
const searchTerm = ref('');
|
||||||
|
const selectedStatus = ref<'all' | StructureStatus>('all');
|
||||||
|
const structures = ref<FixedAssetStructure[]>([]);
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: 'Todos los estatus', value: 'all' },
|
||||||
|
{ label: 'Borrador', value: 'BORRADOR' },
|
||||||
|
{ label: 'Activa', value: 'ACTIVA' },
|
||||||
|
{ label: 'En revision', value: 'EN_REVISION' },
|
||||||
|
{ label: 'Inactiva', value: 'INACTIVA' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusClasses: Record<StructureStatus, string> = {
|
||||||
|
BORRADOR: 'bg-surface-200 text-surface-700',
|
||||||
|
ACTIVA: 'bg-emerald-100 text-emerald-700',
|
||||||
|
EN_REVISION: 'bg-amber-100 text-amber-700',
|
||||||
|
INACTIVA: 'bg-red-100 text-red-700'
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) =>
|
||||||
|
`$${value.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
|
|
||||||
|
const filteredStructures = computed(() => {
|
||||||
|
const query = searchTerm.value.trim().toLowerCase();
|
||||||
|
return structures.value.filter((structure) => {
|
||||||
|
const matchesQuery = !query
|
||||||
|
|| structure.code.toLowerCase().includes(query)
|
||||||
|
|| structure.name.toLowerCase().includes(query)
|
||||||
|
|| structure.containerSerial.toLowerCase().includes(query);
|
||||||
|
const matchesStatus = selectedStatus.value === 'all' || structure.status === selectedStatus.value;
|
||||||
|
return matchesQuery && matchesStatus;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginatedStructures = computed(() =>
|
||||||
|
filteredStructures.value.slice(first.value, first.value + rows.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadStructures = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
structures.value = await fixedAssetStructuresService.getStructures();
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPage = (event: { first: number; rows: number }) => {
|
||||||
|
first.value = event.first;
|
||||||
|
rows.value = event.rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToCreate = () => router.push('/fixed-assets/structures/create');
|
||||||
|
const goToDetails = (id: string) => router.push(`/fixed-assets/structures/${id}`);
|
||||||
|
const goToEdit = (id: string) => router.push(`/fixed-assets/structures/${id}/edit`);
|
||||||
|
|
||||||
|
onMounted(loadStructures);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="space-y-6">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0">
|
||||||
|
Estructuras de Activos Fijos
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-surface-500 dark:text-surface-400">
|
||||||
|
Define, organiza y monitorea activos compuestos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button label="Crear estructura" icon="pi pi-plus" @click="goToCreate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
|
<IconField iconPosition="left" class="w-full md:max-w-lg">
|
||||||
|
<InputIcon class="pi pi-search" />
|
||||||
|
<InputText
|
||||||
|
v-model="searchTerm"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Buscar por codigo, nombre o numero de serie..."
|
||||||
|
/>
|
||||||
|
</IconField>
|
||||||
|
<Select
|
||||||
|
v-model="selectedStatus"
|
||||||
|
:options="statusOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full md:w-56"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl border border-surface-200 dark:border-surface-700">
|
||||||
|
<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">Codigo</th>
|
||||||
|
<th class="px-4 py-3">Estructura</th>
|
||||||
|
<th class="px-4 py-3">Categoria</th>
|
||||||
|
<th class="px-4 py-3">Ubicacion</th>
|
||||||
|
<th class="px-4 py-3">Contenidos</th>
|
||||||
|
<th class="px-4 py-3">Valor Total</th>
|
||||||
|
<th class="px-4 py-3">Estatus</th>
|
||||||
|
<th class="px-4 py-3 text-right">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="loading">
|
||||||
|
<td colspan="8" class="px-4 py-8 text-center text-sm text-surface-500">
|
||||||
|
Cargando estructuras...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
v-for="structure in paginatedStructures"
|
||||||
|
:key="structure.id"
|
||||||
|
class="border-t border-surface-200 text-sm dark:border-surface-700"
|
||||||
|
>
|
||||||
|
<td class="px-4 py-3 font-mono text-xs text-primary">{{ structure.code }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<p class="font-semibold text-surface-900 dark:text-surface-0">{{ structure.name }}</p>
|
||||||
|
<p class="text-xs text-surface-500 dark:text-surface-400">Serie: {{ structure.containerSerial }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">{{ structure.containerCategory }}</td>
|
||||||
|
<td class="px-4 py-3">{{ structure.location }}</td>
|
||||||
|
<td class="px-4 py-3 text-center font-semibold">{{ structure.contents.length }}</td>
|
||||||
|
<td class="px-4 py-3 font-semibold">{{ formatCurrency(structure.containerValue + structure.contents.reduce((sum, item) => sum + item.value, 0)) }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold" :class="statusClasses[structure.status]">
|
||||||
|
{{ structure.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex items-center justify-end gap-1">
|
||||||
|
<Button icon="pi pi-eye" text rounded size="small" @click="goToDetails(structure.id)" />
|
||||||
|
<Button icon="pi pi-pencil" text rounded size="small" @click="goToEdit(structure.id)" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!loading && paginatedStructures.length === 0">
|
||||||
|
<td colspan="8" class="px-4 py-8 text-center text-sm text-surface-500 dark:text-surface-400">
|
||||||
|
No hay estructuras para mostrar.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</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 {{ paginatedStructures.length }} de {{ filteredStructures.length }} estructuras
|
||||||
|
</p>
|
||||||
|
<Paginator
|
||||||
|
:first="first"
|
||||||
|
:rows="rows"
|
||||||
|
:totalRecords="filteredStructures.length"
|
||||||
|
:rowsPerPageOptions="[6, 12, 18]"
|
||||||
|
template="PrevPageLink PageLinks NextPageLink"
|
||||||
|
@page="onPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,181 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import type { ContentCondition, StructureContentAsset } from '../../types/fixedAssetStructure';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: StructureContentAsset[];
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: StructureContentAsset[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const serialQuery = ref('');
|
||||||
|
|
||||||
|
const availableAssets: StructureContentAsset[] = [
|
||||||
|
{
|
||||||
|
id: 'ACT-5001',
|
||||||
|
name: 'Monitor Curvo 34"',
|
||||||
|
category: 'Perifericos',
|
||||||
|
serial: 'MON-CV-4491',
|
||||||
|
condition: 'EXCELENTE',
|
||||||
|
value: 9850
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-5002',
|
||||||
|
name: 'Docking Station USB-C',
|
||||||
|
category: 'Perifericos',
|
||||||
|
serial: 'DCK-USB-112',
|
||||||
|
condition: 'BUENO',
|
||||||
|
value: 3150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-5003',
|
||||||
|
name: 'Gabinete de Red 24U',
|
||||||
|
category: 'Infraestructura TI',
|
||||||
|
serial: 'NET-CAB-240',
|
||||||
|
condition: 'REGULAR',
|
||||||
|
value: 6250
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const conditionClasses: Record<ContentCondition, string> = {
|
||||||
|
EXCELENTE: 'bg-emerald-100 text-emerald-700',
|
||||||
|
BUENO: 'bg-amber-100 text-amber-700',
|
||||||
|
REGULAR: 'bg-orange-100 text-orange-700'
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalValue = computed(() =>
|
||||||
|
props.modelValue.reduce((sum, item) => sum + item.value, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) =>
|
||||||
|
`$${value.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
|
|
||||||
|
const addAsset = () => {
|
||||||
|
const query = serialQuery.value.trim().toLowerCase();
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
const candidate = availableAssets.find((asset) =>
|
||||||
|
asset.serial.toLowerCase().includes(query) || asset.id.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!candidate) return;
|
||||||
|
const exists = props.modelValue.some((item) => item.id === candidate.id);
|
||||||
|
if (exists) return;
|
||||||
|
|
||||||
|
emit('update:modelValue', [...props.modelValue, candidate]);
|
||||||
|
serialQuery.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAsset = (assetId: string) => {
|
||||||
|
emit('update:modelValue', props.modelValue.filter((item) => item.id !== assetId));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2 text-xl">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i class="pi pi-list-check text-primary"></i>
|
||||||
|
<span>Inventario de Activos Contenidos</span>
|
||||||
|
</div>
|
||||||
|
<span class="rounded-full bg-surface-100 px-3 py-1 text-xs font-semibold text-surface-600 dark:bg-surface-800 dark:text-surface-200">
|
||||||
|
{{ modelValue.length }} ELEMENTOS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex flex-col gap-2 md:flex-row">
|
||||||
|
<InputText
|
||||||
|
v-model="serialQuery"
|
||||||
|
class="w-full"
|
||||||
|
:disabled="readOnly"
|
||||||
|
placeholder="Escriba o escanee numero de serie para anadir..."
|
||||||
|
@keyup.enter="addAsset"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Agregar"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
class="min-w-40"
|
||||||
|
:disabled="readOnly"
|
||||||
|
@click="addAsset"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-surface-500 dark:text-surface-400">
|
||||||
|
Tip: puede ingresar serie o ID del activo para anexarlo rapidamente.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl border border-surface-200 dark:border-surface-700">
|
||||||
|
<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">Imagen</th>
|
||||||
|
<th class="px-4 py-3">Activo</th>
|
||||||
|
<th class="px-4 py-3">Serie</th>
|
||||||
|
<th class="px-4 py-3">Condicion</th>
|
||||||
|
<th class="px-4 py-3">Valor</th>
|
||||||
|
<th class="px-4 py-3 text-right">Accion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="asset in modelValue"
|
||||||
|
:key="asset.id"
|
||||||
|
class="border-t border-surface-200 text-sm dark:border-surface-700"
|
||||||
|
>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-surface-100 dark:bg-surface-800">
|
||||||
|
<i class="pi pi-image text-surface-400"></i>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<p class="font-semibold text-surface-900 dark:text-surface-0">{{ asset.name }}</p>
|
||||||
|
<p class="text-xs text-surface-500 dark:text-surface-400">{{ asset.category }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 font-mono text-xs text-surface-600 dark:text-surface-300">{{ asset.serial }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold" :class="conditionClasses[asset.condition]">
|
||||||
|
{{ asset.condition }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 font-semibold text-surface-900 dark:text-surface-0">
|
||||||
|
{{ formatCurrency(asset.value) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-trash"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
severity="danger"
|
||||||
|
size="small"
|
||||||
|
:disabled="readOnly"
|
||||||
|
@click="removeAsset(asset.id)"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="modelValue.length === 0">
|
||||||
|
<td colspan="6" class="px-4 py-8 text-center text-sm text-surface-500 dark:text-surface-400">
|
||||||
|
No hay activos vinculados a la estructura.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="text-sm text-surface-500 dark:text-surface-400">Valor acumulado de contenidos: </span>
|
||||||
|
<strong class="text-lg text-surface-900 dark:text-surface-0">{{ formatCurrency(totalValue) }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import type { FixedAssetStructure } from '../../types/fixedAssetStructure';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
structure: FixedAssetStructure;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-base">
|
||||||
|
<i class="pi pi-id-card text-primary"></i>
|
||||||
|
<span>Informacion de Control</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-surface-400">Estado de estructura</p>
|
||||||
|
<p class="mt-1 text-base font-semibold text-surface-900 dark:text-surface-0">{{ structure.controlInfo.statusLabel }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-surface-400">Fecha de creacion</p>
|
||||||
|
<p class="mt-1 text-base font-medium text-surface-800 dark:text-surface-100">{{ structure.controlInfo.createdAt }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-surface-400">Responsable</p>
|
||||||
|
<p class="mt-1 text-base font-medium text-surface-800 dark:text-surface-100">{{ structure.controlInfo.owner }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Select from 'primevue/select';
|
||||||
|
import type { FixedAssetStructure } from '../../types/fixedAssetStructure';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
structure: FixedAssetStructure;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const containerCategories = [
|
||||||
|
{ label: 'Oficina Movil', value: 'Oficina Movil' },
|
||||||
|
{ label: 'Punto de Venta', value: 'Punto de Venta' },
|
||||||
|
{ label: 'Infraestructura TI', value: 'Infraestructura TI' },
|
||||||
|
{ label: 'Contenedor Operativo', value: 'Contenedor Operativo' }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-xl">
|
||||||
|
<i class="pi pi-building-columns text-primary"></i>
|
||||||
|
<span>Informacion del Activo Padre</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wide text-surface-500">
|
||||||
|
Nombre del contenedor
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="structure.name"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Ej: Oficina Movil Mod-01"
|
||||||
|
:disabled="readOnly"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wide text-surface-500">
|
||||||
|
Categoria de contenedor
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
v-model="structure.containerCategory"
|
||||||
|
:options="containerCategories"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full"
|
||||||
|
:disabled="readOnly"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wide text-surface-500">
|
||||||
|
Numero de serie
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="structure.containerSerial"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="SER-CONT-2024-001"
|
||||||
|
:disabled="readOnly"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wide text-surface-500">
|
||||||
|
Ubicacion fisica
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="structure.location"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Planta Norte - Sector B"
|
||||||
|
:disabled="readOnly"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { FixedAssetStructure } from '../../types/fixedAssetStructure';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
structure: FixedAssetStructure;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const contentsTotal = computed(() =>
|
||||||
|
props.structure.contents.reduce((sum, item) => sum + item.value, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalStructureValue = computed(() => props.structure.containerValue + contentsTotal.value);
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) =>
|
||||||
|
`$${value.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="summary-card border-0 shadow-lg">
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4 text-white">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] opacity-90">
|
||||||
|
Resumen de valoracion
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="opacity-90">Valor de contenedor</span>
|
||||||
|
<strong class="text-xl">{{ formatCurrency(structure.containerValue) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="opacity-90">Valor de contenidos ({{ structure.contents.length }})</span>
|
||||||
|
<strong class="text-xl">{{ formatCurrency(contentsTotal) }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-white/20 pt-4">
|
||||||
|
<p class="text-xs uppercase tracking-wide opacity-80">Valor total de estructura</p>
|
||||||
|
<p class="mt-1 text-4xl font-black leading-none">{{ formatCurrency(totalStructureValue) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg bg-white/12 p-3 text-xs leading-relaxed opacity-95">
|
||||||
|
El valor total se calcula sumando el activo base mas todos los elementos vinculados.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.summary-card {
|
||||||
|
background: linear-gradient(145deg, #1f7ae0 0%, #0b60c2 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
124
src/modules/fixed-assets/services/fixedAssetStructuresService.ts
Normal file
124
src/modules/fixed-assets/services/fixedAssetStructuresService.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import type { FixedAssetStructure } from '../types/fixedAssetStructure';
|
||||||
|
|
||||||
|
const structuresMock: FixedAssetStructure[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
code: 'EST-0001',
|
||||||
|
name: 'Oficina Movil Mod-01',
|
||||||
|
containerCategory: 'Oficina Movil',
|
||||||
|
containerSerial: 'SER-CONT-2024-001',
|
||||||
|
location: 'Planta Norte - Sector B',
|
||||||
|
containerValue: 12500,
|
||||||
|
status: 'BORRADOR',
|
||||||
|
controlInfo: {
|
||||||
|
statusLabel: 'Borrador / En proceso',
|
||||||
|
createdAt: '24 de Mayo, 2024',
|
||||||
|
owner: 'Carlos Mendez'
|
||||||
|
},
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
id: 'ACT-2001',
|
||||||
|
name: 'Laptop Dell XPS 15',
|
||||||
|
category: 'Equipos de Computo',
|
||||||
|
serial: 'DELL-8829-XP',
|
||||||
|
condition: 'EXCELENTE',
|
||||||
|
value: 26500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-2002',
|
||||||
|
name: 'Silla Ergonomica Herman Miller',
|
||||||
|
category: 'Mobiliario de Oficina',
|
||||||
|
serial: 'MOB-HM-7721',
|
||||||
|
condition: 'EXCELENTE',
|
||||||
|
value: 11900
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-2003',
|
||||||
|
name: 'Escritorio Ajustable Pro',
|
||||||
|
category: 'Mobiliario de Oficina',
|
||||||
|
serial: 'MOB-DK-4410',
|
||||||
|
condition: 'BUENO',
|
||||||
|
value: 11000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
code: 'EST-0002',
|
||||||
|
name: 'Kit Punto de Venta Sucursal Centro',
|
||||||
|
containerCategory: 'Punto de Venta',
|
||||||
|
containerSerial: 'POS-CN-001',
|
||||||
|
location: 'Sucursal Centro',
|
||||||
|
containerValue: 9800,
|
||||||
|
status: 'ACTIVA',
|
||||||
|
controlInfo: {
|
||||||
|
statusLabel: 'Aprobada / Activa',
|
||||||
|
createdAt: '12 de Enero, 2025',
|
||||||
|
owner: 'Valeria Ponce'
|
||||||
|
},
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
id: 'ACT-3122',
|
||||||
|
name: 'Terminal POS Verifone',
|
||||||
|
category: 'Equipos POS',
|
||||||
|
serial: 'POS-VERI-8891',
|
||||||
|
condition: 'EXCELENTE',
|
||||||
|
value: 7300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-3123',
|
||||||
|
name: 'Impresora Termica Epson',
|
||||||
|
category: 'Perifericos',
|
||||||
|
serial: 'EPS-TM-T20',
|
||||||
|
condition: 'BUENO',
|
||||||
|
value: 2650
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
code: 'EST-0003',
|
||||||
|
name: 'Cabina Tecnica Almacen A',
|
||||||
|
containerCategory: 'Infraestructura TI',
|
||||||
|
containerSerial: 'CB-TI-004',
|
||||||
|
location: 'Almacen General - Pasillo A',
|
||||||
|
containerValue: 22000,
|
||||||
|
status: 'EN_REVISION',
|
||||||
|
controlInfo: {
|
||||||
|
statusLabel: 'Revision Tecnica',
|
||||||
|
createdAt: '03 de Febrero, 2026',
|
||||||
|
owner: 'Ana Sofia Ruiz'
|
||||||
|
},
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
id: 'ACT-4410',
|
||||||
|
name: 'Switch Cisco 48p',
|
||||||
|
category: 'Networking',
|
||||||
|
serial: 'CS-48-101',
|
||||||
|
condition: 'EXCELENTE',
|
||||||
|
value: 15300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ACT-4411',
|
||||||
|
name: 'UPS APC 3000VA',
|
||||||
|
category: 'Energia',
|
||||||
|
serial: 'UPS-APC-771',
|
||||||
|
condition: 'REGULAR',
|
||||||
|
value: 4800
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
export const fixedAssetStructuresService = {
|
||||||
|
async getStructures(): Promise<FixedAssetStructure[]> {
|
||||||
|
await wait(120);
|
||||||
|
return structuresMock;
|
||||||
|
},
|
||||||
|
async getStructureById(id: string): Promise<FixedAssetStructure | null> {
|
||||||
|
await wait(120);
|
||||||
|
return structuresMock.find((structure) => structure.id === id) ?? null;
|
||||||
|
}
|
||||||
|
};
|
||||||
13
src/modules/fixed-assets/types/fixedAsset.ts
Normal file
13
src/modules/fixed-assets/types/fixedAsset.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export interface FixedAssetFormData {
|
||||||
|
name: string;
|
||||||
|
serial: string;
|
||||||
|
category: string;
|
||||||
|
brand: string;
|
||||||
|
model: string;
|
||||||
|
purchaseDate: string;
|
||||||
|
purchasePrice: number | null;
|
||||||
|
initialLocation: string;
|
||||||
|
initialStatus: string;
|
||||||
|
responsibleEmployee: string;
|
||||||
|
imageFileName: string;
|
||||||
|
}
|
||||||
23
src/modules/fixed-assets/types/fixedAssetAssignment.ts
Normal file
23
src/modules/fixed-assets/types/fixedAssetAssignment.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export interface AssignmentAssetOption {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
serial: string;
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssignmentEmployeeOption {
|
||||||
|
id: string;
|
||||||
|
initials: string;
|
||||||
|
fullName: string;
|
||||||
|
role: string;
|
||||||
|
department: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FixedAssetAssignmentFormData {
|
||||||
|
assetId: string;
|
||||||
|
employeeId: string;
|
||||||
|
deliveredAt: string;
|
||||||
|
condition: string;
|
||||||
|
notes: string;
|
||||||
|
}
|
||||||
30
src/modules/fixed-assets/types/fixedAssetStructure.ts
Normal file
30
src/modules/fixed-assets/types/fixedAssetStructure.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export type StructureStatus = 'BORRADOR' | 'ACTIVA' | 'EN_REVISION' | 'INACTIVA';
|
||||||
|
export type ContentCondition = 'EXCELENTE' | 'BUENO' | 'REGULAR';
|
||||||
|
|
||||||
|
export interface StructureContentAsset {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
serial: string;
|
||||||
|
condition: ContentCondition;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StructureControlInfo {
|
||||||
|
statusLabel: string;
|
||||||
|
createdAt: string;
|
||||||
|
owner: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FixedAssetStructure {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
containerCategory: string;
|
||||||
|
containerSerial: string;
|
||||||
|
location: string;
|
||||||
|
containerValue: number;
|
||||||
|
status: StructureStatus;
|
||||||
|
controlInfo: StructureControlInfo;
|
||||||
|
contents: StructureContentAsset[];
|
||||||
|
}
|
||||||
@ -12,6 +12,14 @@ import Units from '../modules/catalog/components/units/Units.vue';
|
|||||||
import ProductsIndex from '../modules/products/components/ProductsIndex.vue';
|
import ProductsIndex from '../modules/products/components/ProductsIndex.vue';
|
||||||
import ProductForm from '../modules/products/components/ProductForm.vue';
|
import ProductForm from '../modules/products/components/ProductForm.vue';
|
||||||
import StoresIndex from '../modules/stores/components/StoresIndex.vue';
|
import StoresIndex from '../modules/stores/components/StoresIndex.vue';
|
||||||
|
import FixedAssetsIndex from '../modules/fixed-assets/components/FixedAssetsIndex.vue';
|
||||||
|
import FixedAssetForm from '../modules/fixed-assets/components/assets/FixedAssetForm.vue';
|
||||||
|
import FixedAssetAssignmentsIndex from '../modules/fixed-assets/components/assignments/FixedAssetAssignmentsIndex.vue';
|
||||||
|
import FixedAssetAssignmentForm from '../modules/fixed-assets/components/assignments/FixedAssetAssignmentForm.vue';
|
||||||
|
import FixedAssetAssignmentOffboardingForm from '../modules/fixed-assets/components/assignments/FixedAssetAssignmentOffboardingForm.vue';
|
||||||
|
import FixedAssetStructuresIndex from '../modules/fixed-assets/components/structures/FixedAssetStructuresIndex.vue';
|
||||||
|
import FixedAssetStructureForm from '../modules/fixed-assets/components/structures/FixedAssetStructureForm.vue';
|
||||||
|
import FixedAssetStructureDetails from '../modules/fixed-assets/components/structures/FixedAssetStructureDetails.vue';
|
||||||
|
|
||||||
import RolesIndex from '../modules/users/components/RoleIndex.vue';
|
import RolesIndex from '../modules/users/components/RoleIndex.vue';
|
||||||
import RoleForm from '../modules/users/components/RoleForm.vue';
|
import RoleForm from '../modules/users/components/RoleForm.vue';
|
||||||
@ -230,6 +238,107 @@ const routes: RouteRecordRaw[] = [
|
|||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'fixed-assets',
|
||||||
|
name: 'FixedAssetsModule',
|
||||||
|
meta: {
|
||||||
|
title: 'Activos Fijos',
|
||||||
|
requiresAuth: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'FixedAssets',
|
||||||
|
component: FixedAssetsIndex,
|
||||||
|
meta: {
|
||||||
|
title: 'Registro de Activos',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
name: 'FixedAssetCreate',
|
||||||
|
component: FixedAssetForm,
|
||||||
|
meta: {
|
||||||
|
title: 'Registrar Activo Fijo',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'assignments',
|
||||||
|
name: 'FixedAssetAssignmentsModule',
|
||||||
|
meta: {
|
||||||
|
title: 'Asignaciones de Activos',
|
||||||
|
requiresAuth: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'FixedAssetAssignments',
|
||||||
|
component: FixedAssetAssignmentsIndex,
|
||||||
|
meta: {
|
||||||
|
title: 'Listado de Asignaciones',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
name: 'FixedAssetAssignmentCreate',
|
||||||
|
component: FixedAssetAssignmentForm,
|
||||||
|
meta: {
|
||||||
|
title: 'Asignacion de Activo a Empleado',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/offboarding',
|
||||||
|
name: 'FixedAssetAssignmentOffboarding',
|
||||||
|
component: FixedAssetAssignmentOffboardingForm,
|
||||||
|
meta: {
|
||||||
|
title: 'Baja de Asignacion de Activo',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'structures',
|
||||||
|
name: 'FixedAssetStructures',
|
||||||
|
component: FixedAssetStructuresIndex,
|
||||||
|
meta: {
|
||||||
|
title: 'Estructuras de Activos',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'structures/create',
|
||||||
|
name: 'FixedAssetStructuresCreate',
|
||||||
|
component: FixedAssetStructureForm,
|
||||||
|
meta: {
|
||||||
|
title: 'Crear Estructura de Activo',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'structures/:id/edit',
|
||||||
|
name: 'FixedAssetStructuresEdit',
|
||||||
|
component: FixedAssetStructureForm,
|
||||||
|
meta: {
|
||||||
|
title: 'Editar Estructura de Activo',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'structures/:id',
|
||||||
|
name: 'FixedAssetStructuresDetails',
|
||||||
|
component: FixedAssetStructureDetails,
|
||||||
|
meta: {
|
||||||
|
title: 'Detalle de Estructura de Activo',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'stores/:id',
|
path: 'stores/:id',
|
||||||
name: 'StoreDetails',
|
name: 'StoreDetails',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user