refactor: elimina componentes y servicios de estructuras de activos fijos

- Se eliminaron componentes relacionados a estructuras de activos fijos y su servicio.
- Se actualizó fixedAsset.ts con la nueva estructura y campos adicionales.
- Se agregó fixedAssetsService.ts para gestionar activos fijos.
- Se eliminaron rutas relacionadas en el router.
This commit is contained in:
Juan Felipe Zapata Moreno 2026-03-18 18:05:25 -06:00
parent 6fe7c82c6d
commit ee3d0e1134
20 changed files with 419 additions and 1304 deletions

View File

@ -79,11 +79,6 @@ const menuItems = ref<MenuItem[]>([
items: [ items: [
{ label: 'Registro de Activos', icon: 'pi pi-building', to: '/fixed-assets' }, { label: 'Registro de Activos', icon: 'pi pi-building', to: '/fixed-assets' },
{ label: 'Asignacion a Empleado', icon: 'pi pi-send', to: '/fixed-assets/assignments' }, { 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' },
], ],
}, },
{ {

View File

@ -53,7 +53,7 @@ class AuthService {
*/ */
async register(data: RegisterData): Promise<LoginResponse> { async register(data: RegisterData): Promise<LoginResponse> {
try { try {
const response = await api.post<LoginResponse>('/auth/register', data); const response = await api.post<LoginResponse>('/api/auth/register', data);
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al registrar usuario'); throw new Error(error.response?.data?.message || 'Error al registrar usuario');
@ -88,7 +88,7 @@ class AuthService {
*/ */
async getCurrentUser(): Promise<User> { async getCurrentUser(): Promise<User> {
try { try {
const response = await api.get<User>('/auth/me'); const response = await api.get<User>('/api/auth/me');
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al obtener usuario'); throw new Error(error.response?.data?.message || 'Error al obtener usuario');
@ -100,7 +100,7 @@ class AuthService {
*/ */
async updateProfile(data: Partial<User>): Promise<User> { async updateProfile(data: Partial<User>): Promise<User> {
try { try {
const response = await api.put<User>('/auth/profile', data); const response = await api.put<User>('/api/auth/profile', data);
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al actualizar perfil'); throw new Error(error.response?.data?.message || 'Error al actualizar perfil');
@ -126,7 +126,7 @@ class AuthService {
*/ */
async forgotPassword(email: string): Promise<void> { async forgotPassword(email: string): Promise<void> {
try { try {
await api.post('/auth/forgot-password', { email }); await api.post('/api/auth/forgot-password', { email });
} catch (error: any) { } catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al solicitar recuperación'); throw new Error(error.response?.data?.message || 'Error al solicitar recuperación');
} }
@ -137,7 +137,7 @@ class AuthService {
*/ */
async resetPassword(token: string, password: string): Promise<void> { async resetPassword(token: string, password: string): Promise<void> {
try { try {
await api.post('/auth/reset-password', { token, password }); await api.post('/api/auth/reset-password', { token, password });
} catch (error: any) { } catch (error: any) {
throw new Error(error.response?.data?.message || 'Error al resetear contraseña'); throw new Error(error.response?.data?.message || 'Error al resetear contraseña');
} }

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import Button from 'primevue/button'; import Button from 'primevue/button';
import Card from 'primevue/card'; import Card from 'primevue/card';
@ -8,134 +8,88 @@ import IconField from 'primevue/iconfield';
import InputIcon from 'primevue/inputicon'; import InputIcon from 'primevue/inputicon';
import Select from 'primevue/select'; import Select from 'primevue/select';
import Paginator from 'primevue/paginator'; import Paginator from 'primevue/paginator';
import fixedAssetsService, { type Asset, type StatusOption } from '../services/fixedAssetsService';
const router = useRouter(); 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 searchQuery = ref('');
const selectedCategory = ref('all'); const selectedStatus = ref<number | null>(null);
const selectedStatus = ref('all'); const rowsPerPage = ref(15);
const rowsPerPage = ref(4); const currentPage = ref(1);
const first = ref(0); const totalRecords = ref(0);
const assets = ref<Asset[]>([]);
const categoryOptions = [ const statusOptions = ref<{ label: string; value: number | null }[]>([
{ label: 'Todas las Categorías', value: 'all' }, { label: 'Todos los Estatus', value: null }
{ 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 loading = ref(false);
const filteredAssets = computed(() => { let searchTimeout: ReturnType<typeof setTimeout> | null = null;
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 fetchAssets = async () => {
}); loading.value = true;
try {
const response = await fixedAssetsService.getAssets({
q: searchQuery.value || undefined,
status: selectedStatus.value ?? undefined,
page: currentPage.value,
});
const pageData = response as any;
assets.value = pageData.data ?? [];
totalRecords.value = pageData.total ?? 0;
} catch (error) {
console.error('Error al cargar activos:', error);
} finally {
loading.value = false;
}
};
const fetchStatusOptions = async () => {
try {
const options = await fixedAssetsService.getStatusOptions();
statusOptions.value = [
{ label: 'Todos los Estatus', value: null },
...options.map((opt: StatusOption) => ({ label: opt.name, value: opt.id }))
];
} catch (error) {
console.error('Error al cargar estatus:', error);
}
};
onMounted(() => {
fetchAssets();
fetchStatusOptions();
}); });
const paginatedAssets = computed(() => { watch(selectedStatus, () => {
return filteredAssets.value.slice(first.value, first.value + rowsPerPage.value); currentPage.value = 1;
fetchAssets();
}); });
const totalRecords = computed(() => filteredAssets.value.length); watch(searchQuery, () => {
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentPage.value = 1;
fetchAssets();
}, 400);
});
const onPageChange = (event: { first: number; rows: number }) => { const first = computed(() => (currentPage.value - 1) * rowsPerPage.value);
first.value = event.first;
rowsPerPage.value = event.rows; const onPageChange = (event: { first: number; rows: number; page: number }) => {
currentPage.value = event.page + 1;
fetchAssets();
}; };
const formatCurrency = (value: number) => { const formatCurrency = (value: string | number) => {
return `$${value.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; const num = typeof value === 'string' ? parseFloat(value) : value;
return `$${num.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}; };
const statusClasses: Record<FixedAsset['status'], string> = { const statusClasses: Record<number, string> = {
ASIGNADO: 'bg-blue-100 text-blue-700', 1: 'bg-emerald-100 text-emerald-700',
MANTENIMIENTO: 'bg-amber-100 text-amber-700', 2: 'bg-gray-100 text-gray-700',
DISPONIBLE: 'bg-emerald-100 text-emerald-700' 3: 'bg-amber-100 text-amber-700',
}; 4: 'bg-red-100 text-red-700',
const goToStructures = () => {
router.push('/fixed-assets/structures');
}; };
const goToCreateAsset = () => { const goToCreateAsset = () => {
@ -145,6 +99,16 @@ const goToCreateAsset = () => {
const goToAssignment = () => { const goToAssignment = () => {
router.push('/fixed-assets/assignments'); router.push('/fixed-assets/assignments');
}; };
const handleDelete = async (asset: Asset) => {
if (!confirm(`¿Estás seguro de eliminar el activo ${asset.sku}?`)) return;
try {
await fixedAssetsService.deleteAsset(asset.id);
fetchAssets();
} catch (error) {
console.error('Error al eliminar activo:', error);
}
};
</script> </script>
<template> <template>
@ -161,27 +125,12 @@ const goToAssignment = () => {
</p> </p>
</div> </div>
<div class="flex flex-wrap items-center gap-3"> <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 <Button
label="Registrar Activo" label="Registrar Activo"
icon="pi pi-plus" icon="pi pi-plus"
class="min-w-[200px]" class="min-w-[200px]"
@click="goToCreateAsset" @click="goToCreateAsset"
/> />
<Button
label="Estructuras"
icon="pi pi-sitemap"
severity="secondary"
outlined
class="min-w-40"
@click="goToStructures"
/>
<Button <Button
label="Asignar Activo" label="Asignar Activo"
icon="pi pi-send" icon="pi pi-send"
@ -204,26 +153,12 @@ const goToAssignment = () => {
<InputText <InputText
v-model="searchQuery" v-model="searchQuery"
class="w-full" class="w-full"
placeholder="Buscar por código, serie o descripción..." placeholder="Buscar por código o etiqueta..."
/> />
</IconField> </IconField>
<Button
icon="pi pi-sliders-h"
outlined
severity="secondary"
aria-label="Filtros avanzados"
/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<Select
v-model="selectedCategory"
:options="categoryOptions"
optionLabel="label"
optionValue="value"
class="min-w-48"
/>
<Select <Select
v-model="selectedStatus" v-model="selectedStatus"
:options="statusOptions" :options="statusOptions"
@ -238,23 +173,29 @@ const goToAssignment = () => {
<table class="min-w-full border-collapse"> <table class="min-w-full border-collapse">
<thead> <thead>
<tr class="bg-surface-50 text-left text-xs font-semibold uppercase tracking-wide text-surface-500 dark:bg-surface-800 dark:text-surface-300"> <tr class="bg-surface-50 text-left text-xs font-semibold uppercase tracking-wide text-surface-500 dark:bg-surface-800 dark:text-surface-300">
<th class="px-4 py-4">ID</th> <th class="px-4 py-4">SKU</th>
<th class="px-4 py-4">Descripción / Activo</th> <th class="px-4 py-4">Producto</th>
<th class="px-4 py-4">Categoría</th> <th class="px-4 py-4">Etiqueta</th>
<th class="px-4 py-4">Ubicación</th> <th class="px-4 py-4">Asignado a</th>
<th class="px-4 py-4">Estatus</th> <th class="px-4 py-4">Estatus</th>
<th class="px-4 py-4">Valor MXN</th> <th class="px-4 py-4">Valor</th>
<th class="px-4 py-4 text-right">Acciones</th> <th class="px-4 py-4 text-right">Acciones</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-if="loading">
<td colspan="7" class="px-4 py-10 text-center text-surface-500 dark:text-surface-400">
<i class="pi pi-spin pi-spinner mr-2"></i>Cargando activos...
</td>
</tr>
<tr <tr
v-for="asset in paginatedAssets" v-else
v-for="asset in assets"
:key="asset.id" :key="asset.id"
class="border-t border-surface-200 text-sm text-surface-700 dark:border-surface-700 dark:text-surface-200" 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"> <td class="px-4 py-4 font-medium text-surface-500 dark:text-surface-400">
{{ asset.id }} {{ asset.sku }}
</td> </td>
<td class="px-4 py-4"> <td class="px-4 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
@ -263,36 +204,42 @@ const goToAssignment = () => {
</div> </div>
<div> <div>
<p class="font-semibold text-surface-900 dark:text-surface-0"> <p class="font-semibold text-surface-900 dark:text-surface-0">
{{ asset.name }} {{ asset.inventory_warehouse?.product?.name ?? 'Sin producto' }}
</p> </p>
<p class="text-xs text-surface-500 dark:text-surface-400"> <p class="text-xs text-surface-500 dark:text-surface-400">
Serie: {{ asset.serial }} Serie: {{ asset.inventory_warehouse?.product?.serial_number ?? 'N/A' }}
</p> </p>
</div> </div>
</div> </div>
</td> </td>
<td class="px-4 py-4">{{ asset.category }}</td> <td class="px-4 py-4">{{ asset.asset_tag ?? '—' }}</td>
<td class="px-4 py-4">{{ asset.location }}</td> <td class="px-4 py-4">
<template v-if="asset.active_assignment">
<p class="font-medium">{{ asset.active_assignment.employee.name }}</p>
<p class="text-xs text-surface-500">{{ asset.active_assignment.employee.department?.name }}</p>
</template>
<span v-else class="text-surface-400">Sin asignar</span>
</td>
<td class="px-4 py-4"> <td class="px-4 py-4">
<span <span
class="inline-flex rounded-full px-3 py-1 text-xs font-semibold" class="inline-flex rounded-full px-3 py-1 text-xs font-semibold"
:class="statusClasses[asset.status]" :class="statusClasses[asset.status?.id] ?? 'bg-gray-100 text-gray-700'"
> >
{{ asset.status }} {{ asset.status?.name ?? 'Desconocido' }}
</span> </span>
</td> </td>
<td class="px-4 py-4 text-lg font-semibold text-surface-900 dark:text-surface-0"> <td class="px-4 py-4 text-lg font-semibold text-surface-900 dark:text-surface-0">
{{ formatCurrency(asset.value) }} {{ formatCurrency(asset.book_value) }}
</td> </td>
<td class="px-4 py-4"> <td class="px-4 py-4">
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Button icon="pi pi-pencil" text rounded size="small" /> <Button icon="pi pi-pencil" text rounded size="small" />
<Button icon="pi pi-qrcode" text rounded size="small" /> <Button icon="pi pi-qrcode" text rounded size="small" />
<Button icon="pi pi-trash" text rounded size="small" severity="danger" /> <Button icon="pi pi-trash" text rounded size="small" severity="danger" @click="handleDelete(asset)" />
</div> </div>
</td> </td>
</tr> </tr>
<tr v-if="paginatedAssets.length === 0"> <tr v-if="!loading && assets.length === 0">
<td colspan="7" class="px-4 py-10 text-center text-surface-500 dark:text-surface-400"> <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. No se encontraron activos con los filtros seleccionados.
</td> </td>
@ -303,14 +250,13 @@ const goToAssignment = () => {
<div class="flex flex-col gap-2 pt-1 md:flex-row md:items-center md:justify-between"> <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"> <p class="text-sm text-surface-500 dark:text-surface-400">
Mostrando {{ paginatedAssets.length }} de {{ totalRecords }} activos Mostrando {{ assets.length }} de {{ totalRecords }} activos
</p> </p>
<Paginator <Paginator
:first="first" :first="first"
:rows="rowsPerPage" :rows="rowsPerPage"
:totalRecords="totalRecords" :totalRecords="totalRecords"
:rowsPerPageOptions="[4, 8, 12]"
template="PrevPageLink PageLinks NextPageLink" template="PrevPageLink PageLinks NextPageLink"
@page="onPageChange" @page="onPageChange"
/> />

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import Card from 'primevue/card'; import Card from 'primevue/card';
import InputText from 'primevue/inputtext';
import InputNumber from 'primevue/inputnumber'; import InputNumber from 'primevue/inputnumber';
import Select from 'primevue/select'; import Select from 'primevue/select';
import type { FixedAssetFormData } from '../../types/fixedAsset'; import type { FixedAssetFormData } from '../../types/fixedAsset';
@ -11,12 +10,9 @@ interface Props {
defineProps<Props>(); defineProps<Props>();
const locationOptions = [ const depreciationOptions = [
{ label: 'Seleccione ubicacion de almacen', value: '' }, { label: 'Linea Recta', value: 'straight_line' },
{ label: 'Almacen General', value: 'Almacen General' }, { label: 'Saldo Decreciente', value: 'declining_balance' }
{ label: 'Sucursal Norte', value: 'Sucursal Norte' },
{ label: 'Oficina Central - Piso 2', value: 'Oficina Central - Piso 2' },
{ label: 'Data Center', value: 'Data Center' }
]; ];
</script> </script>
@ -25,44 +21,47 @@ const locationOptions = [
<template #title> <template #title>
<div class="flex items-center gap-2 text-xl"> <div class="flex items-center gap-2 text-xl">
<i class="pi pi-wallet text-primary"></i> <i class="pi pi-wallet text-primary"></i>
<span>Adquisicion y Ubicacion</span> <span>Depreciacion y Vida Util</span>
</div> </div>
</template> </template>
<template #content> <template #content>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100"> <label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
Fecha de Compra Vida Util Estimada (meses) *
</label> </label>
<InputText <InputNumber
v-model="form.purchaseDate" v-model="form.estimated_useful_life"
type="date" :min="1"
suffix=" meses"
class="w-full"
placeholder="Ej: 60"
/>
</div>
<div class="space-y-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
Metodo de Depreciacion
</label>
<Select
v-model="form.depreciation_method"
:options="depreciationOptions"
optionLabel="label"
optionValue="value"
class="w-full" class="w-full"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100"> <label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
Precio de Compra (MXN) Valor Residual (MXN)
</label> </label>
<InputNumber <InputNumber
v-model="form.purchasePrice" v-model="form.residual_value"
mode="currency" mode="currency"
currency="MXN" currency="MXN"
locale="es-MX" locale="es-MX"
class="w-full" :min="0"
/>
</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" class="w-full"
/> />
</div> </div>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Card from 'primevue/card'; import Card from 'primevue/card';
import InputNumber from 'primevue/inputnumber';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import Select from 'primevue/select';
import type { FixedAssetFormData } from '../../types/fixedAsset'; import type { FixedAssetFormData } from '../../types/fixedAsset';
interface Props { interface Props {
@ -9,45 +9,39 @@ interface Props {
} }
defineProps<Props>(); defineProps<Props>();
const statusOptions = [
{ label: 'Disponible', value: 'Disponible' },
{ label: 'Asignado', value: 'Asignado' },
{ label: 'Mantenimiento', value: 'Mantenimiento' }
];
</script> </script>
<template> <template>
<Card class="shadow-sm"> <Card class="shadow-sm">
<template #title> <template #title>
<div class="flex items-center gap-2 text-xl"> <div class="flex items-center gap-2 text-xl">
<i class="pi pi-user-plus text-primary"></i> <i class="pi pi-shield text-primary"></i>
<span>Asignacion Inicial</span> <span>Garantia</span>
</div> </div>
</template> </template>
<template #content> <template #content>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100"> <label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
Estatus Inicial Dias de Garantia
</label> </label>
<Select <InputNumber
v-model="form.initialStatus" v-model="form.warranty_days"
:options="statusOptions" :min="0"
optionLabel="label" suffix=" dias"
optionValue="value"
class="w-full" class="w-full"
placeholder="Ej: 365"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100"> <label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
Empleado Responsable Fecha Fin de Garantia
</label> </label>
<InputText <InputText
v-model="form.responsibleEmployee" v-model="form.warranty_end_date"
type="date"
class="w-full" class="w-full"
placeholder="Buscar empleado por nombre o ID"
/> />
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@ import Button from 'primevue/button';
import FixedAssetGeneralInfoSection from './FixedAssetGeneralInfoSection.vue'; import FixedAssetGeneralInfoSection from './FixedAssetGeneralInfoSection.vue';
import FixedAssetAcquisitionSection from './FixedAssetAcquisitionSection.vue'; import FixedAssetAcquisitionSection from './FixedAssetAcquisitionSection.vue';
import FixedAssetAssignmentSection from './FixedAssetAssignmentSection.vue'; import FixedAssetAssignmentSection from './FixedAssetAssignmentSection.vue';
import FixedAssetImageCard from './FixedAssetImageCard.vue'; import fixedAssetsService from '../../services/fixedAssetsService';
import type { FixedAssetFormData } from '../../types/fixedAsset'; import type { FixedAssetFormData } from '../../types/fixedAsset';
const router = useRouter(); const router = useRouter();
@ -15,46 +15,64 @@ const toast = useToast();
const saving = ref(false); const saving = ref(false);
const form = ref<FixedAssetFormData>({ const form = ref<FixedAssetFormData>({
name: '', inventory_warehouse_id: null,
serial: '', estimated_useful_life: null,
category: '', depreciation_method: 'straight_line',
brand: '', residual_value: null,
model: '', asset_tag: '',
purchaseDate: '', warranty_days: null,
purchasePrice: null, warranty_end_date: ''
initialLocation: '',
initialStatus: 'Disponible',
responsibleEmployee: '',
imageFileName: ''
}); });
const cancel = () => { const cancel = () => {
router.push('/fixed-assets'); router.push('/api/fixed-assets');
}; };
const saveAsset = async () => { const saveAsset = async () => {
if (!form.value.name || !form.value.serial) { if (!form.value.inventory_warehouse_id || !form.value.estimated_useful_life) {
toast.add({ toast.add({
severity: 'warn', severity: 'warn',
summary: 'Campos requeridos', summary: 'Campos requeridos',
detail: 'Completa nombre y numero de serie del activo.', detail: 'Selecciona un producto de almacen e indica la vida util estimada.',
life: 3000 life: 3000
}); });
return; return;
} }
saving.value = true; saving.value = true;
await new Promise((resolve) => setTimeout(resolve, 500)); try {
saving.value = false; const payload: Record<string, unknown> = {
inventory_warehouse_id: form.value.inventory_warehouse_id,
estimated_useful_life: form.value.estimated_useful_life,
depreciation_method: form.value.depreciation_method || 'straight_line',
};
toast.add({ if (form.value.residual_value != null) payload.residual_value = form.value.residual_value;
severity: 'success', if (form.value.asset_tag) payload.asset_tag = form.value.asset_tag;
summary: 'Activo registrado', if (form.value.warranty_days != null) payload.warranty_days = form.value.warranty_days;
detail: `El activo "${form.value.name}" se registro correctamente.`, if (form.value.warranty_end_date) payload.warranty_end_date = form.value.warranty_end_date;
life: 2600
});
router.push('/fixed-assets'); await fixedAssetsService.createAsset(payload);
toast.add({
severity: 'success',
summary: 'Activo registrado',
detail: 'El activo fijo se registro correctamente.',
life: 2600
});
router.push('/api/fixed-assets');
} catch (error: any) {
const message = error.response?.data?.message || 'Error al registrar el activo.';
toast.add({
severity: 'error',
summary: 'Error',
detail: message,
life: 4000
});
} finally {
saving.value = false;
}
}; };
</script> </script>
@ -67,21 +85,15 @@ const saveAsset = async () => {
Registrar Nuevo Activo Fijo Registrar Nuevo Activo Fijo
</h1> </h1>
<p class="mt-1 text-surface-500 dark:text-surface-400"> <p class="mt-1 text-surface-500 dark:text-surface-400">
Complete los detalles para dar de alta un nuevo activo en el inventario global del almacen. Selecciona un producto del inventario de almacen para darlo de alta como activo fijo.
</p> </p>
</div> </div>
<FixedAssetGeneralInfoSection :form="form" /> <FixedAssetGeneralInfoSection :form="form" />
<div class="grid grid-cols-1 gap-5 xl:grid-cols-12"> <div class="grid grid-cols-1 gap-5 xl:grid-cols-2">
<div class="space-y-5 xl:col-span-8"> <FixedAssetAcquisitionSection :form="form" />
<FixedAssetAcquisitionSection :form="form" /> <FixedAssetAssignmentSection :form="form" />
<FixedAssetAssignmentSection :form="form" />
</div>
<div class="xl:col-span-4">
<FixedAssetImageCard :form="form" />
</div>
</div> </div>
<div class="flex flex-wrap items-center justify-end gap-3"> <div class="flex flex-wrap items-center justify-end gap-3">

View File

@ -1,22 +1,69 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue';
import Card from 'primevue/card'; import Card from 'primevue/card';
import InputText from 'primevue/inputtext';
import Select from 'primevue/select'; import Select from 'primevue/select';
import type { FixedAssetFormData } from '../../types/fixedAsset'; import InputText from 'primevue/inputtext';
import type { FixedAssetFormData, InventoryWarehouseOption } from '../../types/fixedAsset';
import api from '../../../../services/api';
interface Props { interface Props {
form: FixedAssetFormData; form: FixedAssetFormData;
} }
defineProps<Props>(); const props = defineProps<Props>();
const categoryOptions = [ interface Warehouse {
{ label: 'Seleccione una categoria', value: '' }, id: number;
{ label: 'Computo', value: 'Computo' }, name: string;
{ label: 'Maquinaria', value: 'Maquinaria' }, }
{ label: 'Mobiliario', value: 'Mobiliario' },
{ label: 'Infraestructura TI', value: 'Infraestructura TI' } const warehouses = ref<Warehouse[]>([]);
]; const selectedWarehouseId = ref<number | null>(null);
const inventoryItems = ref<InventoryWarehouseOption[]>([]);
const loadingWarehouses = ref(false);
const loadingItems = ref(false);
const fetchWarehouses = async () => {
loadingWarehouses.value = true;
try {
const response = await api.get('/api/warehouses');
warehouses.value = response.data.data.warehouses?.data ?? response.data.data.warehouses ?? [];
} catch (error) {
console.error('Error al cargar almacenes:', error);
} finally {
loadingWarehouses.value = false;
}
};
const fetchInventoryItems = async (warehouseId: number) => {
loadingItems.value = true;
props.form.inventory_warehouse_id = null;
inventoryItems.value = [];
try {
const response = await api.get(`/api/warehouses/${warehouseId}`);
inventoryItems.value = response.data.data.items ?? [];
} catch (error) {
console.error('Error al cargar items de almacén:', error);
} finally {
loadingItems.value = false;
}
};
watch(selectedWarehouseId, (id) => {
if (id) fetchInventoryItems(id);
else {
inventoryItems.value = [];
props.form.inventory_warehouse_id = null;
}
});
const formatItemLabel = (item: InventoryWarehouseOption) => {
const product = item.product?.name ?? 'Sin producto';
const serial = item.serial_number ? `${item.serial_number}` : '';
return `${product}${serial}`;
};
fetchWarehouses();
</script> </script>
<template> <template>
@ -29,62 +76,51 @@ const categoryOptions = [
</template> </template>
<template #content> <template #content>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <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"> <div class="space-y-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100"> <label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
Numero de Serie / Serial * Almacen *
</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> </label>
<Select <Select
v-model="form.category" v-model="selectedWarehouseId"
:options="categoryOptions" :options="warehouses"
optionLabel="label" optionLabel="name"
optionValue="value" optionValue="id"
filter
:loading="loadingWarehouses"
placeholder="Selecciona un almacen..."
class="w-full" class="w-full"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100"> <label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
Marca Producto *
</label> </label>
<InputText <Select
v-model="form.brand" v-model="form.inventory_warehouse_id"
:options="inventoryItems"
:optionLabel="formatItemLabel"
optionValue="id"
filter
:loading="loadingItems"
:disabled="!selectedWarehouseId"
placeholder="Selecciona un producto..."
class="w-full" class="w-full"
placeholder="Ej: Toyota, Dell, Bosch"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2 md:col-span-2">
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100"> <label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
Modelo Etiqueta del Activo
</label> </label>
<InputText <InputText
v-model="form.model" v-model="form.asset_tag"
class="w-full" class="w-full"
placeholder="Ej: Series-X 2023" placeholder="Ej: ACT-OFC-001"
/> />
</div> </div>
</div> </div>
</template> </template>
</Card> </Card>

View File

@ -1,64 +0,0 @@
<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>

View File

@ -1,76 +0,0 @@
<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>

View File

@ -1,128 +0,0 @@
<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>

View File

@ -1,182 +0,0 @@
<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>

View File

@ -1,181 +0,0 @@
<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>

View File

@ -1,37 +0,0 @@
<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>

View File

@ -1,81 +0,0 @@
<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>

View File

@ -1,56 +0,0 @@
<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>

View File

@ -1,124 +0,0 @@
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;
}
};

View File

@ -0,0 +1,120 @@
import api from '../../../services/api';
export interface Asset {
id: number;
sku: string;
asset_tag: string | null;
status: { id: number; name: string };
estimated_useful_life: number | null;
depreciation_method: string;
residual_value: string;
accumulated_depreciation: string;
book_value: string;
warranty_days: number | null;
warranty_end_date: string | null;
inventory_warehouse: {
id: number;
product: {
id: number;
name: string;
serial_number?: string;
category?: string;
};
} | null;
active_assignment: {
id: number;
employee: {
id: number;
name: string;
department: {
id: number;
name: string;
} | null;
};
} | null;
}
export interface AssetsPaginatedResponse {
status: string;
data: {
current_page: number;
data: Asset[];
last_page: number;
per_page: number;
total: number;
};
}
export interface AssetsListResponse {
status: string;
data: {
data: Asset[];
};
}
export interface AssetDetailResponse {
status: string;
data: {
data: Asset;
};
}
export interface StatusOption {
id: number;
name: string;
}
export interface StatusOptionsResponse {
status: string;
data: {
data: StatusOption[];
};
}
interface AssetFilters {
q?: string;
status?: number;
page?: number;
paginate?: boolean;
}
class FixedAssetsService {
async getAssets(filters: AssetFilters = {}): Promise<AssetsPaginatedResponse> {
const params: Record<string, string | number | boolean> = {};
if (filters.q) params.q = filters.q;
if (filters.status) params.status = filters.status;
if (filters.page) params.page = filters.page;
if (filters.paginate !== undefined) params.paginate = filters.paginate;
const response = await api.get<AssetsPaginatedResponse>('/api/assets', { params });
return response.data;
}
async getAsset(id: number): Promise<AssetDetailResponse> {
const response = await api.get<AssetDetailResponse>(`/api/assets/${id}`);
return response.data;
}
async createAsset(data: Record<string, unknown>): Promise<AssetDetailResponse> {
const response = await api.post<AssetDetailResponse>('/api/assets', data);
return response.data;
}
async updateAsset(id: number, data: Record<string, unknown>): Promise<AssetDetailResponse> {
const response = await api.put<AssetDetailResponse>(`/api/assets/${id}`, data);
return response.data;
}
async deleteAsset(id: number): Promise<void> {
await api.delete(`/api/assets/${id}`);
}
async getStatusOptions(): Promise<StatusOption[]> {
const response = await api.get<StatusOptionsResponse>('/api/assets/options/status');
return response.data.data.data;
}
}
export const fixedAssetsService = new FixedAssetsService();
export default fixedAssetsService;

View File

@ -1,13 +1,24 @@
export interface FixedAssetFormData { export interface FixedAssetFormData {
name: string; inventory_warehouse_id: number | null;
serial: string; estimated_useful_life: number | null;
category: string; depreciation_method: string;
brand: string; residual_value: number | null;
model: string; asset_tag: string;
purchaseDate: string; warranty_days: number | null;
purchasePrice: number | null; warranty_end_date: string;
initialLocation: string; }
initialStatus: string;
responsibleEmployee: string; export interface InventoryWarehouseOption {
imageFileName: string; id: number;
serial_number: string | null;
quantity: number;
purchase_cost: number;
product: {
id: number;
name: string;
} | null;
warehouse: {
id: number;
name: string;
} | null;
} }

View File

@ -1,30 +0,0 @@
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[];
}

View File

@ -17,9 +17,6 @@ import FixedAssetForm from '../modules/fixed-assets/components/assets/FixedAsset
import FixedAssetAssignmentsIndex from '../modules/fixed-assets/components/assignments/FixedAssetAssignmentsIndex.vue'; import FixedAssetAssignmentsIndex from '../modules/fixed-assets/components/assignments/FixedAssetAssignmentsIndex.vue';
import FixedAssetAssignmentForm from '../modules/fixed-assets/components/assignments/FixedAssetAssignmentForm.vue'; import FixedAssetAssignmentForm from '../modules/fixed-assets/components/assignments/FixedAssetAssignmentForm.vue';
import FixedAssetAssignmentOffboardingForm from '../modules/fixed-assets/components/assignments/FixedAssetAssignmentOffboardingForm.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';
@ -324,42 +321,6 @@ const routes: RouteRecordRaw[] = [
} }
] ]
}, },
{
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
}
}
] ]
}, },
{ {