From ab04091306a53d534faf7affc3727739233789b2 Mon Sep 17 00:00:00 2001 From: raul310882 Date: Sun, 5 Apr 2026 19:27:30 -0600 Subject: [PATCH] feat: implement catalog modules for routes, risk factors, and route segments with supporting services and UI components. --- src/components/layout/Sidebar.vue | 36 + .../components/locations/LocationForm.vue | 76 +- .../risk-factors/RiskFactorModal.vue | 283 ++++++++ .../components/risk-factors/RiskFactors.vue | 222 ++++++ .../route-segments/RouteSegmentForm.vue | 672 ++++++++++++++++++ .../RouteSegmentRiskAssessment.vue | 221 ++++++ .../route-segments/RouteSegments.vue | 410 +++++++++++ .../catalog/components/routes/RouteForm.vue | 404 +++++++++++ .../catalog/components/routes/Routes.vue | 343 +++++++++ .../catalog/services/location.services.ts | 2 +- .../catalog/services/risk-factors.services.ts | 69 ++ .../catalog/services/route.services.ts | 94 +++ .../catalog/services/routeSegment.services.ts | 127 ++++ .../catalog/types/risk-factors.interfaces.ts | 45 ++ .../catalog/types/routeSegments.interfaces.ts | 150 ++++ .../catalog/types/routes.interfaces.ts | 77 ++ src/router/index.ts | 30 + 17 files changed, 3254 insertions(+), 7 deletions(-) create mode 100644 src/modules/catalog/components/risk-factors/RiskFactorModal.vue create mode 100644 src/modules/catalog/components/risk-factors/RiskFactors.vue create mode 100644 src/modules/catalog/components/route-segments/RouteSegmentForm.vue create mode 100644 src/modules/catalog/components/route-segments/RouteSegmentRiskAssessment.vue create mode 100644 src/modules/catalog/components/route-segments/RouteSegments.vue create mode 100644 src/modules/catalog/components/routes/RouteForm.vue create mode 100644 src/modules/catalog/components/routes/Routes.vue create mode 100644 src/modules/catalog/services/risk-factors.services.ts create mode 100644 src/modules/catalog/services/route.services.ts create mode 100644 src/modules/catalog/services/routeSegment.services.ts create mode 100644 src/modules/catalog/types/risk-factors.interfaces.ts create mode 100644 src/modules/catalog/types/routeSegments.interfaces.ts create mode 100644 src/modules/catalog/types/routes.interfaces.ts diff --git a/src/components/layout/Sidebar.vue b/src/components/layout/Sidebar.vue index d161551..f12371d 100644 --- a/src/components/layout/Sidebar.vue +++ b/src/components/layout/Sidebar.vue @@ -81,6 +81,42 @@ const menuItems = ref([ 'locations.destroy', ], }, + { + label: 'Segmentos de Ruta', + icon: 'pi pi-map', + to: '/catalog/route-segments', + permission: [ + 'route-segments.index', + 'route-segments.show', + 'route-segments.store', + 'route-segments.update', + 'route-segments.destroy', + ], + }, + { + label: 'Catálogo de Rutas', + icon: 'pi pi-sitemap', + to: '/catalog/routes', + permission: [ + 'routes.index', + 'routes.show', + 'routes.store', + 'routes.update', + 'routes.destroy', + ], + }, + { + label: 'Factores de Riesgo', + icon: 'pi pi-exclamation-triangle', + to: '/catalog/risk-factors', + permission: [ + 'risk-factors.index', + 'risk-factors.show', + 'risk-factors.store', + 'risk-factors.update', + 'risk-factors.destroy', + ], + }, ] }, { diff --git a/src/modules/catalog/components/locations/LocationForm.vue b/src/modules/catalog/components/locations/LocationForm.vue index 8362558..df774c4 100644 --- a/src/modules/catalog/components/locations/LocationForm.vue +++ b/src/modules/catalog/components/locations/LocationForm.vue @@ -1,5 +1,5 @@ + + diff --git a/src/modules/catalog/components/risk-factors/RiskFactors.vue b/src/modules/catalog/components/risk-factors/RiskFactors.vue new file mode 100644 index 0000000..59c7b4d --- /dev/null +++ b/src/modules/catalog/components/risk-factors/RiskFactors.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/src/modules/catalog/components/route-segments/RouteSegmentForm.vue b/src/modules/catalog/components/route-segments/RouteSegmentForm.vue new file mode 100644 index 0000000..2384c2d --- /dev/null +++ b/src/modules/catalog/components/route-segments/RouteSegmentForm.vue @@ -0,0 +1,672 @@ + + + + + diff --git a/src/modules/catalog/components/route-segments/RouteSegmentRiskAssessment.vue b/src/modules/catalog/components/route-segments/RouteSegmentRiskAssessment.vue new file mode 100644 index 0000000..d333a1c --- /dev/null +++ b/src/modules/catalog/components/route-segments/RouteSegmentRiskAssessment.vue @@ -0,0 +1,221 @@ + + + diff --git a/src/modules/catalog/components/route-segments/RouteSegments.vue b/src/modules/catalog/components/route-segments/RouteSegments.vue new file mode 100644 index 0000000..9ea1f8a --- /dev/null +++ b/src/modules/catalog/components/route-segments/RouteSegments.vue @@ -0,0 +1,410 @@ + + + diff --git a/src/modules/catalog/components/routes/RouteForm.vue b/src/modules/catalog/components/routes/RouteForm.vue new file mode 100644 index 0000000..045534c --- /dev/null +++ b/src/modules/catalog/components/routes/RouteForm.vue @@ -0,0 +1,404 @@ + + + diff --git a/src/modules/catalog/components/routes/Routes.vue b/src/modules/catalog/components/routes/Routes.vue new file mode 100644 index 0000000..2023ce4 --- /dev/null +++ b/src/modules/catalog/components/routes/Routes.vue @@ -0,0 +1,343 @@ + + + diff --git a/src/modules/catalog/services/location.services.ts b/src/modules/catalog/services/location.services.ts index f39d74c..8b4597e 100644 --- a/src/modules/catalog/services/location.services.ts +++ b/src/modules/catalog/services/location.services.ts @@ -19,7 +19,7 @@ const locationServices = { ): Promise { try { const params: any = {}; - if (paginated === false) params.paginated = false; + if (paginated === false) params.paginate = false; if (name) params.name = name; if (city) params.city = city; if (state) params.state = state; diff --git a/src/modules/catalog/services/risk-factors.services.ts b/src/modules/catalog/services/risk-factors.services.ts new file mode 100644 index 0000000..cd8e172 --- /dev/null +++ b/src/modules/catalog/services/risk-factors.services.ts @@ -0,0 +1,69 @@ +import api from '../../../services/api'; +import type { + RiskFactor, + RiskFactorPaginatedResponse +} from '../types/risk-factors.interfaces'; + +const riskFactorServices = { + async getRiskFactors(params?: any): Promise { + try { + const response = await api.get('/api/risk-factors', { params }); + return response.data; + } catch (error) { + console.error('Error fetching risk factors:', error); + throw error; + } + }, + + async getRiskFactorById(id: number): Promise<{ data: RiskFactor }> { + try { + const response = await api.get(`/api/risk-factors/${id}`); + return response.data; + } catch (error) { + console.error(`Error fetching risk factor ${id}:`, error); + throw error; + } + }, + + async createRiskFactor(data: RiskFactor): Promise<{ data: RiskFactor; message: string }> { + try { + const response = await api.post('/api/risk-factors', data); + return response.data; + } catch (error) { + console.error('Error creating risk factor:', error); + throw error; + } + }, + + async updateRiskFactor(id: number, data: RiskFactor): Promise<{ data: RiskFactor; message: string }> { + try { + const response = await api.put(`/api/risk-factors/${id}`, data); + return response.data; + } catch (error) { + console.error(`Error updating risk factor ${id}:`, error); + throw error; + } + }, + + async duplicateRiskFactor(id: number): Promise<{ data: RiskFactor; message: string }> { + try { + const response = await api.post(`/api/risk-factors/${id}/duplicate`); + return response.data; + } catch (error) { + console.error(`Error duplicating risk factor ${id}:`, error); + throw error; + } + }, + + async deleteRiskFactor(id: number): Promise<{ message: string }> { + try { + const response = await api.delete(`/api/risk-factors/${id}`); + return response.data; + } catch (error) { + console.error(`Error deleting risk factor ${id}:`, error); + throw error; + } + } +}; + +export { riskFactorServices }; diff --git a/src/modules/catalog/services/route.services.ts b/src/modules/catalog/services/route.services.ts new file mode 100644 index 0000000..9153497 --- /dev/null +++ b/src/modules/catalog/services/route.services.ts @@ -0,0 +1,94 @@ +import api from '../../../services/api'; +import type { + RouteCreateRequest, + RouteCreateResponse, + RouteDeleteResponse, + RouteListResponse, + RoutePaginatedResponse, + RouteUpdateRequest, + RouteUpdateResponse, +} from '../types/routes.interfaces'; + +const routeServices = { + /** + * Listar rutas (paginadas o todas) + */ + async getRoutes( + paginated?: boolean, + active?: boolean, + page?: number, + perPage?: number, + ): Promise { + try { + const params: Record = {}; + if (paginated === false) params.paginate = false; + if (active !== undefined) params.active = active; + if (page) params.page = page; + if (perPage) params.per_page = perPage; + + const response = await api.get('/api/routes', { params }); + return response.data; + } catch (error) { + console.error('Error fetching routes:', error); + throw error; + } + }, + + /** + * Obtener una ruta por ID + */ + async getRouteById(routeId: number): Promise { + try { + const response = await api.get(`/api/routes/${routeId}`); + return response.data; + } catch (error) { + console.error(`Error fetching route with ID ${routeId}:`, error); + throw error; + } + }, + + /** + * Crear una nueva ruta + */ + async createRoute(data: RouteCreateRequest): Promise { + try { + const response = await api.post('/api/routes', data); + return response.data; + } catch (error) { + console.error('Error creating route:', error); + throw error; + } + }, + + /** + * Actualizar una ruta existente + */ + async updateRoute( + routeId: number, + data: RouteUpdateRequest, + method: 'patch' | 'put' = 'patch', + ): Promise { + try { + const response = await api[method](`/api/routes/${routeId}`, data); + return response.data; + } catch (error) { + console.error(`Error updating route with ID ${routeId}:`, error); + throw error; + } + }, + + /** + * Eliminar una ruta (soft delete) + */ + async deleteRoute(routeId: number): Promise { + try { + const response = await api.delete(`/api/routes/${routeId}`); + return response.data; + } catch (error) { + console.error(`Error deleting route with ID ${routeId}:`, error); + throw error; + } + }, +}; + +export { routeServices }; diff --git a/src/modules/catalog/services/routeSegment.services.ts b/src/modules/catalog/services/routeSegment.services.ts new file mode 100644 index 0000000..232f17f --- /dev/null +++ b/src/modules/catalog/services/routeSegment.services.ts @@ -0,0 +1,127 @@ +import api from '../../../services/api'; +import type { + RouteSegmentCreateRequest, + RouteSegmentCreateResponse, + RouteSegmentDeleteResponse, + RouteSegmentListResponse, + RouteSegmentPaginatedResponse, + RouteSegmentUpdateRequest, + RouteSegmentUpdateResponse, + UpdateRiskEvaluationsRequest, + RiskFactorsResponse, +} from '../types/routeSegments.interfaces'; + +const routeSegmentServices = { + /** + * Listar segmentos de ruta (paginados o todos) + */ + async getSegments( + paginated?: boolean, + originName?: string, + destinationName?: string, + page?: number, + perPage?: number, + ): Promise { + try { + const params: Record = {}; + if (paginated === false) params.paginate = false; + if (originName) params.origin_name = originName; + if (destinationName) params.destination_name = destinationName; + if (page) params.page = page; + if (perPage) params.per_page = perPage; + + const response = await api.get('/api/route-segments', { params }); + return response.data; + } catch (error) { + console.error('Error fetching route segments:', error); + throw error; + } + }, + + /** + * Obtener un segmento por ID + */ + async getSegmentById(segmentId: number): Promise { + try { + const response = await api.get(`/api/route-segments/${segmentId}`); + return response.data; + } catch (error) { + console.error(`Error fetching route segment with ID ${segmentId}:`, error); + throw error; + } + }, + + /** + * Crear un nuevo segmento + */ + async createSegment(data: RouteSegmentCreateRequest): Promise { + try { + const response = await api.post('/api/route-segments', data); + return response.data; + } catch (error) { + console.error('Error creating route segment:', error); + throw error; + } + }, + + /** + * Actualizar un segmento existente + */ + async updateSegment( + segmentId: number, + data: RouteSegmentUpdateRequest, + method: 'patch' | 'put' = 'patch', + ): Promise { + try { + const response = await api[method](`/api/route-segments/${segmentId}`, data); + return response.data; + } catch (error) { + console.error(`Error updating route segment with ID ${segmentId}:`, error); + throw error; + } + }, + + /** + * Eliminar un segmento (soft delete) + */ + async deleteSegment(segmentId: number): Promise { + try { + const response = await api.delete(`/api/route-segments/${segmentId}`); + return response.data; + } catch (error) { + console.error(`Error deleting route segment with ID ${segmentId}:`, error); + throw error; + } + }, + + /** + * Obtener factores de riesgo para un segmento (con evaluación actual) + */ + async getRiskFactors(segmentId: number): Promise { + try { + const response = await api.get(`/api/route-segments/${segmentId}/risk-factors`); + return response.data; + } catch (error) { + console.error(`Error fetching risk factors for segment ${segmentId}:`, error); + throw error; + } + }, + + /** + * Guardar/actualizar evaluaciones de riesgo de un segmento + */ + async updateRiskEvaluations( + segmentId: number, + data: UpdateRiskEvaluationsRequest, + ): Promise { + try { + const response = await api.post(`/api/route-segments/${segmentId}/risk-evaluations`, data); + return response.data; + } catch (error) { + console.error(`Error updating risk evaluations for segment ${segmentId}:`, error); + throw error; + } + }, +}; + +export { routeSegmentServices }; diff --git a/src/modules/catalog/types/risk-factors.interfaces.ts b/src/modules/catalog/types/risk-factors.interfaces.ts new file mode 100644 index 0000000..56209c7 --- /dev/null +++ b/src/modules/catalog/types/risk-factors.interfaces.ts @@ -0,0 +1,45 @@ +/** + * @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved + */ + +export interface RiskOption { + id?: number; + risk_factor_id?: number; + name: string; + value: number; +} + +export interface RiskFactor { + id?: number; + name: string; + description: string | null; + evaluation_context: number; + tenant_id?: number; + options?: RiskOption[]; + created_at?: string; + updated_at?: string; +} + +export interface RiskFactorFormErrors { + name?: string[]; + description?: string[]; + evaluation_context?: string[]; + options?: string[]; + [key: string]: string[] | undefined; +} + +export interface RiskFactorPaginatedResponse { + current_page: number; + data: RiskFactor[]; + first_page_url: string; + from: number; + last_page: number; + last_page_url: string; + links: { url: string | null; label: string; active: boolean }[]; + next_page_url: string | null; + path: string; + per_page: number; + prev_page_url: string | null; + to: number; + total: number; +} diff --git a/src/modules/catalog/types/routeSegments.interfaces.ts b/src/modules/catalog/types/routeSegments.interfaces.ts new file mode 100644 index 0000000..fd7a42d --- /dev/null +++ b/src/modules/catalog/types/routeSegments.interfaces.ts @@ -0,0 +1,150 @@ +import type { Location } from './locations.interfaces'; + +// ─── Entidades principales ─────────────────────────────────── + +export interface RoutePoint { + lat: number; + lng: number; +} + +export interface NavigationStep { + maneuver: { + instruction: string; + }; +} + +export interface NavigationLeg { + steps: NavigationStep[]; +} + +export interface RiskOption { + id: number; + risk_factor_id: number; + name: string; + value: number; +} + +export interface RiskFactor { + id: number; + name: string; + description: string | null; + evaluation_context: number; + options?: RiskOption[]; + selected_option_id?: number | null; +} + +export interface RouteRiskEvaluation { + id: number; + route_segment_id: number; + risk_factor_id: number; + risk_option_id: number; + risk_factor?: RiskFactor; + risk_option?: RiskOption; +} + +export interface GeoJSONLineString { + type: 'LineString'; + coordinates: [number, number][]; +} + +export interface RouteSegment { + id: number; + origin_location_id: number; + destination_location_id: number; + sales_service_type_id: number | null; + meters_distance: number | null; + seconds_duration: number | null; + path_geojson: GeoJSONLineString | null; + navigation_metadata: NavigationLeg[] | null; + route_points: RoutePoint[] | null; + tenant_id: number; + created_at: string; + updated_at: string; + deleted_at: string | null; + // Relaciones cargadas + origin?: Location; + destination?: Location; + risk_evaluations?: RouteRiskEvaluation[]; +} + +// ─── Errores de formulario ─────────────────────────────────── + +export interface RouteSegmentFormErrors { + origin_location_id?: string[]; + destination_location_id?: string[]; + sales_service_type_id?: string[]; + meters_distance?: string[]; + seconds_duration?: string[]; + path_geometry?: string[]; + navigation_metadata?: string[]; + route_points?: string[]; +} + +// ─── Paginación ────────────────────────────────────────────── + +export interface PaginationLink { + url: string | null; + label: string; + active: boolean; +} + +export interface RouteSegmentPaginatedResponse { + current_page: number; + data: RouteSegment[]; + first_page_url: string; + from: number; + last_page: number; + last_page_url: string; + links: PaginationLink[]; + next_page_url: string | null; + path: string; + per_page: number; + prev_page_url: string | null; + to: number; + total: number; +} + +export interface RouteSegmentListResponse { + data: RouteSegment[]; +} + +// ─── Request/Response ──────────────────────────────────────── + +export interface RouteSegmentCreateRequest { + origin_location_id: number; + destination_location_id: number; + sales_service_type_id?: number | null; + meters_distance?: number | null; + seconds_duration?: number | null; + path_geometry?: GeoJSONLineString | null; + navigation_metadata?: NavigationLeg[] | null; + route_points?: RoutePoint[] | null; +} + +export type RouteSegmentUpdateRequest = Partial; + +export interface RouteSegmentCreateResponse { + data: RouteSegment; +} + +export type RouteSegmentUpdateResponse = RouteSegmentCreateResponse; + +export interface RouteSegmentDeleteResponse { + message: string; + data: null; +} + +// ─── Evaluaciones de Riesgo ────────────────────────────────── + +export interface RiskEvaluationPayload { + risk_factor_id: number; + risk_option_id: number; +} + +export interface UpdateRiskEvaluationsRequest { + evaluations: RiskEvaluationPayload[]; +} + +export interface RiskFactorsResponse { + data: RiskFactor[]; +} diff --git a/src/modules/catalog/types/routes.interfaces.ts b/src/modules/catalog/types/routes.interfaces.ts new file mode 100644 index 0000000..98a7873 --- /dev/null +++ b/src/modules/catalog/types/routes.interfaces.ts @@ -0,0 +1,77 @@ +import type { Location } from './locations.interfaces'; +import type { RouteSegment } from './routeSegments.interfaces'; + +export interface RouteCheckpoint { + id: number; + route_id: number; + location_id: number; + sequence: number; + location?: Location; +} + +export interface Route { + id: number; + sales_service_type_id: number | null; + meters_distance: number | null; + seconds_duration: number | null; + active: boolean; + tenant_id: number; + created_at: string; + updated_at: string; + deleted_at: string | null; + origin?: Location; + destination?: Location; + checkpoints?: RouteCheckpoint[]; + segments?: RouteSegment[]; +} + +export interface RouteCreateRequest { + sales_service_type_id?: number | null; + active?: boolean; + route_segments: number[]; // Array of RouteSegment IDs +} + +export type RouteUpdateRequest = Partial; + +export interface RouteFormErrors { + sales_service_type_id?: string[]; + active?: string[]; + route_segments?: string[]; +} + +export interface PaginationLink { + url: string | null; + label: string; + active: boolean; +} + +export interface RoutePaginatedResponse { + current_page: number; + data: Route[]; + first_page_url: string; + from: number; + last_page: number; + last_page_url: string; + links: PaginationLink[]; + next_page_url: string | null; + path: string; + per_page: number; + prev_page_url: string | null; + to: number; + total: number; +} + +export interface RouteListResponse { + data: Route[]; +} + +export interface RouteCreateResponse { + data: Route; +} + +export type RouteUpdateResponse = RouteCreateResponse; + +export interface RouteDeleteResponse { + message: string; + data: null; +} diff --git a/src/router/index.ts b/src/router/index.ts index 031b7bf..1ceabb8 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -32,6 +32,8 @@ import Companies from '../modules/catalog/components/companies/Companies.vue'; import '../modules/catalog/components/suppliers/Suppliers.vue'; import Suppliers from '../modules/catalog/components/suppliers/Suppliers.vue'; import Locations from '../modules/catalog/components/locations/Locations.vue'; +import RouteSegments from '../modules/catalog/components/route-segments/RouteSegments.vue'; +import Routes from '../modules/catalog/components/routes/Routes.vue'; import Purchases from '../modules/purchases/components/Purchases.vue'; import PurchaseDetails from '../modules/purchases/components/PurchaseDetails.vue'; import PurchaseForm from '../modules/purchases/components/PurchaseForm.vue'; @@ -205,6 +207,24 @@ const routes: RouteRecordRaw[] = [ requiresAuth: true } }, + { + path: 'route-segments', + name: 'RouteSegments', + component: RouteSegments, + meta: { + title: 'Segmentos de Ruta', + requiresAuth: true + } + }, + { + path: 'routes', + name: 'Routes', + component: Routes, + meta: { + title: 'Catálogo de Rutas', + requiresAuth: true + } + }, { path: 'model-documents', name: 'ModelDocuments', @@ -224,6 +244,16 @@ const routes: RouteRecordRaw[] = [ permission: 'document_concepts.index' } }, + { + path: 'risk-factors', + name: 'RiskFactors', + component: () => import('@/modules/catalog/components/risk-factors/RiskFactors.vue'), + meta: { + title: 'Factores de Riesgo', + requiresAuth: true, + permission: 'risk-factors.index' + } + }, companiesRouter ] },