feat: Implement add location with mapbox
This commit is contained in:
parent
755bcd6503
commit
643c698de2
237
package-lock.json
generated
237
package-lock.json
generated
@ -13,6 +13,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@vueuse/core": "^14.0.0",
|
"@vueuse/core": "^14.0.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"mapbox-gl": "^3.15.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.4.1",
|
"primevue": "^4.4.1",
|
||||||
@ -537,6 +538,49 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
|
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"integrity": "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/point-geometry": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/tiny-sdf": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/unitbezier": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/vector-tile": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "~1.1.0",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"pbf": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@primeuix/styled": {
|
"node_modules/@primeuix/styled": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz",
|
||||||
@ -1182,6 +1226,21 @@
|
|||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/geojson-vt": {
|
||||||
|
"version": "3.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
|
||||||
|
"integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.10.0",
|
"version": "24.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz",
|
||||||
@ -1192,6 +1251,21 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pbf": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/supercluster": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/web-bluetooth": {
|
"node_modules/@types/web-bluetooth": {
|
||||||
"version": "0.0.21",
|
"version": "0.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||||
@ -1505,6 +1579,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cheap-ruler": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
@ -1553,6 +1633,12 @@
|
|||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/csscolorparser": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
@ -1608,6 +1694,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/earcut": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.3",
|
"version": "5.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||||
@ -1807,6 +1899,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/geojson-vt": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
@ -1844,6 +1942,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gl-matrix": {
|
||||||
|
"version": "3.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||||
|
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@ -1862,6 +1966,12 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/grid-index": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/has-symbols": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
@ -1928,6 +2038,12 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kdbush": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||||
@ -2203,6 +2319,55 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mapbox-gl": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-+rVQkf6ymUlAEJiQBZy0OiamJvQN4Uk15mRHI98PRUSmRS40GOoLJyEZEG39LEUtvmzc7qGh+4ygZfJ//O5VnQ==",
|
||||||
|
"license": "SEE LICENSE IN LICENSE.txt",
|
||||||
|
"workspaces": [
|
||||||
|
"src/style-spec",
|
||||||
|
"test/build/vite",
|
||||||
|
"test/build/webpack",
|
||||||
|
"test/build/typings"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||||
|
"@mapbox/mapbox-gl-supported": "^3.0.0",
|
||||||
|
"@mapbox/point-geometry": "^1.1.0",
|
||||||
|
"@mapbox/tiny-sdf": "^2.0.6",
|
||||||
|
"@mapbox/unitbezier": "^0.0.1",
|
||||||
|
"@mapbox/vector-tile": "^2.0.4",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"@types/geojson-vt": "^3.2.5",
|
||||||
|
"@types/pbf": "^3.0.5",
|
||||||
|
"@types/supercluster": "^7.1.3",
|
||||||
|
"cheap-ruler": "^4.0.0",
|
||||||
|
"csscolorparser": "~1.0.3",
|
||||||
|
"earcut": "^3.0.1",
|
||||||
|
"geojson-vt": "^4.0.2",
|
||||||
|
"gl-matrix": "^3.4.4",
|
||||||
|
"grid-index": "^1.1.0",
|
||||||
|
"kdbush": "^4.0.2",
|
||||||
|
"martinez-polygon-clipping": "^0.8.1",
|
||||||
|
"murmurhash-js": "^1.0.0",
|
||||||
|
"pbf": "^4.0.1",
|
||||||
|
"potpack": "^2.0.0",
|
||||||
|
"quickselect": "^3.0.0",
|
||||||
|
"supercluster": "^8.0.1",
|
||||||
|
"tinyqueue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/martinez-polygon-clipping": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-9PLLMzMPI6ihHox4Ns6LpVBLpRc7sbhULybZ/wyaY8sY3ECNe2+hxm1hA2/9bEEpRrdpjoeduBuZLg2aq1cSIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"robust-predicates": "^2.0.4",
|
||||||
|
"splaytree": "^0.1.4",
|
||||||
|
"tinyqueue": "3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@ -2281,6 +2446,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/murmurhash-js": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@ -2312,6 +2483,18 @@
|
|||||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pbf": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-protobuf-schema": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pbf": "bin/pbf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/perfect-debounce": {
|
"node_modules/perfect-debounce": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
@ -2405,6 +2588,12 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/potpack": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/primeicons": {
|
"node_modules/primeicons": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
||||||
@ -2427,6 +2616,12 @@
|
|||||||
"node": ">=12.11.0"
|
"node": ">=12.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/protocol-buffers-schema": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@ -2449,6 +2644,12 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/quickselect": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@ -2462,12 +2663,27 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve-protobuf-schema": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"protocol-buffers-schema": "^3.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rfdc": {
|
"node_modules/rfdc": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/robust-predicates": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.52.5",
|
"version": "4.52.5",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
|
||||||
@ -2527,6 +2743,21 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/splaytree": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/supercluster": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"kdbush": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/superjson": {
|
"node_modules/superjson": {
|
||||||
"version": "2.2.5",
|
"version": "2.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz",
|
||||||
@ -2583,6 +2814,12 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyqueue": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@vueuse/core": "^14.0.0",
|
"@vueuse/core": "^14.0.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"mapbox-gl": "^3.15.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.4.1",
|
"primevue": "^4.4.1",
|
||||||
|
|||||||
@ -72,7 +72,7 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
{
|
{
|
||||||
label: 'Ubicaciones',
|
label: 'Ubicaciones',
|
||||||
icon: 'pi pi-map-marker',
|
icon: 'pi pi-map-marker',
|
||||||
to: '/catalog/companies',
|
to: '/catalog/locations',
|
||||||
permission: [
|
permission: [
|
||||||
'locations.index',
|
'locations.index',
|
||||||
'locations.show',
|
'locations.show',
|
||||||
|
|||||||
411
src/modules/catalog/components/locations/LocationForm.vue
Normal file
411
src/modules/catalog/components/locations/LocationForm.vue
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
|
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import Textarea from 'primevue/textarea';
|
||||||
|
|
||||||
|
import type { LocationCreateRequest, LocationFormErrors } from '../../types/locations.interfaces';
|
||||||
|
|
||||||
|
type FormData = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
country: string;
|
||||||
|
zip_code: string;
|
||||||
|
latitude: string;
|
||||||
|
longitude: string;
|
||||||
|
geofence_radius_m: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
initialData?: Partial<LocationCreateRequest> & {
|
||||||
|
name?: string;
|
||||||
|
description?: string | null;
|
||||||
|
address?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
state?: string | null;
|
||||||
|
country?: string | null;
|
||||||
|
zip_code?: string | null;
|
||||||
|
coordinates?: string | null;
|
||||||
|
geofence?: string | null;
|
||||||
|
};
|
||||||
|
formErrors?: LocationFormErrors;
|
||||||
|
isEditing?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'submit', payload: LocationCreateRequest): void;
|
||||||
|
(e: 'cancel'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const mapToken = import.meta.env.VITE_MAPBOX_TOKEN as string | undefined;
|
||||||
|
const mapContainer = ref<HTMLElement | null>(null);
|
||||||
|
const mapRef = ref<any>(null);
|
||||||
|
const markerRef = ref<any>(null);
|
||||||
|
const mapReady = ref(false);
|
||||||
|
|
||||||
|
const form = ref<FormData>({
|
||||||
|
name: props.initialData?.name ?? '',
|
||||||
|
description: props.initialData?.description ?? '',
|
||||||
|
address: props.initialData?.address ?? '',
|
||||||
|
city: props.initialData?.city ?? '',
|
||||||
|
state: props.initialData?.state ?? '',
|
||||||
|
country: props.initialData?.country ?? '',
|
||||||
|
zip_code: props.initialData?.zip_code ?? '',
|
||||||
|
latitude: '18.039300',
|
||||||
|
longitude: '-92.579000',
|
||||||
|
geofence_radius_m: '75',
|
||||||
|
});
|
||||||
|
|
||||||
|
const geocodingSearch = ref('');
|
||||||
|
const geocodingLoading = ref(false);
|
||||||
|
const errorMessage = ref('');
|
||||||
|
|
||||||
|
const canInitializeMap = computed(() => !!mapToken && !!mapContainer.value);
|
||||||
|
|
||||||
|
const toRadians = (value: number) => (value * Math.PI) / 180;
|
||||||
|
const toDegrees = (value: number) => (value * 180) / Math.PI;
|
||||||
|
|
||||||
|
const parseCoordinates = (value?: string | null): { lat: number; lng: number } | null => {
|
||||||
|
if (!value) return null;
|
||||||
|
const match = value.match(/POINT\s*\(\s*([-\d.]+)\s+([-\d.]+)\s*\)/i);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const lng = Number(match[1]);
|
||||||
|
const lat = Number(match[2]);
|
||||||
|
if (Number.isNaN(lat) || Number.isNaN(lng)) return null;
|
||||||
|
return { lat, lng };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLatLng = (lat: number, lng: number) => {
|
||||||
|
form.value.latitude = lat.toFixed(6);
|
||||||
|
form.value.longitude = lng.toFixed(6);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentLatLng = (): { lat: number; lng: number } | null => {
|
||||||
|
const lat = Number(form.value.latitude);
|
||||||
|
const lng = Number(form.value.longitude);
|
||||||
|
if (Number.isNaN(lat) || Number.isNaN(lng)) return null;
|
||||||
|
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) return null;
|
||||||
|
return { lat, lng };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCirclePolygonWkt = (lat: number, lng: number, radiusMeters: number, segments = 48): string => {
|
||||||
|
const earthRadius = 6378137;
|
||||||
|
const angularDistance = radiusMeters / earthRadius;
|
||||||
|
const latRad = toRadians(lat);
|
||||||
|
const lngRad = toRadians(lng);
|
||||||
|
const points: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i <= segments; i += 1) {
|
||||||
|
const bearing = (2 * Math.PI * i) / segments;
|
||||||
|
const destLat = Math.asin(
|
||||||
|
Math.sin(latRad) * Math.cos(angularDistance) +
|
||||||
|
Math.cos(latRad) * Math.sin(angularDistance) * Math.cos(bearing)
|
||||||
|
);
|
||||||
|
const destLng =
|
||||||
|
lngRad +
|
||||||
|
Math.atan2(
|
||||||
|
Math.sin(bearing) * Math.sin(angularDistance) * Math.cos(latRad),
|
||||||
|
Math.cos(angularDistance) - Math.sin(latRad) * Math.sin(destLat)
|
||||||
|
);
|
||||||
|
|
||||||
|
points.push(`${toDegrees(destLng)} ${toDegrees(destLat)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `SRID=4326;POLYGON((${points.join(',')}))`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildPostgisPayload = (): LocationCreateRequest | null => {
|
||||||
|
const current = getCurrentLatLng();
|
||||||
|
if (!current) {
|
||||||
|
errorMessage.value = 'Las coordenadas no son válidas.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const radius = Number(form.value.geofence_radius_m);
|
||||||
|
if (Number.isNaN(radius) || radius < 0) {
|
||||||
|
errorMessage.value = 'El radio de geocerca debe ser un número válido.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage.value = '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: form.value.name.trim(),
|
||||||
|
description: form.value.description?.trim() || null,
|
||||||
|
address: form.value.address.trim(),
|
||||||
|
city: form.value.city.trim() || null,
|
||||||
|
state: form.value.state.trim() || null,
|
||||||
|
country: form.value.country.trim() || null,
|
||||||
|
zip_code: form.value.zip_code.trim() || null,
|
||||||
|
coordinates: `SRID=4326;POINT(${current.lng} ${current.lat})`,
|
||||||
|
geofence: radius > 0 ? createCirclePolygonWkt(current.lat, current.lng, radius) : null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncMarkerWithInputs = () => {
|
||||||
|
if (!mapRef.value || !markerRef.value) return;
|
||||||
|
const current = getCurrentLatLng();
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
|
markerRef.value.setLngLat([current.lng, current.lat]);
|
||||||
|
mapRef.value.flyTo({ center: [current.lng, current.lat], zoom: 14 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const reverseGeocode = async (lat: number, lng: number) => {
|
||||||
|
if (!mapToken) return;
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.mapbox.com/geocoding/v5/mapbox.places/${lng},${lat}.json?language=es&limit=1&access_token=${mapToken}`
|
||||||
|
);
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const first = data?.features?.[0];
|
||||||
|
if (!first) return;
|
||||||
|
|
||||||
|
form.value.address = first.place_name || form.value.address;
|
||||||
|
const context = Array.isArray(first.context) ? first.context : [];
|
||||||
|
for (const item of context) {
|
||||||
|
if (item.id?.startsWith('place')) form.value.city = item.text || form.value.city;
|
||||||
|
if (item.id?.startsWith('region')) form.value.state = item.text || form.value.state;
|
||||||
|
if (item.id?.startsWith('country')) form.value.country = item.text || form.value.country;
|
||||||
|
if (item.id?.startsWith('postcode')) form.value.zip_code = item.text || form.value.zip_code;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore reverse geocoding errors to avoid blocking form flow.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchAddress = async () => {
|
||||||
|
if (!mapToken || !geocodingSearch.value.trim() || !mapRef.value || !markerRef.value) return;
|
||||||
|
geocodingLoading.value = true;
|
||||||
|
try {
|
||||||
|
const query = encodeURIComponent(geocodingSearch.value.trim());
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.mapbox.com/geocoding/v5/mapbox.places/${query}.json?language=es&limit=1&access_token=${mapToken}`
|
||||||
|
);
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const first = data?.features?.[0];
|
||||||
|
if (!first?.center) return;
|
||||||
|
|
||||||
|
const [lng, lat] = first.center;
|
||||||
|
updateLatLng(lat, lng);
|
||||||
|
markerRef.value.setLngLat([lng, lat]);
|
||||||
|
mapRef.value.flyTo({ center: [lng, lat], zoom: 15 });
|
||||||
|
await reverseGeocode(lat, lng);
|
||||||
|
} finally {
|
||||||
|
geocodingLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeMap = () => {
|
||||||
|
if (!canInitializeMap.value || mapRef.value) return;
|
||||||
|
|
||||||
|
mapboxgl.accessToken = mapToken!;
|
||||||
|
const current = getCurrentLatLng() ?? { lat: 18.0393, lng: -92.579 };
|
||||||
|
|
||||||
|
mapRef.value = new mapboxgl.Map({
|
||||||
|
container: mapContainer.value!,
|
||||||
|
style: 'mapbox://styles/mapbox/streets-v12',
|
||||||
|
center: [current.lng, current.lat],
|
||||||
|
zoom: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
mapRef.value.addControl(new mapboxgl.NavigationControl(), 'top-right');
|
||||||
|
|
||||||
|
markerRef.value = new mapboxgl.Marker({ draggable: true })
|
||||||
|
.setLngLat([current.lng, current.lat])
|
||||||
|
.addTo(mapRef.value);
|
||||||
|
|
||||||
|
markerRef.value.on('dragend', () => {
|
||||||
|
const lngLat = markerRef.value!.getLngLat();
|
||||||
|
updateLatLng(lngLat.lat, lngLat.lng);
|
||||||
|
reverseGeocode(lngLat.lat, lngLat.lng);
|
||||||
|
});
|
||||||
|
|
||||||
|
mapRef.value.on('click', (event: any) => {
|
||||||
|
markerRef.value!.setLngLat(event.lngLat);
|
||||||
|
updateLatLng(event.lngLat.lat, event.lngLat.lng);
|
||||||
|
reverseGeocode(event.lngLat.lat, event.lngLat.lng);
|
||||||
|
});
|
||||||
|
|
||||||
|
mapRef.value.on('load', () => {
|
||||||
|
mapReady.value = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = () => {
|
||||||
|
const payload = buildPostgisPayload();
|
||||||
|
if (!payload) return;
|
||||||
|
emit('submit', payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelForm = () => emit('cancel');
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialData,
|
||||||
|
(newData: typeof props.initialData) => {
|
||||||
|
if (!newData) return;
|
||||||
|
form.value.name = newData.name ?? '';
|
||||||
|
form.value.description = newData.description ?? '';
|
||||||
|
form.value.address = newData.address ?? '';
|
||||||
|
form.value.city = newData.city ?? '';
|
||||||
|
form.value.state = newData.state ?? '';
|
||||||
|
form.value.country = newData.country ?? '';
|
||||||
|
form.value.zip_code = newData.zip_code ?? '';
|
||||||
|
|
||||||
|
const parsed = parseCoordinates(newData.coordinates ?? null);
|
||||||
|
if (parsed) {
|
||||||
|
updateLatLng(parsed.lat, parsed.lng);
|
||||||
|
syncMarkerWithInputs();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.value.latitude, form.value.longitude],
|
||||||
|
() => {
|
||||||
|
syncMarkerWithInputs();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initializeMap();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (mapRef.value) {
|
||||||
|
mapRef.value.remove();
|
||||||
|
mapRef.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col lg:flex-row gap-5 min-h-[75vh]">
|
||||||
|
<div class="w-full lg:basis-2/5 lg:max-w-[40%]">
|
||||||
|
<Card>
|
||||||
|
<template #content>
|
||||||
|
<form class="space-y-4" @submit.prevent="submitForm">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-500 uppercase mb-2">Nombre de la Ubicación</label>
|
||||||
|
<InputText v-model="form.name" class="w-full" placeholder="Ej. Centro Logístico Norte" />
|
||||||
|
<small v-if="props.formErrors?.name" class="text-red-500">{{ props.formErrors.name[0] }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-500 uppercase mb-2">Descripción</label>
|
||||||
|
<Textarea v-model="form.description" rows="3" class="w-full" placeholder="Detalles operativos" />
|
||||||
|
<small v-if="props.formErrors?.description" class="text-red-500">{{ props.formErrors.description[0] }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<template #title>Dirección</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="block text-xs font-semibold mb-2">Calle y Número</label>
|
||||||
|
<InputText v-model="form.address" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold mb-2">Ciudad</label>
|
||||||
|
<InputText v-model="form.city" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold mb-2">Estado</label>
|
||||||
|
<InputText v-model="form.state" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold mb-2">País</label>
|
||||||
|
<InputText v-model="form.country" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold mb-2">C.P.</label>
|
||||||
|
<InputText v-model="form.zip_code" class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<template #title>Geolocalización</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold mb-2">Latitud</label>
|
||||||
|
<InputText v-model="form.latitude" class="w-full" placeholder="19.432600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold mb-2">Longitud</label>
|
||||||
|
<InputText v-model="form.longitude" class="w-full" placeholder="-99.133200" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="block text-xs font-semibold mb-2">Radio geocerca (metros)</label>
|
||||||
|
<InputText v-model="form.geofence_radius_m" class="w-full" placeholder="75" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div v-if="errorMessage" class="p-3 bg-red-50 border border-red-200 text-red-700 rounded-md text-sm">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<Button type="button" text severity="secondary" label="Cancelar" @click="cancelForm" />
|
||||||
|
<Button type="submit" :loading="props.loading" label="Guardar Ubicación" icon="pi pi-check" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full lg:basis-3/5 lg:max-w-[60%]">
|
||||||
|
<Card class="h-full">
|
||||||
|
<template #title>Mapa</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex flex-col gap-3 h-full">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<InputText
|
||||||
|
v-model="geocodingSearch"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Buscar dirección o referencia"
|
||||||
|
@keyup.enter="searchAddress"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-search"
|
||||||
|
label="Buscar"
|
||||||
|
:loading="geocodingLoading"
|
||||||
|
@click="searchAddress"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map" ref="mapContainer" class="w-full h-[60vh] rounded-md border border-surface-200" />
|
||||||
|
|
||||||
|
<small v-if="!mapToken" class="text-amber-600">
|
||||||
|
Define VITE_MAPBOX_TOKEN para habilitar el mapa.
|
||||||
|
</small>
|
||||||
|
<small v-else-if="mapReady" class="text-surface-500">
|
||||||
|
Haz clic o arrastra el marcador para actualizar coordenadas y dirección.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
</style>
|
||||||
342
src/modules/catalog/components/locations/Locations.vue
Normal file
342
src/modules/catalog/components/locations/Locations.vue
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
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';
|
||||||
|
|
||||||
|
import { locationServices } from '../../services/location.services';
|
||||||
|
import type { Location, LocationCreateRequest, LocationFormErrors, LocationPaginatedResponse } from '../../types/locations.interfaces';
|
||||||
|
import LocationForm from './LocationForm.vue';
|
||||||
|
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||||
|
|
||||||
|
const locations = ref<Location[]>([]);
|
||||||
|
const pagination = ref({
|
||||||
|
first: 0,
|
||||||
|
rows: 5,
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
lastPage: 1,
|
||||||
|
});
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
|
||||||
|
const searchName = ref('');
|
||||||
|
const searchCity = ref('');
|
||||||
|
const searchState = ref('');
|
||||||
|
const searchCountry = ref('');
|
||||||
|
|
||||||
|
const showFormDialog = ref(false);
|
||||||
|
const isEditMode = ref(false);
|
||||||
|
const currentLocation = ref<Location | null>(null);
|
||||||
|
const formErrors = ref<LocationFormErrors>({});
|
||||||
|
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const toast = useToast();
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
|
||||||
|
const canViewLocations = computed(() =>
|
||||||
|
hasPermission([
|
||||||
|
'locations.index',
|
||||||
|
'locations.show',
|
||||||
|
'locations.store',
|
||||||
|
'locations.update',
|
||||||
|
'locations.destroy',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const canCreateLocation = computed(() => hasPermission('locations.store'));
|
||||||
|
const canUpdateLocation = computed(() => hasPermission('locations.update'));
|
||||||
|
const canDeleteLocation = computed(() => hasPermission('locations.destroy'));
|
||||||
|
|
||||||
|
const handleCreateClick = () => {
|
||||||
|
if (!canCreateLocation.value) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Sin permisos', detail: 'No puedes crear ubicaciones.', life: 4000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditMode.value = false;
|
||||||
|
currentLocation.value = null;
|
||||||
|
formErrors.value = {};
|
||||||
|
showFormDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = async (location: Location) => {
|
||||||
|
if (!canUpdateLocation.value) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Sin permisos', detail: 'No puedes actualizar ubicaciones.', life: 4000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditMode.value = true;
|
||||||
|
formErrors.value = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await locationServices.getLocationById(location.id);
|
||||||
|
currentLocation.value = response.data;
|
||||||
|
showFormDialog.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo cargar la ubicación', life: 3000 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeFormDialog = () => {
|
||||||
|
showFormDialog.value = false;
|
||||||
|
isEditMode.value = false;
|
||||||
|
currentLocation.value = null;
|
||||||
|
formErrors.value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async (payload: LocationCreateRequest) => {
|
||||||
|
formErrors.value = {};
|
||||||
|
submitting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEditMode.value && currentLocation.value) {
|
||||||
|
await locationServices.updateLocation(currentLocation.value.id, payload, 'patch');
|
||||||
|
toast.add({ severity: 'success', summary: 'Ubicación actualizada', detail: 'La ubicación se actualizó correctamente.', life: 3000 });
|
||||||
|
} else {
|
||||||
|
await locationServices.createLocation(payload);
|
||||||
|
toast.add({ severity: 'success', summary: 'Ubicación creada', detail: 'La ubicación se registró correctamente.', life: 3000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
closeFormDialog();
|
||||||
|
fetchLocations(pagination.value.page);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.response?.data?.errors) {
|
||||||
|
formErrors.value = e.response.data.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error',
|
||||||
|
detail: e?.response?.data?.message || 'No se pudo guardar la ubicación.',
|
||||||
|
life: 3500,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (locationId: number) => {
|
||||||
|
if (!canDeleteLocation.value) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Sin permisos', detail: 'No puedes eliminar ubicaciones.', life: 4000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm.require({
|
||||||
|
message: '¿Seguro que deseas eliminar esta ubicación?',
|
||||||
|
header: 'Confirmar eliminación',
|
||||||
|
icon: 'pi pi-exclamation-triangle',
|
||||||
|
acceptLabel: 'Sí, eliminar',
|
||||||
|
rejectLabel: 'Cancelar',
|
||||||
|
accept: async () => {
|
||||||
|
try {
|
||||||
|
await locationServices.deleteLocation(locationId);
|
||||||
|
toast.add({ severity: 'success', summary: 'Eliminado', detail: 'Ubicación eliminada correctamente', life: 3000 });
|
||||||
|
fetchLocations(pagination.value.page);
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo eliminar la ubicación', life: 3000 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchLocations = async (page = 1) => {
|
||||||
|
if (!canViewLocations.value) {
|
||||||
|
locations.value = [];
|
||||||
|
pagination.value = {
|
||||||
|
...pagination.value,
|
||||||
|
page: 1,
|
||||||
|
total: 0,
|
||||||
|
first: 0,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pagination.value.page = page;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = searchName.value || undefined;
|
||||||
|
const city = searchCity.value || undefined;
|
||||||
|
const state = searchState.value || undefined;
|
||||||
|
const country = searchCountry.value || undefined;
|
||||||
|
|
||||||
|
const response = await locationServices.getLocations(true, name, city, state, country);
|
||||||
|
const paginated = response as LocationPaginatedResponse;
|
||||||
|
|
||||||
|
locations.value = paginated.data;
|
||||||
|
pagination.value.total = paginated.total;
|
||||||
|
pagination.value.page = paginated.current_page;
|
||||||
|
pagination.value.lastPage = paginated.last_page;
|
||||||
|
pagination.value.first = (paginated.current_page - 1) * paginated.per_page;
|
||||||
|
pagination.value.rows = paginated.per_page;
|
||||||
|
} catch (e) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar las ubicaciones.', life: 3000 });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => canViewLocations.value,
|
||||||
|
(allowed) => {
|
||||||
|
if (allowed) {
|
||||||
|
fetchLocations();
|
||||||
|
} else {
|
||||||
|
locations.value = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
searchName.value = '';
|
||||||
|
searchCity.value = '';
|
||||||
|
searchState.value = '';
|
||||||
|
searchCountry.value = '';
|
||||||
|
fetchLocations(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFilter = () => {
|
||||||
|
fetchLocations(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPageChange = (event: any) => {
|
||||||
|
const newPage = Math.floor(event.first / event.rows) + 1;
|
||||||
|
fetchLocations(newPage);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-3xl font-black text-surface-900 dark:text-white tracking-tight">Gestión de Ubicaciones</h2>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">Administra las ubicaciones operativas del sistema.</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-if="canCreateLocation"
|
||||||
|
label="Nueva Ubicación"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
@click="handleCreateClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card v-if="canViewLocations">
|
||||||
|
<template #content>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 items-end">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">Nombre</label>
|
||||||
|
<InputText v-model="searchName" placeholder="Ej. Bodega Norte" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">Ciudad</label>
|
||||||
|
<InputText v-model="searchCity" placeholder="Ej. Monterrey" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">Estado</label>
|
||||||
|
<InputText v-model="searchState" placeholder="Ej. Nuevo León" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">País</label>
|
||||||
|
<InputText v-model="searchCountry" placeholder="Ej. México" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button icon="pi pi-search" text rounded @click="onFilter" />
|
||||||
|
<Button icon="pi pi-times" text rounded severity="secondary" label="Limpiar" @click="clearFilters" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card v-if="canViewLocations">
|
||||||
|
<template #content>
|
||||||
|
<DataTable :value="locations" :loading="loading" stripedRows responsiveLayout="scroll" class="p-datatable-sm">
|
||||||
|
<Column field="name" header="Nombre" style="min-width: 180px" />
|
||||||
|
<Column field="address" header="Dirección" style="min-width: 220px" />
|
||||||
|
<Column field="city" header="Ciudad" style="min-width: 140px" />
|
||||||
|
<Column field="state" header="Estado" style="min-width: 140px" />
|
||||||
|
<Column field="country" header="País" style="min-width: 120px" />
|
||||||
|
<Column field="zip_code" header="CP" style="min-width: 100px" />
|
||||||
|
<Column header="Coordenadas" style="min-width: 220px">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="font-mono text-xs">
|
||||||
|
{{ data.coordinates || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="geofence" header="Geocerca" style="min-width: 180px">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-xs">{{ data.geofence ? 'Definida' : 'Sin definir' }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="created_at" header="Fecha de Registro" style="min-width: 120px">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-sm">{{ new Date(data.created_at).toLocaleDateString('es-MX') }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right" style="min-width: 130px">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
v-if="canUpdateLocation"
|
||||||
|
icon="pi pi-pencil"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
size="small"
|
||||||
|
@click="handleEditClick(data)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="canDeleteLocation"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
size="small"
|
||||||
|
severity="danger"
|
||||||
|
@click="handleDelete(data.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
<div class="mt-4">
|
||||||
|
<Paginator :first="pagination.first" :rows="pagination.rows" :totalRecords="pagination.total" :rowsPerPageOptions="[5, 10, 20, 50]" @page="onPageChange" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card v-else>
|
||||||
|
<template #content>
|
||||||
|
<div class="text-center py-10 text-surface-500 dark:text-surface-400">
|
||||||
|
<i class="pi pi-lock text-4xl mb-3"></i>
|
||||||
|
<p>No tienes permisos para visualizar este módulo.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="showFormDialog"
|
||||||
|
modal
|
||||||
|
maximizable
|
||||||
|
:header="isEditMode ? 'Editar Ubicación' : 'Nueva Ubicación'"
|
||||||
|
:style="{ width: '95vw', maxWidth: '1400px' }"
|
||||||
|
>
|
||||||
|
<LocationForm
|
||||||
|
:initialData="currentLocation || undefined"
|
||||||
|
:formErrors="formErrors"
|
||||||
|
:isEditing="isEditMode"
|
||||||
|
:loading="submitting"
|
||||||
|
@submit="handleFormSubmit"
|
||||||
|
@cancel="closeFormDialog"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<Toast />
|
||||||
|
</template>
|
||||||
81
src/modules/catalog/services/location.services.ts
Normal file
81
src/modules/catalog/services/location.services.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import api from '../../../services/api';
|
||||||
|
import type {
|
||||||
|
LocationCreateRequest,
|
||||||
|
LocationCreateResponse,
|
||||||
|
LocationDeleteResponse,
|
||||||
|
LocationListResponse,
|
||||||
|
LocationPaginatedResponse,
|
||||||
|
LocationUpdateRequest,
|
||||||
|
LocationUpdateResponse,
|
||||||
|
} from '../types/locations.interfaces';
|
||||||
|
|
||||||
|
const locationServices = {
|
||||||
|
async getLocations(
|
||||||
|
paginated?: boolean,
|
||||||
|
name?: string,
|
||||||
|
city?: string,
|
||||||
|
state?: string,
|
||||||
|
country?: string
|
||||||
|
): Promise<LocationPaginatedResponse | LocationListResponse> {
|
||||||
|
try {
|
||||||
|
const params: any = {};
|
||||||
|
if (paginated === false) params.paginated = false;
|
||||||
|
if (name) params.name = name;
|
||||||
|
if (city) params.city = city;
|
||||||
|
if (state) params.state = state;
|
||||||
|
if (country) params.country = country;
|
||||||
|
|
||||||
|
const response = await api.get('/api/locations', { params });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching locations:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getLocationById(locationId: number): Promise<LocationCreateResponse> {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/api/locations/${locationId}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching location with ID ${locationId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createLocation(data: LocationCreateRequest): Promise<LocationCreateResponse> {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/api/locations', data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating location:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateLocation(
|
||||||
|
locationId: number,
|
||||||
|
data: LocationUpdateRequest,
|
||||||
|
method: 'patch' | 'put' = 'patch'
|
||||||
|
): Promise<LocationUpdateResponse> {
|
||||||
|
try {
|
||||||
|
const response = await api[method](`/api/locations/${locationId}`, data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error updating location with ID ${locationId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteLocation(locationId: number): Promise<LocationDeleteResponse> {
|
||||||
|
try {
|
||||||
|
const response = await api.delete(`/api/locations/${locationId}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error deleting location with ID ${locationId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { locationServices };
|
||||||
79
src/modules/catalog/types/locations.interfaces.ts
Normal file
79
src/modules/catalog/types/locations.interfaces.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
export interface LocationFormErrors {
|
||||||
|
name?: string[];
|
||||||
|
description?: string[];
|
||||||
|
address?: string[];
|
||||||
|
city?: string[];
|
||||||
|
state?: string[];
|
||||||
|
country?: string[];
|
||||||
|
zip_code?: string[];
|
||||||
|
coordinates?: string[];
|
||||||
|
geofence?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Location {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
address: string | null;
|
||||||
|
city: string | null;
|
||||||
|
state: string | null;
|
||||||
|
country: string | null;
|
||||||
|
zip_code: string | null;
|
||||||
|
coordinates: string | null;
|
||||||
|
geofence: string | null;
|
||||||
|
tenant_id: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deleted_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationPaginationLink {
|
||||||
|
url: string | null;
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationPaginatedResponse {
|
||||||
|
current_page: number;
|
||||||
|
data: Location[];
|
||||||
|
first_page_url: string;
|
||||||
|
from: number;
|
||||||
|
last_page: number;
|
||||||
|
last_page_url: string;
|
||||||
|
links: LocationPaginationLink[];
|
||||||
|
next_page_url: string | null;
|
||||||
|
path: string;
|
||||||
|
per_page: number;
|
||||||
|
prev_page_url: string | null;
|
||||||
|
to: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationListResponse {
|
||||||
|
data: Location[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationCreateRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
address?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
state?: string | null;
|
||||||
|
country?: string | null;
|
||||||
|
zip_code?: string | null;
|
||||||
|
coordinates?: string | null;
|
||||||
|
geofence?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LocationUpdateRequest = Partial<LocationCreateRequest>;
|
||||||
|
|
||||||
|
export interface LocationCreateResponse {
|
||||||
|
data: Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LocationUpdateResponse = LocationCreateResponse;
|
||||||
|
|
||||||
|
export interface LocationDeleteResponse {
|
||||||
|
message: string;
|
||||||
|
data: null;
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ import Departments from '../modules/rh/components/departments/Departments.vue';
|
|||||||
import Companies from '../modules/catalog/components/companies/Companies.vue';
|
import Companies from '../modules/catalog/components/companies/Companies.vue';
|
||||||
import '../modules/catalog/components/suppliers/Suppliers.vue';
|
import '../modules/catalog/components/suppliers/Suppliers.vue';
|
||||||
import Suppliers from '../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 Purchases from '../modules/purchases/components/Purchases.vue';
|
import Purchases from '../modules/purchases/components/Purchases.vue';
|
||||||
import PurchaseDetails from '../modules/purchases/components/PurchaseDetails.vue';
|
import PurchaseDetails from '../modules/purchases/components/PurchaseDetails.vue';
|
||||||
import PurchaseForm from '../modules/purchases/components/PurchaseForm.vue';
|
import PurchaseForm from '../modules/purchases/components/PurchaseForm.vue';
|
||||||
@ -195,6 +196,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'locations',
|
||||||
|
name: 'Locations',
|
||||||
|
component: Locations,
|
||||||
|
meta: {
|
||||||
|
title: 'Ubicaciones',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'model-documents',
|
path: 'model-documents',
|
||||||
name: 'ModelDocuments',
|
name: 'ModelDocuments',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user