diff --git a/package-lock.json b/package-lock.json index 047b75c..dfee2f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "golscontros-frontend-v1", "version": "0.0.0", "dependencies": { + "@mapbox/mapbox-gl-draw": "^1.5.1", "@primeuix/themes": "^1.2.5", "@primevue/auto-import-resolver": "^4.4.1", "@tailwindcss/vite": "^4.1.16", @@ -538,6 +539,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/geojson-area": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz", + "integrity": "sha512-bBqqFn1kIbLBfn7Yq1PzzwVkPYQr9lVUeT8Dhd0NL5n76PBuXzOcuLV7GOSbEB1ia8qWxH4COCvFpziEu/yReA==", + "license": "BSD-2-Clause", + "dependencies": { + "wgs84": "0.0.0" + } + }, + "node_modules/@mapbox/geojson-normalize": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-normalize/-/geojson-normalize-0.0.1.tgz", + "integrity": "sha512-82V7YHcle8lhgIGqEWwtXYN5cy0QM/OHq3ypGhQTbvHR57DF0vMHMjjVSQKFfVXBe/yWCBZTyOuzvK7DFFnx5Q==", + "license": "ISC", + "bin": { + "geojson-normalize": "geojson-normalize" + } + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -546,6 +565,41 @@ "node": ">= 0.6" } }, + "node_modules/@mapbox/mapbox-gl-draw": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.5.1.tgz", + "integrity": "sha512-DnR/oarZVoIrVHssAn+mtpuGzYH+ebORoPjow46zTBNPod/HQnvIZGtL6hIb5BVWxxH49RC9D20ipxiO9WDRxA==", + "license": "ISC", + "dependencies": { + "@mapbox/geojson-area": "^0.2.2", + "@mapbox/geojson-normalize": "^0.0.1", + "@mapbox/point-geometry": "^1.1.0", + "@turf/projection": "^7.2.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^5.0.9" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/@mapbox/mapbox-gl-draw/node_modules/nanoid": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz", + "integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/@mapbox/mapbox-gl-supported": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz", @@ -1220,6 +1274,63 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@turf/clone": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-7.3.4.tgz", + "integrity": "sha512-pwQ+RyQw986uu7IulY/18NRAebwZZScb084bvVqVkTrllwLSv4oVBqUxmUMiwtp+PNdiRGRFOvNyZqtRsiD+Jw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.4.tgz", + "integrity": "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.3.4.tgz", + "integrity": "sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/projection": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/projection/-/projection-7.3.4.tgz", + "integrity": "sha512-p91zOaLmzoBHzU/2H6Ot1tOhTmAom85n1P7I4Oo0V9xU8hmJXWfNnomLFf/6rnkKDIFZkncLQIBz4iIecZ61sA==", + "license": "MIT", + "dependencies": { + "@turf/clone": "7.3.4", + "@turf/helpers": "7.3.4", + "@turf/meta": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1823,6 +1934,12 @@ "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2820,6 +2937,12 @@ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", "license": "ISC" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3052,6 +3175,12 @@ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" + }, + "node_modules/wgs84": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/wgs84/-/wgs84-0.0.0.tgz", + "integrity": "sha512-ANHlY4Rb5kHw40D0NJ6moaVfOCMrp9Gpd1R/AIQYg2ko4/jzcJ+TVXYYF6kXJqQwITvEZP4yEthjM7U6rYlljQ==", + "license": "BSD-2-Clause" } } } diff --git a/package.json b/package.json index d35cf2c..b447cbd 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@mapbox/mapbox-gl-draw": "^1.5.1", "@primeuix/themes": "^1.2.5", "@primevue/auto-import-resolver": "^4.4.1", "@tailwindcss/vite": "^4.1.16", diff --git a/src/modules/catalog/components/locations/LocationForm.vue b/src/modules/catalog/components/locations/LocationForm.vue index c05692f..8362558 100644 --- a/src/modules/catalog/components/locations/LocationForm.vue +++ b/src/modules/catalog/components/locations/LocationForm.vue @@ -1,13 +1,19 @@ - - + + - + - Nombre de la Ubicación + Nombre de la Ubicación {{ props.formErrors.name[0] }} - Descripción - + Descripción + {{ props.formErrors.description[0] }} Dirección - + - Calle y Número - + Calle y Número + - Ciudad + Ciudad - Estado + Estado - País + País - C.P. + C.P. @@ -341,18 +722,18 @@ onBeforeUnmount(() => { Geolocalización - + - Latitud + Latitud - Longitud + Longitud - Radio geocerca (metros) - + Radio geocerca (metros) + @@ -371,8 +752,8 @@ onBeforeUnmount(() => { - - + + Mapa @@ -391,7 +772,7 @@ onBeforeUnmount(() => { /> - + Define VITE_MAPBOX_TOKEN para habilitar el mapa. @@ -407,5 +788,47 @@ onBeforeUnmount(() => { \ No newline at end of file +/* Make the map Card grow and its content area fill remaining height */ +.lg\:flex-1 :deep(.p-card) { + display: flex; + flex-direction: column; + height: 100%; +} +.lg\:flex-1 :deep(.p-card-body), +.lg\:flex-1 :deep(.p-card-content) { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} +.lg\:flex-1 :deep(.p-card-content > div) { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +/* Align Mapbox Draw controls with PrimeVue visual language */ +:deep(.mapboxgl-ctrl-group) { + border: 1px solid var(--p-content-border-color, #d6d9de); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 4px rgb(15 23 42 / 12%); +} + +:deep(.mapboxgl-ctrl-group button) { + width: 34px; + height: 34px; + background-color: var(--p-content-background, #ffffff); +} + +:deep(.mapboxgl-ctrl-group button:hover) { + background-color: var(--p-surface-100, #f1f5f9); +} + +:deep(.mapboxgl-ctrl-group button:focus-visible) { + outline: 2px solid var(--p-primary-color, #3b82f6); + outline-offset: -2px; +} + + diff --git a/src/modules/catalog/components/locations/Locations.vue b/src/modules/catalog/components/locations/Locations.vue index 9584507..f3725d6 100644 --- a/src/modules/catalog/components/locations/Locations.vue +++ b/src/modules/catalog/components/locations/Locations.vue @@ -7,7 +7,6 @@ import DataTable from 'primevue/datatable'; import Column from 'primevue/column'; import InputText from 'primevue/inputtext'; import Paginator from 'primevue/paginator'; -import Dialog from 'primevue/dialog'; import { useConfirm } from 'primevue/useconfirm'; import { useToast } from 'primevue/usetoast'; @@ -211,132 +210,201 @@ const onPageChange = (event: any) => { const newPage = Math.floor(event.first / event.rows) + 1; fetchLocations(newPage); }; + +const hexToBytes = (hex: string): Uint8Array | null => { + const normalized = hex.trim(); + if (normalized.length % 2 !== 0) return null; + if (!/^[0-9a-fA-F]+$/.test(normalized)) return null; + + const bytes = new Uint8Array(normalized.length / 2); + for (let i = 0; i < normalized.length; i += 2) { + bytes[i / 2] = Number.parseInt(normalized.slice(i, i + 2), 16); + } + + return bytes; +}; + +const parseHexEwkbPoint = (value: string): { lat: number; lng: number } | null => { + const bytes = hexToBytes(value); + if (!bytes || bytes.length < 1 + 4 + 16) return null; + + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const littleEndian = view.getUint8(0) === 1; + + const typeWithFlags = view.getUint32(1, littleEndian); + const hasSrid = (typeWithFlags & 0x20000000) !== 0; + const geometryType = typeWithFlags & 0x0fffffff; + if (geometryType !== 1) return null; + + let offset = 1 + 4; + if (hasSrid) { + if (bytes.length < offset + 4 + 16) return null; + offset += 4; + } + + if (bytes.length < offset + 16) return null; + const lng = view.getFloat64(offset, littleEndian); + const lat = view.getFloat64(offset + 8, littleEndian); + if (Number.isNaN(lat) || Number.isNaN(lng)) return null; + return { lat, lng }; +}; + +const parseCoordinates = (value?: string | null): { lat: number; lng: number } | null => { + if (!value) return null; + + const normalized = value.trim(); + const wkt = normalized.replace(/^SRID=\d+;/i, ''); + const match = wkt.match(/POINT\s*\(\s*([-\d.]+)\s+([-\d.]+)\s*\)/i); + if (match) { + const lng = Number(match[1]); + const lat = Number(match[2]); + if (Number.isNaN(lat) || Number.isNaN(lng)) return null; + return { lat, lng }; + } + + return parseHexEwkbPoint(normalized); +}; + +const formatCoordinates = (value?: string | null): string => { + const point = parseCoordinates(value); + if (!point) return value || '-'; + + return `Lat ${point.lat.toFixed(6)}, Lng ${point.lng.toFixed(6)}`; +}; - - - Gestión de Ubicaciones - Administra las ubicaciones operativas del sistema. + + + + + + + {{ isEditMode ? 'Editar Ubicación' : 'Nueva Ubicación' }} + + Administra los datos y la geolocalización de la ubicación. + - - + - - - - - Nombre - - - - Ciudad - - - - Estado - - - - País - - - - - - + + + + + Gestión de Ubicaciones + Administra las ubicaciones operativas del sistema. - - + + - - - - - - - - - - - - - {{ data.coordinates || '-' }} - - - - - - {{ data.geofence ? 'Definida' : 'Sin definir' }} - - - - - {{ new Date(data.created_at).toLocaleDateString('es-MX') }} - - - - - - - - - - - - - - - - + + + + + Nombre + + + + Ciudad + + + + Estado + + + + País + + + + + + + + + - - - - - No tienes permisos para visualizar este módulo. - - - + + + + + + + + + + + + + {{ formatCoordinates(data.coordinates) }} + + + + + + {{ data.geofence ? 'Definida' : 'Sin definir' }} + + + + + {{ new Date(data.created_at).toLocaleDateString('es-MX') }} + + + + + + + + + + + + + + + + + + + + + + No tienes permisos para visualizar este módulo. + + + + - - - diff --git a/src/modules/catalog/types/locations.interfaces.ts b/src/modules/catalog/types/locations.interfaces.ts index 26bc22c..11f484c 100644 --- a/src/modules/catalog/types/locations.interfaces.ts +++ b/src/modules/catalog/types/locations.interfaces.ts @@ -1,3 +1,7 @@ +import type { Polygon } from 'geojson'; + +export type LocationGeofenceGeoJSON = Polygon; + export interface LocationFormErrors { name?: string[]; description?: string[]; @@ -20,7 +24,7 @@ export interface Location { country: string | null; zip_code: string | null; coordinates: string | null; - geofence: string | null; + geofence: LocationGeofenceGeoJSON | null; tenant_id: number; created_at: string; updated_at: string; @@ -62,7 +66,7 @@ export interface LocationCreateRequest { country?: string | null; zip_code?: string | null; coordinates?: string | null; - geofence?: string | null; + geofence?: LocationGeofenceGeoJSON | null; } export type LocationUpdateRequest = Partial; diff --git a/src/types/mapbox-gl-draw.d.ts b/src/types/mapbox-gl-draw.d.ts new file mode 100644 index 0000000..e0be59b --- /dev/null +++ b/src/types/mapbox-gl-draw.d.ts @@ -0,0 +1,32 @@ +declare module '@mapbox/mapbox-gl-draw' { + import type { FeatureCollection, Polygon } from 'geojson'; + + type DrawMode = 'simple_select' | 'direct_select' | 'draw_polygon' | string; + + interface DrawControls { + point?: boolean; + line_string?: boolean; + polygon?: boolean; + trash?: boolean; + combine_features?: boolean; + uncombine_features?: boolean; + } + + interface DrawOptions { + displayControlsDefault?: boolean; + controls?: DrawControls; + defaultMode?: DrawMode; + } + + export default class MapboxDraw { + constructor(options?: DrawOptions); + add(feature: GeoJSON.Feature): string[] | string; + get(id: string): GeoJSON.Feature | undefined; + getAll(): FeatureCollection; + delete(id: string): void; + deleteAll(): void; + getMode(): DrawMode; + } + + export type DrawPolygonFeature = GeoJSON.Feature; +}
Administra las ubicaciones operativas del sistema.
Administra los datos y la geolocalización de la ubicación.
No tienes permisos para visualizar este módulo.