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:
parent
6fe7c82c6d
commit
ee3d0e1134
@ -79,11 +79,6 @@ const menuItems = ref<MenuItem[]>([
|
||||
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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -53,7 +53,7 @@ class AuthService {
|
||||
*/
|
||||
async register(data: RegisterData): Promise<LoginResponse> {
|
||||
try {
|
||||
const response = await api.post<LoginResponse>('/auth/register', data);
|
||||
const response = await api.post<LoginResponse>('/api/auth/register', data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.data?.message || 'Error al registrar usuario');
|
||||
@ -88,7 +88,7 @@ class AuthService {
|
||||
*/
|
||||
async getCurrentUser(): Promise<User> {
|
||||
try {
|
||||
const response = await api.get<User>('/auth/me');
|
||||
const response = await api.get<User>('/api/auth/me');
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.data?.message || 'Error al obtener usuario');
|
||||
@ -100,7 +100,7 @@ class AuthService {
|
||||
*/
|
||||
async updateProfile(data: Partial<User>): Promise<User> {
|
||||
try {
|
||||
const response = await api.put<User>('/auth/profile', data);
|
||||
const response = await api.put<User>('/api/auth/profile', data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.data?.message || 'Error al actualizar perfil');
|
||||
@ -126,7 +126,7 @@ class AuthService {
|
||||
*/
|
||||
async forgotPassword(email: string): Promise<void> {
|
||||
try {
|
||||
await api.post('/auth/forgot-password', { email });
|
||||
await api.post('/api/auth/forgot-password', { email });
|
||||
} catch (error: any) {
|
||||
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> {
|
||||
try {
|
||||
await api.post('/auth/reset-password', { token, password });
|
||||
await api.post('/api/auth/reset-password', { token, password });
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.data?.message || 'Error al resetear contraseña');
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Button from 'primevue/button';
|
||||
import Card from 'primevue/card';
|
||||
@ -8,134 +8,88 @@ import IconField from 'primevue/iconfield';
|
||||
import InputIcon from 'primevue/inputicon';
|
||||
import Select from 'primevue/select';
|
||||
import Paginator from 'primevue/paginator';
|
||||
import fixedAssetsService, { type Asset, type StatusOption } from '../services/fixedAssetsService';
|
||||
|
||||
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 selectedStatus = ref<number | null>(null);
|
||||
const rowsPerPage = ref(15);
|
||||
const currentPage = ref(1);
|
||||
const totalRecords = ref(0);
|
||||
const assets = ref<Asset[]>([]);
|
||||
const statusOptions = ref<{ label: string; value: number | null }[]>([
|
||||
{ label: 'Todos los Estatus', value: null }
|
||||
]);
|
||||
const loading = ref(false);
|
||||
|
||||
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;
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
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 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 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 formatCurrency = (value: number) => {
|
||||
return `$${value.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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'
|
||||
onMounted(() => {
|
||||
fetchAssets();
|
||||
fetchStatusOptions();
|
||||
});
|
||||
|
||||
watch(selectedStatus, () => {
|
||||
currentPage.value = 1;
|
||||
fetchAssets();
|
||||
});
|
||||
|
||||
watch(searchQuery, () => {
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
currentPage.value = 1;
|
||||
fetchAssets();
|
||||
}, 400);
|
||||
});
|
||||
|
||||
const first = computed(() => (currentPage.value - 1) * rowsPerPage.value);
|
||||
|
||||
const onPageChange = (event: { first: number; rows: number; page: number }) => {
|
||||
currentPage.value = event.page + 1;
|
||||
fetchAssets();
|
||||
};
|
||||
|
||||
const goToStructures = () => {
|
||||
router.push('/fixed-assets/structures');
|
||||
const formatCurrency = (value: string | number) => {
|
||||
const num = typeof value === 'string' ? parseFloat(value) : value;
|
||||
return `$${num.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
};
|
||||
|
||||
const statusClasses: Record<number, string> = {
|
||||
1: 'bg-emerald-100 text-emerald-700',
|
||||
2: 'bg-gray-100 text-gray-700',
|
||||
3: 'bg-amber-100 text-amber-700',
|
||||
4: 'bg-red-100 text-red-700',
|
||||
};
|
||||
|
||||
const goToCreateAsset = () => {
|
||||
@ -145,6 +99,16 @@ const goToCreateAsset = () => {
|
||||
const goToAssignment = () => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -161,27 +125,12 @@ const goToAssignment = () => {
|
||||
</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"
|
||||
@ -204,26 +153,12 @@ const goToAssignment = () => {
|
||||
<InputText
|
||||
v-model="searchQuery"
|
||||
class="w-full"
|
||||
placeholder="Buscar por código, serie o descripción..."
|
||||
placeholder="Buscar por código o etiqueta..."
|
||||
/>
|
||||
</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"
|
||||
@ -238,23 +173,29 @@ const goToAssignment = () => {
|
||||
<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">SKU</th>
|
||||
<th class="px-4 py-4">Producto</th>
|
||||
<th class="px-4 py-4">Etiqueta</th>
|
||||
<th class="px-4 py-4">Asignado a</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<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
|
||||
v-for="asset in paginatedAssets"
|
||||
v-else
|
||||
v-for="asset in assets"
|
||||
: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 }}
|
||||
{{ asset.sku }}
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
@ -263,36 +204,42 @@ const goToAssignment = () => {
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-surface-900 dark:text-surface-0">
|
||||
{{ asset.name }}
|
||||
{{ asset.inventory_warehouse?.product?.name ?? 'Sin producto' }}
|
||||
</p>
|
||||
<p class="text-xs text-surface-500 dark:text-surface-400">
|
||||
Serie: {{ asset.serial }}
|
||||
Serie: {{ asset.inventory_warehouse?.product?.serial_number ?? 'N/A' }}
|
||||
</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">{{ asset.asset_tag ?? '—' }}</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">
|
||||
<span
|
||||
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>
|
||||
</td>
|
||||
<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 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" />
|
||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger" @click="handleDelete(asset)" />
|
||||
</div>
|
||||
</td>
|
||||
</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">
|
||||
No se encontraron activos con los filtros seleccionados.
|
||||
</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">
|
||||
<p class="text-sm text-surface-500 dark:text-surface-400">
|
||||
Mostrando {{ paginatedAssets.length }} de {{ totalRecords }} activos
|
||||
Mostrando {{ assets.length }} de {{ totalRecords }} activos
|
||||
</p>
|
||||
|
||||
<Paginator
|
||||
:first="first"
|
||||
:rows="rowsPerPage"
|
||||
:totalRecords="totalRecords"
|
||||
:rowsPerPageOptions="[4, 8, 12]"
|
||||
template="PrevPageLink PageLinks NextPageLink"
|
||||
@page="onPageChange"
|
||||
/>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<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';
|
||||
@ -11,12 +10,9 @@ interface Props {
|
||||
|
||||
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' }
|
||||
const depreciationOptions = [
|
||||
{ label: 'Linea Recta', value: 'straight_line' },
|
||||
{ label: 'Saldo Decreciente', value: 'declining_balance' }
|
||||
];
|
||||
</script>
|
||||
|
||||
@ -25,44 +21,47 @@ const locationOptions = [
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2 text-xl">
|
||||
<i class="pi pi-wallet text-primary"></i>
|
||||
<span>Adquisicion y Ubicacion</span>
|
||||
<span>Depreciacion y Vida Util</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
|
||||
Vida Util Estimada (meses) *
|
||||
</label>
|
||||
<InputText
|
||||
v-model="form.purchaseDate"
|
||||
type="date"
|
||||
<InputNumber
|
||||
v-model="form.estimated_useful_life"
|
||||
: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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||
Precio de Compra (MXN)
|
||||
Valor Residual (MXN)
|
||||
</label>
|
||||
<InputNumber
|
||||
v-model="form.purchasePrice"
|
||||
v-model="form.residual_value"
|
||||
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"
|
||||
:min="0"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Card from 'primevue/card';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Select from 'primevue/select';
|
||||
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
||||
|
||||
interface Props {
|
||||
@ -9,45 +9,39 @@ interface Props {
|
||||
}
|
||||
|
||||
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>
|
||||
<i class="pi pi-shield text-primary"></i>
|
||||
<span>Garantia</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
|
||||
Dias de Garantia
|
||||
</label>
|
||||
<Select
|
||||
v-model="form.initialStatus"
|
||||
:options="statusOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
<InputNumber
|
||||
v-model="form.warranty_days"
|
||||
:min="0"
|
||||
suffix=" dias"
|
||||
class="w-full"
|
||||
placeholder="Ej: 365"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||
Empleado Responsable
|
||||
Fecha Fin de Garantia
|
||||
</label>
|
||||
<InputText
|
||||
v-model="form.responsibleEmployee"
|
||||
v-model="form.warranty_end_date"
|
||||
type="date"
|
||||
class="w-full"
|
||||
placeholder="Buscar empleado por nombre o ID"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -7,7 +7,7 @@ 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 fixedAssetsService from '../../services/fixedAssetsService';
|
||||
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
||||
|
||||
const router = useRouter();
|
||||
@ -15,46 +15,64 @@ const toast = useToast();
|
||||
const saving = ref(false);
|
||||
|
||||
const form = ref<FixedAssetFormData>({
|
||||
name: '',
|
||||
serial: '',
|
||||
category: '',
|
||||
brand: '',
|
||||
model: '',
|
||||
purchaseDate: '',
|
||||
purchasePrice: null,
|
||||
initialLocation: '',
|
||||
initialStatus: 'Disponible',
|
||||
responsibleEmployee: '',
|
||||
imageFileName: ''
|
||||
inventory_warehouse_id: null,
|
||||
estimated_useful_life: null,
|
||||
depreciation_method: 'straight_line',
|
||||
residual_value: null,
|
||||
asset_tag: '',
|
||||
warranty_days: null,
|
||||
warranty_end_date: ''
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
router.push('/fixed-assets');
|
||||
router.push('/api/fixed-assets');
|
||||
};
|
||||
|
||||
const saveAsset = async () => {
|
||||
if (!form.value.name || !form.value.serial) {
|
||||
if (!form.value.inventory_warehouse_id || !form.value.estimated_useful_life) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
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
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
saving.value = false;
|
||||
try {
|
||||
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',
|
||||
};
|
||||
|
||||
if (form.value.residual_value != null) payload.residual_value = form.value.residual_value;
|
||||
if (form.value.asset_tag) payload.asset_tag = form.value.asset_tag;
|
||||
if (form.value.warranty_days != null) payload.warranty_days = form.value.warranty_days;
|
||||
if (form.value.warranty_end_date) payload.warranty_end_date = form.value.warranty_end_date;
|
||||
|
||||
await fixedAssetsService.createAsset(payload);
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Activo registrado',
|
||||
detail: `El activo "${form.value.name}" se registro correctamente.`,
|
||||
detail: 'El activo fijo se registro correctamente.',
|
||||
life: 2600
|
||||
});
|
||||
|
||||
router.push('/fixed-assets');
|
||||
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>
|
||||
|
||||
@ -67,23 +85,17 @@ const saveAsset = async () => {
|
||||
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.
|
||||
Selecciona un producto del inventario de almacen para darlo de alta como activo fijo.
|
||||
</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">
|
||||
<div class="grid grid-cols-1 gap-5 xl:grid-cols-2">
|
||||
<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
|
||||
|
||||
@ -1,22 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import Card from 'primevue/card';
|
||||
import InputText from 'primevue/inputtext';
|
||||
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 {
|
||||
form: FixedAssetFormData;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
const props = 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' }
|
||||
];
|
||||
interface Warehouse {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -29,62 +76,51 @@ const categoryOptions = [
|
||||
</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
|
||||
Almacen *
|
||||
</label>
|
||||
<Select
|
||||
v-model="form.category"
|
||||
:options="categoryOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
v-model="selectedWarehouseId"
|
||||
:options="warehouses"
|
||||
optionLabel="name"
|
||||
optionValue="id"
|
||||
filter
|
||||
:loading="loadingWarehouses"
|
||||
placeholder="Selecciona un almacen..."
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||
Marca
|
||||
Producto *
|
||||
</label>
|
||||
<InputText
|
||||
v-model="form.brand"
|
||||
<Select
|
||||
v-model="form.inventory_warehouse_id"
|
||||
:options="inventoryItems"
|
||||
:optionLabel="formatItemLabel"
|
||||
optionValue="id"
|
||||
filter
|
||||
:loading="loadingItems"
|
||||
:disabled="!selectedWarehouseId"
|
||||
placeholder="Selecciona un producto..."
|
||||
class="w-full"
|
||||
placeholder="Ej: Toyota, Dell, Bosch"
|
||||
/>
|
||||
</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">
|
||||
Modelo
|
||||
Etiqueta del Activo
|
||||
</label>
|
||||
<InputText
|
||||
v-model="form.model"
|
||||
v-model="form.asset_tag"
|
||||
class="w-full"
|
||||
placeholder="Ej: Series-X 2023"
|
||||
placeholder="Ej: ACT-OFC-001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
120
src/modules/fixed-assets/services/fixedAssetsService.ts
Normal file
120
src/modules/fixed-assets/services/fixedAssetsService.ts
Normal 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;
|
||||
@ -1,13 +1,24 @@
|
||||
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;
|
||||
inventory_warehouse_id: number | null;
|
||||
estimated_useful_life: number | null;
|
||||
depreciation_method: string;
|
||||
residual_value: number | null;
|
||||
asset_tag: string;
|
||||
warranty_days: number | null;
|
||||
warranty_end_date: string;
|
||||
}
|
||||
|
||||
export interface InventoryWarehouseOption {
|
||||
id: number;
|
||||
serial_number: string | null;
|
||||
quantity: number;
|
||||
purchase_cost: number;
|
||||
product: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
warehouse: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
@ -17,9 +17,6 @@ import FixedAssetForm from '../modules/fixed-assets/components/assets/FixedAsset
|
||||
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 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
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user