feat: agrega gestión de mantenimiento de activos fijos
- Se implementaron componentes de listado y formulario para mantenimientos. - Se agregaron rutas para listar, crear y editar mantenimientos. - Se integraron servicios para operaciones CRUD de mantenimiento. - Se mejoró el índice de activos con acceso a mantenimientos. - Se actualizó la lógica de asignaciones para soportar mantenimientos. - Se refactorizó la configuración de PrimeVue para manejo de temas. - Se agregaron tipos para mantenimientos y estructuras relacionadas.
This commit is contained in:
parent
8632e5c34a
commit
c4b8ba2037
1176
package-lock.json
generated
1176
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
1923
pnpm-lock.yaml
generated
1923
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
40
src/config/primevue.ts
Normal file
40
src/config/primevue.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import Aura from "@primeuix/themes/aura";
|
||||
import { definePreset } from "@primeuix/themes";
|
||||
|
||||
// Crear un preset personalizado basado en Aura con color azul
|
||||
const MyPreset = definePreset(Aura, {
|
||||
semantic: {
|
||||
primary: {
|
||||
50: "{blue.50}",
|
||||
100: "{blue.100}",
|
||||
200: "{blue.200}",
|
||||
300: "{blue.300}",
|
||||
400: "{blue.400}",
|
||||
500: "{blue.500}",
|
||||
600: "{blue.600}",
|
||||
700: "{blue.700}",
|
||||
800: "{blue.800}",
|
||||
900: "{blue.900}",
|
||||
950: "{blue.950}"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const primeVueConfig = {
|
||||
theme: {
|
||||
preset: MyPreset,
|
||||
options: {
|
||||
darkModeSelector: ".p-dark",
|
||||
},
|
||||
},
|
||||
locale: {
|
||||
dayNames: ['domingo','lunes','martes','miércoles','jueves','viernes','sábado'],
|
||||
dayNamesShort: ['dom','lun','mar','mié','jue','vie','sáb'],
|
||||
dayNamesMin: ['D','L','M','M','J','V','S'],
|
||||
monthNames: ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'],
|
||||
monthNamesShort: ['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic'],
|
||||
today: 'Hoy',
|
||||
weekHeader: 'Sem',
|
||||
firstDayOfWeek: 1
|
||||
}
|
||||
}
|
||||
31
src/main.ts
31
src/main.ts
@ -1,7 +1,5 @@
|
||||
import "./assets/styles/main.css";
|
||||
|
||||
import Aura from "@primeuix/themes/aura";
|
||||
import { definePreset } from "@primeuix/themes";
|
||||
import PrimeVue from "primevue/config";
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
import ToastService from 'primevue/toastservice';
|
||||
@ -12,25 +10,7 @@ import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { useAuth } from "./modules/auth/composables/useAuth";
|
||||
|
||||
// Crear un preset personalizado basado en Aura con color azul
|
||||
const MyPreset = definePreset(Aura, {
|
||||
semantic: {
|
||||
primary: {
|
||||
50: "{blue.50}",
|
||||
100: "{blue.100}",
|
||||
200: "{blue.200}",
|
||||
300: "{blue.300}",
|
||||
400: "{blue.400}",
|
||||
500: "{blue.500}",
|
||||
600: "{blue.600}",
|
||||
700: "{blue.700}",
|
||||
800: "{blue.800}",
|
||||
900: "{blue.900}",
|
||||
950: "{blue.950}"
|
||||
}
|
||||
}
|
||||
});
|
||||
import { primeVueConfig } from './config/primevue'
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
@ -39,14 +19,7 @@ app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(ConfirmationService);
|
||||
app.use(ToastService);
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: MyPreset,
|
||||
options: {
|
||||
darkModeSelector: ".p-dark",
|
||||
},
|
||||
},
|
||||
});
|
||||
app.use(PrimeVue, primeVueConfig );
|
||||
|
||||
app.directive("styleclass", StyleClass);
|
||||
app.directive("tooltip", Tooltip);
|
||||
|
||||
@ -107,6 +107,10 @@ const goToEdit = (asset: Asset) => {
|
||||
router.push(`/fixed-assets/${asset.id}/edit`);
|
||||
};
|
||||
|
||||
const goToMaintenance = (asset: Asset) => {
|
||||
router.push(`/fixed-assets/${asset.id}/maintenances`);
|
||||
};
|
||||
|
||||
const handleDelete = (asset: Asset) => {
|
||||
confirm.require({
|
||||
message: `¿Estás seguro de eliminar el activo ${asset.sku}?`,
|
||||
@ -255,8 +259,9 @@ const handleDelete = (asset: Asset) => {
|
||||
</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" @click="goToEdit(asset)" />
|
||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger" @click="handleDelete(asset)" />
|
||||
<Button icon="pi pi-pencil" text rounded size="small" v-tooltip.top="'Editar'" @click="goToEdit(asset)" />
|
||||
<Button icon="pi pi-wrench" text rounded size="small" severity="info" v-tooltip.top="'Mantenimientos'" @click="goToMaintenance(asset)" />
|
||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger" v-tooltip.top="'Eliminar'" @click="handleDelete(asset)" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -0,0 +1,464 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import Toast from 'primevue/toast';
|
||||
import Button from 'primevue/button';
|
||||
import Card from 'primevue/card';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import Select from 'primevue/select';
|
||||
import DatePicker from 'primevue/datepicker';
|
||||
import { fixedAssetsService } from '../../services/fixedAssetsService';
|
||||
import type { MaintenanceFormData, MaintenanceEvidence } from '../../types/assetMaintenance';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
|
||||
const assetId = computed(() => Number(route.params.id));
|
||||
const maintenanceId = computed(() => {
|
||||
const v = route.params.maintenanceId;
|
||||
return v ? Number(v) : null;
|
||||
});
|
||||
const isEditing = computed(() => !!maintenanceId.value);
|
||||
|
||||
const loading = ref(false);
|
||||
const loadingData = ref(false);
|
||||
const assetLabel = ref('');
|
||||
|
||||
const evidence = ref<MaintenanceEvidence[]>([]);
|
||||
const uploadingEvidence = ref(false);
|
||||
const deletingEvidenceId = ref<number | null>(null);
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const form = ref<MaintenanceFormData>({
|
||||
assetId: null,
|
||||
type: null,
|
||||
status: 1,
|
||||
description: '',
|
||||
maintenanceDate: '',
|
||||
performedDate: '',
|
||||
cost: '',
|
||||
performedBy: '',
|
||||
odometerReading: '',
|
||||
nextMaintenanceDate: '',
|
||||
nextMaintenanceKm: '',
|
||||
serviceOrderNumber: '',
|
||||
});
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'Preventivo', value: 1 },
|
||||
{ label: 'Correctivo', value: 2 },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: 'Pendiente', value: 1 },
|
||||
{ label: 'En progreso', value: 2 },
|
||||
];
|
||||
|
||||
const toDateString = (d: Date | null | undefined): string => {
|
||||
if (!d) return '';
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
};
|
||||
|
||||
const toDateObj = (s: string): Date | null => {
|
||||
if (!s) return null;
|
||||
const datePart = (s.includes('T') ? s.split('T')[0] : s) ?? '';
|
||||
const [y = NaN, m = NaN, d = NaN] = datePart.split('-').map(Number);
|
||||
if (Number.isNaN(y) || Number.isNaN(m) || Number.isNaN(d)) return null;
|
||||
return new Date(y, m - 1, d);
|
||||
};
|
||||
|
||||
const maintenanceDateModel = computed({
|
||||
get: () => toDateObj(form.value.maintenanceDate),
|
||||
set: (v: Date | null) => { form.value.maintenanceDate = v ? toDateString(v) : ''; },
|
||||
});
|
||||
|
||||
const performedDateModel = computed({
|
||||
get: () => toDateObj(form.value.performedDate),
|
||||
set: (v: Date | null) => { form.value.performedDate = v ? toDateString(v) : ''; },
|
||||
});
|
||||
|
||||
const nextMaintenanceDateModel = computed({
|
||||
get: () => toDateObj(form.value.nextMaintenanceDate),
|
||||
set: (v: Date | null) => { form.value.nextMaintenanceDate = v ? toDateString(v) : ''; },
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
loadingData.value = true;
|
||||
try {
|
||||
const assetRes = await fixedAssetsService.getAsset(assetId.value);
|
||||
const asset = (assetRes as any).data?.data;
|
||||
assetLabel.value = asset?.inventory_warehouse?.product?.name ?? asset?.sku ?? '';
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar los datos.', life: 4000 });
|
||||
} finally {
|
||||
loadingData.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMaintenance = async () => {
|
||||
if (!maintenanceId.value) return;
|
||||
loadingData.value = true;
|
||||
try {
|
||||
const res = await fixedAssetsService.getMaintenance(assetId.value, maintenanceId.value);
|
||||
const m = res.data.data;
|
||||
form.value = {
|
||||
assetId: m.asset_id,
|
||||
type: m.type.id,
|
||||
status: m.status.id,
|
||||
description: m.description ?? '',
|
||||
maintenanceDate: m.maintenance_date ?? '',
|
||||
performedDate: m.performed_date ?? '',
|
||||
cost: m.cost ?? '',
|
||||
performedBy: m.performed_by ?? '',
|
||||
odometerReading: m.odometer_reading ?? '',
|
||||
nextMaintenanceDate: m.next_maintenance_date ?? '',
|
||||
nextMaintenanceKm: m.next_maintenance_km ?? '',
|
||||
serviceOrderNumber: m.service_order_number ?? '',
|
||||
};
|
||||
evidence.value = m.evidence ?? [];
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo cargar el mantenimiento.', life: 4000 });
|
||||
} finally {
|
||||
loadingData.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const validate = (): boolean => {
|
||||
if (!form.value.type) {
|
||||
toast.add({ severity: 'warn', summary: 'Campo requerido', detail: 'Seleccione el tipo de mantenimiento.', life: 3000 });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const p: Record<string, unknown> = {
|
||||
type: form.value.type,
|
||||
status: form.value.status,
|
||||
};
|
||||
if (form.value.description.trim()) p.description = form.value.description.trim();
|
||||
if (form.value.maintenanceDate) p.maintenance_date = form.value.maintenanceDate;
|
||||
if (form.value.performedDate) p.performed_date = form.value.performedDate;
|
||||
if (form.value.cost !== '') p.cost = form.value.cost;
|
||||
if (form.value.performedBy) p.performed_by = form.value.performedBy;
|
||||
if (form.value.odometerReading !== '') p.odometer_reading = form.value.odometerReading;
|
||||
if (form.value.nextMaintenanceDate) p.next_maintenance_date = form.value.nextMaintenanceDate;
|
||||
if (form.value.nextMaintenanceKm !== '') p.next_maintenance_km = form.value.nextMaintenanceKm;
|
||||
if (form.value.serviceOrderNumber.trim()) p.service_order_number = form.value.serviceOrderNumber.trim();
|
||||
return p;
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (!validate()) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await fixedAssetsService.updateMaintenance(assetId.value, maintenanceId.value!, buildPayload());
|
||||
toast.add({ severity: 'success', summary: 'Actualizado', detail: 'Mantenimiento actualizado correctamente.', life: 2500 });
|
||||
} else {
|
||||
await fixedAssetsService.createMaintenance(assetId.value, buildPayload());
|
||||
toast.add({ severity: 'success', summary: 'Registrado', detail: 'Mantenimiento registrado correctamente.', life: 2500 });
|
||||
}
|
||||
router.push(`/fixed-assets/${assetId.value}/maintenances`);
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message ?? 'Error al guardar el mantenimiento.';
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: message, life: 4000 });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => router.push(`/fixed-assets/${assetId.value}/maintenances`);
|
||||
|
||||
const evidenceUrl = (e: MaintenanceEvidence) =>
|
||||
`${import.meta.env.VITE_API_URL}/storage/${e.file_path}`;
|
||||
|
||||
const isImage = (e: MaintenanceEvidence) =>
|
||||
['image/jpeg', 'image/png', 'image/webp'].includes(e.file_type);
|
||||
|
||||
const handleEvidenceUpload = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file || !maintenanceId.value) return;
|
||||
uploadingEvidence.value = true;
|
||||
try {
|
||||
const res = await fixedAssetsService.uploadEvidence(assetId.value, maintenanceId.value, file);
|
||||
evidence.value.push(res.data.data);
|
||||
toast.add({ severity: 'success', summary: 'Evidencia subida', life: 2500 });
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo subir la evidencia.', life: 4000 });
|
||||
} finally {
|
||||
uploadingEvidence.value = false;
|
||||
if (fileInputRef.value) fileInputRef.value.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const deleteEvidence = async (e: MaintenanceEvidence) => {
|
||||
if (!maintenanceId.value) return;
|
||||
deletingEvidenceId.value = e.id;
|
||||
try {
|
||||
await fixedAssetsService.deleteEvidence(assetId.value, maintenanceId.value, e.id);
|
||||
evidence.value = evidence.value.filter(ev => ev.id !== e.id);
|
||||
toast.add({ severity: 'info', summary: 'Eliminado', detail: 'Evidencia eliminada.', life: 2500 });
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo eliminar la evidencia.', life: 4000 });
|
||||
} finally {
|
||||
deletingEvidenceId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadData();
|
||||
if (isEditing.value) await loadMaintenance();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<Toast position="bottom-right" />
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button icon="pi pi-arrow-left" text rounded severity="secondary" @click="cancel" />
|
||||
<h1 class="text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0">
|
||||
{{ isEditing ? 'Editar Mantenimiento' : 'Registrar Mantenimiento' }}
|
||||
</h1>
|
||||
</div>
|
||||
<p class="mt-1 text-surface-500 dark:text-surface-400">
|
||||
{{ assetLabel ? `Activo: ${assetLabel}` : 'Complete los campos para registrar el mantenimiento.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Información general -->
|
||||
<Card class="shadow-sm">
|
||||
<template #title>Información general</template>
|
||||
<template #content>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
|
||||
<!-- Tipo -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
Tipo <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
v-model="form.type"
|
||||
:options="typeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Seleccione el tipo..."
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Estatus -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
Estatus <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
v-model="form.status"
|
||||
:options="statusOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Descripción -->
|
||||
<div class="flex flex-col gap-1 md:col-span-2">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
Descripción
|
||||
</label>
|
||||
<Textarea
|
||||
v-model="form.description"
|
||||
:maxlength="2000"
|
||||
rows="3"
|
||||
placeholder="Describa el trabajo de mantenimiento..."
|
||||
class="w-full"
|
||||
autoResize
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Fecha programada -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
Fecha programada
|
||||
</label>
|
||||
<DatePicker
|
||||
v-model="maintenanceDateModel"
|
||||
dateFormat="dd/mm/yy"
|
||||
placeholder="DD/MM/AA"
|
||||
showIcon
|
||||
class="w-80"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Fecha realizado -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
Fecha realizado
|
||||
</label>
|
||||
<DatePicker
|
||||
v-model="performedDateModel"
|
||||
dateFormat="dd/mm/yy"
|
||||
placeholder="DD/MM/AA"
|
||||
showIcon
|
||||
class="w-80"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Costo -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
Costo (MXN)
|
||||
</label>
|
||||
<InputText
|
||||
v-model="form.cost"
|
||||
placeholder="0.00"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Realizado por -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
Realizado por
|
||||
</label>
|
||||
<InputText
|
||||
v-model="form.performedBy"
|
||||
placeholder="Nombre del técnico o empresa..."
|
||||
:maxlength="255"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Orden de servicio -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
No. orden de servicio
|
||||
</label>
|
||||
<InputText
|
||||
v-model="form.serviceOrderNumber"
|
||||
placeholder="Ej. OS-2026-001"
|
||||
:maxlength="100"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<!-- Campos de vehículo -->
|
||||
<Card class="shadow-sm">
|
||||
<template #title>Datos de vehículo (opcional)</template>
|
||||
<template #subtitle>Complete estos campos si el activo es un vehículo.</template>
|
||||
<template #content>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
|
||||
<!-- Lectura odómetro -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
Lectura odómetro (km)
|
||||
</label>
|
||||
<InputText
|
||||
v-model="form.odometerReading"
|
||||
placeholder="Ej. 45,000.00"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Próximo mantenimiento km -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
Próximo mantenimiento (km)
|
||||
</label>
|
||||
<InputText
|
||||
v-model="form.nextMaintenanceKm"
|
||||
placeholder="Ej. 50,000.00"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Próxima fecha de mantenimiento -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium text-surface-700 dark:text-surface-300">
|
||||
Próxima fecha de mantenimiento
|
||||
</label>
|
||||
<DatePicker
|
||||
v-model="nextMaintenanceDateModel"
|
||||
dateFormat="dd/mm/yy"
|
||||
placeholder="DD/MM/AA"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<!-- Evidencias (solo en edición) -->
|
||||
<Card v-if="isEditing" class="shadow-sm">
|
||||
<template #title>Evidencias</template>
|
||||
<template #subtitle>Fotos y documentos del mantenimiento (jpg, png, webp, pdf — máx. 10 MB)</template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div v-if="evidence.length > 0" class="flex flex-wrap gap-3">
|
||||
<div
|
||||
v-for="ev in evidence"
|
||||
:key="ev.id"
|
||||
class="relative group w-28 h-28 rounded-xl border border-surface-200 dark:border-surface-700 overflow-hidden flex items-center justify-center bg-surface-50 dark:bg-surface-800"
|
||||
>
|
||||
<a v-if="isImage(ev)" :href="evidenceUrl(ev)" target="_blank" class="w-full h-full">
|
||||
<img :src="evidenceUrl(ev)" class="w-full h-full object-cover" />
|
||||
</a>
|
||||
<a v-else :href="evidenceUrl(ev)" target="_blank" class="flex flex-col items-center gap-1 p-2 text-center text-xs text-surface-500">
|
||||
<i class="pi pi-file-pdf text-3xl text-red-500" />
|
||||
<span class="truncate w-full">PDF</span>
|
||||
</a>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
size="small"
|
||||
severity="danger"
|
||||
rounded
|
||||
class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
:loading="deletingEvidenceId === ev.id"
|
||||
@click.prevent="deleteEvidence(ev)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-sm text-surface-400">No hay evidencias adjuntas.</p>
|
||||
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.webp,.pdf"
|
||||
class="hidden"
|
||||
@change="handleEvidenceUpload"
|
||||
/>
|
||||
<Button
|
||||
label="Subir archivo"
|
||||
icon="pi pi-upload"
|
||||
outlined
|
||||
:loading="uploadingEvidence"
|
||||
@click="fileInputRef?.click()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||
<Button label="Cancelar" text severity="secondary" @click="cancel" />
|
||||
<Button
|
||||
:label="isEditing ? 'Guardar cambios' : 'Registrar mantenimiento'"
|
||||
icon="pi pi-check"
|
||||
:loading="loading"
|
||||
:disabled="loadingData"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@ -0,0 +1,325 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import Toast from 'primevue/toast';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import Button from 'primevue/button';
|
||||
import Card from 'primevue/card';
|
||||
import Select from 'primevue/select';
|
||||
import Paginator from 'primevue/paginator';
|
||||
import Tag from 'primevue/tag';
|
||||
import { fixedAssetsService } from '../../services/fixedAssetsService';
|
||||
import type { AssetMaintenance } from '../../types/assetMaintenance';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const assetId = computed(() => Number(route.params.id));
|
||||
|
||||
const maintenances = ref<AssetMaintenance[]>([]);
|
||||
const assetName = ref('');
|
||||
const assetSku = ref('');
|
||||
const selectedType = ref<number | 'all'>('all');
|
||||
const selectedStatus = ref<number | 'all'>('all');
|
||||
const loading = ref(false);
|
||||
const currentPage = ref(1);
|
||||
const totalRecords = ref(0);
|
||||
const rows = ref(15);
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'Todos', value: 'all' },
|
||||
{ label: 'Preventivo', value: 1 },
|
||||
{ label: 'Correctivo', value: 2 },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: 'Todos', value: 'all' },
|
||||
{ label: 'Pendiente', value: 1 },
|
||||
{ label: 'En progreso', value: 2 },
|
||||
{ label: 'Completado', value: 3 },
|
||||
{ label: 'Cancelado', value: 4 },
|
||||
];
|
||||
|
||||
const statusSeverity = (statusId: number) => {
|
||||
if (statusId === 1) return 'warn';
|
||||
if (statusId === 2) return 'info';
|
||||
if (statusId === 3) return 'success';
|
||||
return 'secondary';
|
||||
};
|
||||
|
||||
const typeSeverity = (typeId: number) => {
|
||||
return typeId === 1 ? 'success' : 'danger';
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return '—';
|
||||
return new Date(dateStr).toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
};
|
||||
|
||||
const formatCost = (cost: string | null) => {
|
||||
if (!cost) return '—';
|
||||
return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(Number(cost));
|
||||
};
|
||||
|
||||
const performedByName = (m: AssetMaintenance) => m.performed_by || '—';
|
||||
|
||||
const loadAssetInfo = async () => {
|
||||
try {
|
||||
const res = await fixedAssetsService.getAsset(assetId.value);
|
||||
const asset = (res as any).data?.data;
|
||||
assetName.value = asset?.inventory_warehouse?.product?.name ?? asset?.sku ?? '';
|
||||
assetSku.value = asset?.sku ?? '';
|
||||
} catch {
|
||||
// silently fail, header will show fallback
|
||||
}
|
||||
};
|
||||
|
||||
const loadMaintenances = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await fixedAssetsService.getMaintenances(assetId.value, {
|
||||
type: selectedType.value !== 'all' ? selectedType.value : undefined,
|
||||
status: selectedStatus.value !== 'all' ? selectedStatus.value : undefined,
|
||||
page: currentPage.value,
|
||||
});
|
||||
maintenances.value = res.data.data.data;
|
||||
totalRecords.value = res.data.data.total;
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar los mantenimientos.', life: 4000 });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onFilter = () => {
|
||||
currentPage.value = 1;
|
||||
loadMaintenances();
|
||||
};
|
||||
|
||||
const onPage = (event: { page: number }) => {
|
||||
currentPage.value = event.page + 1;
|
||||
loadMaintenances();
|
||||
};
|
||||
|
||||
const goBack = () => router.push('/fixed-assets');
|
||||
|
||||
const goToCreate = () => {
|
||||
router.push(`/fixed-assets/${assetId.value}/maintenances/create`);
|
||||
};
|
||||
|
||||
const goToEdit = (m: AssetMaintenance) => {
|
||||
router.push(`/fixed-assets/${assetId.value}/maintenances/${m.id}/edit`);
|
||||
};
|
||||
|
||||
const completing = ref<number | null>(null);
|
||||
const cancelling = ref<number | null>(null);
|
||||
|
||||
const confirmComplete = (m: AssetMaintenance) => {
|
||||
confirm.require({
|
||||
message: '¿Desea marcar este mantenimiento como completado? El activo volverá a estado Activo.',
|
||||
header: 'Completar mantenimiento',
|
||||
icon: 'pi pi-check-circle',
|
||||
acceptLabel: 'Completar',
|
||||
rejectLabel: 'Cancelar',
|
||||
accept: () => doComplete(m),
|
||||
});
|
||||
};
|
||||
|
||||
const doComplete = async (m: AssetMaintenance) => {
|
||||
completing.value = m.id;
|
||||
try {
|
||||
await fixedAssetsService.completeMaintenance(assetId.value, m.id);
|
||||
toast.add({ severity: 'success', summary: 'Completado', detail: 'Mantenimiento marcado como completado.', life: 3000 });
|
||||
loadMaintenances();
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.message ?? 'Error al completar el mantenimiento.';
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: msg, life: 4000 });
|
||||
} finally {
|
||||
completing.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmCancel = (m: AssetMaintenance) => {
|
||||
confirm.require({
|
||||
message: '¿Desea cancelar este mantenimiento?',
|
||||
header: 'Cancelar mantenimiento',
|
||||
icon: 'pi pi-times-circle',
|
||||
acceptLabel: 'Cancelar mantenimiento',
|
||||
rejectLabel: 'Volver',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: () => doCancel(m),
|
||||
});
|
||||
};
|
||||
|
||||
const doCancel = async (m: AssetMaintenance) => {
|
||||
cancelling.value = m.id;
|
||||
try {
|
||||
await fixedAssetsService.cancelMaintenance(assetId.value, m.id);
|
||||
toast.add({ severity: 'info', summary: 'Cancelado', detail: 'Mantenimiento cancelado.', life: 3000 });
|
||||
loadMaintenances();
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.message ?? 'Error al cancelar el mantenimiento.';
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: msg, life: 4000 });
|
||||
} finally {
|
||||
cancelling.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const canEdit = (m: AssetMaintenance) => m.status.id !== 3 && m.status.id !== 4;
|
||||
const canComplete = (m: AssetMaintenance) => m.status.id !== 3 && m.status.id !== 4;
|
||||
const canCancel = (m: AssetMaintenance) => m.status.id !== 3 && m.status.id !== 4;
|
||||
|
||||
onMounted(() => {
|
||||
loadAssetInfo();
|
||||
loadMaintenances();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<Toast position="bottom-right" />
|
||||
<ConfirmDialog />
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button icon="pi pi-arrow-left" text rounded severity="secondary" @click="goBack" />
|
||||
<h1 class="text-3xl font-black tracking-tight text-surface-900 dark:text-surface-0">
|
||||
Mantenimientos
|
||||
</h1>
|
||||
</div>
|
||||
<p class="mt-1 text-surface-500 dark:text-surface-400">
|
||||
{{ assetName ? `${assetName} — ${assetSku}` : 'Historial de mantenimiento del activo' }}
|
||||
</p>
|
||||
</div>
|
||||
<Button label="Nuevo Mantenimiento" icon="pi pi-plus" @click="goToCreate" />
|
||||
</div>
|
||||
|
||||
<Card class="shadow-sm">
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div class="flex w-full flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-surface-600 dark:text-surface-300">Tipo:</span>
|
||||
<Select
|
||||
v-model="selectedType"
|
||||
:options="typeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-36"
|
||||
@change="onFilter"
|
||||
/>
|
||||
<span class="text-sm font-medium text-surface-600 dark:text-surface-300">Estatus:</span>
|
||||
<Select
|
||||
v-model="selectedStatus"
|
||||
:options="statusOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-40"
|
||||
@change="onFilter"
|
||||
/>
|
||||
</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">Tipo</th>
|
||||
<th class="px-4 py-3">Estatus</th>
|
||||
<th class="px-4 py-3">Descripción</th>
|
||||
<th class="px-4 py-3">Fecha Programada</th>
|
||||
<th class="px-4 py-3">Fecha Realizado</th>
|
||||
<th class="px-4 py-3">Costo</th>
|
||||
<th class="px-4 py-3">Realizado por</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-surface-500">Cargando...</td>
|
||||
</tr>
|
||||
<template v-else>
|
||||
<tr
|
||||
v-for="m in maintenances"
|
||||
:key="m.id"
|
||||
class="border-t border-surface-200 text-sm dark:border-surface-700"
|
||||
>
|
||||
<td class="px-4 py-3">
|
||||
<Tag :value="m.type.name" :severity="typeSeverity(m.type.id)" />
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<Tag :value="m.status.name" :severity="statusSeverity(m.status.id)" />
|
||||
</td>
|
||||
<td class="px-4 py-3 max-w-xs truncate">
|
||||
{{ m.description || '—' }}
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ formatDate(m.maintenance_date) }}</td>
|
||||
<td class="px-4 py-3">{{ formatDate(m.performed_date) }}</td>
|
||||
<td class="px-4 py-3">{{ formatCost(m.cost) }}</td>
|
||||
<td class="px-4 py-3">{{ performedByName(m) }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
v-if="canEdit(m)"
|
||||
icon="pi pi-pencil"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
v-tooltip.top="'Editar'"
|
||||
@click="goToEdit(m)"
|
||||
/>
|
||||
<Button
|
||||
v-if="canComplete(m)"
|
||||
icon="pi pi-check-circle"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="success"
|
||||
v-tooltip.top="'Marcar como completado'"
|
||||
:loading="completing === m.id"
|
||||
@click="confirmComplete(m)"
|
||||
/>
|
||||
<Button
|
||||
v-if="canCancel(m)"
|
||||
icon="pi pi-times-circle"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="danger"
|
||||
v-tooltip.top="'Cancelar'"
|
||||
:loading="cancelling === m.id"
|
||||
@click="confirmCancel(m)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="maintenances.length === 0">
|
||||
<td colspan="8" class="px-4 py-8 text-center text-surface-500 dark:text-surface-400">
|
||||
No hay mantenimientos registrados para este activo.
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</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">
|
||||
{{ totalRecords }} mantenimiento(s) en total
|
||||
</p>
|
||||
<Paginator
|
||||
:rows="rows"
|
||||
:totalRecords="totalRecords"
|
||||
template="PrevPageLink PageLinks NextPageLink"
|
||||
@page="onPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</section>
|
||||
</template>
|
||||
@ -39,7 +39,7 @@ const formatDate = (dateStr: string | null) => {
|
||||
};
|
||||
|
||||
const assigneeName = (assignment: AssetAssignment) => {
|
||||
if (assignment.assignee_type?.id === 2) {
|
||||
if (assignment.assignee_type === 2) {
|
||||
return [assignment.external_name, assignment.external_paternal, assignment.external_maternal]
|
||||
.filter(Boolean).join(' ') || '—';
|
||||
}
|
||||
@ -49,7 +49,7 @@ const assigneeName = (assignment: AssetAssignment) => {
|
||||
};
|
||||
|
||||
const assigneeSubtitle = (assignment: AssetAssignment) => {
|
||||
if (assignment.assignee_type?.id === 2) return assignment.external_company ?? 'Externo';
|
||||
if (assignment.assignee_type === 2) return assignment.external_company ?? 'Externo';
|
||||
return assignment.employee?.department?.name ?? '—';
|
||||
};
|
||||
|
||||
|
||||
@ -19,9 +19,9 @@ defineProps<Props>();
|
||||
const reasonOptions = [
|
||||
{ label: 'Seleccione un motivo', value: '' },
|
||||
{ label: 'Cambio de colaborador', value: 'Cambio de colaborador' },
|
||||
{ label: 'Baja por danio', value: 'Baja por danio' },
|
||||
{ label: 'Baja por daño', value: 'Baja por daño' },
|
||||
{ label: 'Robo o extravio', value: 'Robo o extravio' },
|
||||
{ label: 'Fin de proyecto', value: 'Fin de proyecto' }
|
||||
{ label: 'Retorno de externo', value: 'Retorno de externo' }
|
||||
];
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import api from '../../../services/api';
|
||||
import type { UsefulLifeCatalogOption, FiscalDepreciationRateOption, UsefulLifeCalculationResult } from '../types/fixedAsset';
|
||||
import type {
|
||||
AssetMaintenance,
|
||||
AssetMaintenancePaginatedResponse,
|
||||
AssetMaintenanceResponse,
|
||||
MaintenanceEvidence,
|
||||
} from '../types/assetMaintenance';
|
||||
|
||||
export type { AssetMaintenance, AssetMaintenancePaginatedResponse, AssetMaintenanceResponse, MaintenanceEvidence };
|
||||
|
||||
export interface Asset {
|
||||
id: number;
|
||||
@ -87,7 +95,7 @@ export interface AssetAssignment {
|
||||
id: number;
|
||||
asset_id: number;
|
||||
employee_id: number | null;
|
||||
assignee_type: { id: number; name: string };
|
||||
assignee_type: number;
|
||||
external_name: string | null;
|
||||
external_paternal: string | null;
|
||||
external_maternal: string | null;
|
||||
@ -253,6 +261,77 @@ class FixedAssetsService {
|
||||
link.click();
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
|
||||
async getMaintenances(
|
||||
assetId: number,
|
||||
filters: { type?: number; status?: number; page?: number; paginate?: boolean } = {}
|
||||
): Promise<AssetMaintenancePaginatedResponse> {
|
||||
const params: Record<string, string | number | boolean> = {};
|
||||
if (filters.type !== undefined) params.type = filters.type;
|
||||
if (filters.status !== undefined) params.status = filters.status;
|
||||
if (filters.page) params.page = filters.page;
|
||||
if (filters.paginate !== undefined) params.paginate = filters.paginate;
|
||||
|
||||
const response = await api.get<AssetMaintenancePaginatedResponse>(
|
||||
`/api/assets/${assetId}/maintenances`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createMaintenance(assetId: number, data: Record<string, unknown>): Promise<AssetMaintenanceResponse> {
|
||||
const response = await api.post<AssetMaintenanceResponse>(
|
||||
`/api/assets/${assetId}/maintenances`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getMaintenance(assetId: number, maintenanceId: number): Promise<AssetMaintenanceResponse> {
|
||||
const response = await api.get<AssetMaintenanceResponse>(
|
||||
`/api/assets/${assetId}/maintenances/${maintenanceId}`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateMaintenance(assetId: number, maintenanceId: number, data: Record<string, unknown>): Promise<AssetMaintenanceResponse> {
|
||||
const response = await api.put<AssetMaintenanceResponse>(
|
||||
`/api/assets/${assetId}/maintenances/${maintenanceId}`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async completeMaintenance(assetId: number, maintenanceId: number, data: Record<string, unknown> = {}): Promise<AssetMaintenanceResponse> {
|
||||
const response = await api.put<AssetMaintenanceResponse>(
|
||||
`/api/assets/${assetId}/maintenances/${maintenanceId}/complete`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async cancelMaintenance(assetId: number, maintenanceId: number): Promise<AssetMaintenanceResponse> {
|
||||
const response = await api.put<AssetMaintenanceResponse>(
|
||||
`/api/assets/${assetId}/maintenances/${maintenanceId}/cancel`,
|
||||
{}
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async uploadEvidence(assetId: number, maintenanceId: number, file: File): Promise<{ status: string; data: { data: MaintenanceEvidence } }> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const response = await api.post(
|
||||
`/api/assets/${assetId}/maintenances/${maintenanceId}/evidence`,
|
||||
formData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteEvidence(assetId: number, maintenanceId: number, evidenceId: number): Promise<void> {
|
||||
await api.delete(`/api/assets/${assetId}/maintenances/${maintenanceId}/evidence/${evidenceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const fixedAssetsService = new FixedAssetsService();
|
||||
|
||||
68
src/modules/fixed-assets/types/assetMaintenance.ts
Normal file
68
src/modules/fixed-assets/types/assetMaintenance.ts
Normal file
@ -0,0 +1,68 @@
|
||||
export interface AssetMaintenance {
|
||||
id: number;
|
||||
asset_id: number;
|
||||
type: { id: number; name: string };
|
||||
status: { id: number; name: string };
|
||||
description: string | null;
|
||||
maintenance_date: string | null;
|
||||
performed_date: string | null;
|
||||
cost: string | null;
|
||||
performed_by: string | null;
|
||||
supplier_id: number | null;
|
||||
odometer_reading: string | null;
|
||||
next_maintenance_date: string | null;
|
||||
next_maintenance_km: string | null;
|
||||
service_order_number: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
supplier: { id: number; name: string } | null;
|
||||
evidence: MaintenanceEvidence[];
|
||||
asset?: {
|
||||
id: number;
|
||||
sku: string;
|
||||
inventory_warehouse?: {
|
||||
product?: { name: string };
|
||||
} | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MaintenanceEvidence {
|
||||
id: number;
|
||||
maintenance_id: number;
|
||||
file_path: string;
|
||||
file_type: string;
|
||||
uploaded_at: string;
|
||||
}
|
||||
|
||||
export interface AssetMaintenancePaginatedResponse {
|
||||
status: string;
|
||||
data: {
|
||||
data: {
|
||||
current_page: number;
|
||||
data: AssetMaintenance[];
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface AssetMaintenanceResponse {
|
||||
status: string;
|
||||
data: { data: AssetMaintenance };
|
||||
}
|
||||
|
||||
export interface MaintenanceFormData {
|
||||
assetId: number | null;
|
||||
type: number | null;
|
||||
status: number;
|
||||
description: string;
|
||||
maintenanceDate: string;
|
||||
performedDate: string;
|
||||
cost: string;
|
||||
performedBy: string;
|
||||
odometerReading: string;
|
||||
nextMaintenanceDate: string;
|
||||
nextMaintenanceKm: string;
|
||||
serviceOrderNumber: string;
|
||||
}
|
||||
@ -17,6 +17,8 @@ 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 MaintenanceIndex from '../modules/fixed-assets/components/Maintenance/MaintenanceIndex.vue';
|
||||
import MaintenanceForm from '../modules/fixed-assets/components/Maintenance/MaintenanceForm.vue';
|
||||
|
||||
import RolesIndex from '../modules/users/components/RoleIndex.vue';
|
||||
import RoleForm from '../modules/users/components/RoleForm.vue';
|
||||
@ -340,6 +342,43 @@ const routes: RouteRecordRaw[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: ':id/maintenances',
|
||||
name: 'FixedAssetMaintenancesModule',
|
||||
meta: {
|
||||
title: 'Mantenimientos de Activos',
|
||||
requiresAuth: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'FixedAssetMaintenances',
|
||||
component: MaintenanceIndex,
|
||||
meta: {
|
||||
title: 'Listado de Mantenimientos',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
name: 'FixedAssetMaintenanceCreate',
|
||||
component: MaintenanceForm,
|
||||
meta: {
|
||||
title: 'Registrar Mantenimiento',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':maintenanceId/edit',
|
||||
name: 'FixedAssetMaintenanceEdit',
|
||||
component: MaintenanceForm,
|
||||
meta: {
|
||||
title: 'Editar Mantenimiento',
|
||||
requiresAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user