Compare commits
2 Commits
6fe7c82c6d
...
d6d91aeaf9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6d91aeaf9 | ||
|
|
ee3d0e1134 |
@ -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' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 ?? [];
|
||||||
const paginatedAssets = computed(() => {
|
totalRecords.value = pageData.total ?? 0;
|
||||||
return filteredAssets.value.slice(first.value, first.value + rowsPerPage.value);
|
} catch (error) {
|
||||||
});
|
console.error('Error al cargar activos:', error);
|
||||||
|
} finally {
|
||||||
const totalRecords = computed(() => filteredAssets.value.length);
|
loading.value = false;
|
||||||
|
}
|
||||||
const onPageChange = (event: { first: number; rows: number }) => {
|
|
||||||
first.value = event.first;
|
|
||||||
rowsPerPage.value = event.rows;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCurrency = (value: number) => {
|
const fetchStatusOptions = async () => {
|
||||||
return `$${value.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
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> = {
|
onMounted(() => {
|
||||||
ASIGNADO: 'bg-blue-100 text-blue-700',
|
fetchAssets();
|
||||||
MANTENIMIENTO: 'bg-amber-100 text-amber-700',
|
fetchStatusOptions();
|
||||||
DISPONIBLE: 'bg-emerald-100 text-emerald-700'
|
});
|
||||||
|
|
||||||
|
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 = () => {
|
const formatCurrency = (value: string | number) => {
|
||||||
router.push('/fixed-assets/structures');
|
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 = () => {
|
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 contable</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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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',
|
||||||
|
};
|
||||||
|
|
||||||
|
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({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Activo registrado',
|
summary: 'Activo registrado',
|
||||||
detail: `El activo "${form.value.name}" se registro correctamente.`,
|
detail: 'El activo fijo se registro correctamente.',
|
||||||
life: 2600
|
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>
|
</script>
|
||||||
|
|
||||||
@ -67,23 +85,17 @@ 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>
|
||||||
|
|
||||||
<div class="xl:col-span-4">
|
|
||||||
<FixedAssetImageCard :form="form" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-end gap-3">
|
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||||
<Button label="Cancelar" text severity="secondary" @click="cancel" />
|
<Button label="Cancelar" text severity="secondary" @click="cancel" />
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 {
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user