Merge branch 'sidebar-fix' into 'master'

Latest and greatest Dashboard project (at the moment: 2024.02.15)

See merge request jcruzbaasworkspace/enruta/webeta!1
This commit is contained in:
Josepablo Cruz Baas
2024-02-16 01:22:49 +00:00
86 changed files with 9245 additions and 149 deletions

376
package-lock.json generated
View File

@@ -9,11 +9,17 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"axios": "^1.6.2", "axios": "^1.6.2",
"chart.js": "^4.4.1",
"html2pdf.js": "^0.10.1",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"qalendar": "^3.7.0",
"sass": "^1.69.5", "sass": "^1.69.5",
"sweetalert2": "^11.10.1",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-chartjs": "^5.3.0",
"vue-multiselect": "^3.0.0-beta.3", "vue-multiselect": "^3.0.0-beta.3",
"vue-router": "^4.2.5" "vue-router": "^4.2.5",
"vue3-google-map": "^0.18.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",
@@ -31,6 +37,17 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz",
"integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.18.20", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
@@ -383,11 +400,93 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
"integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
"integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz",
"integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
"integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/vue-fontawesome": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.5.tgz",
"integrity": "sha512-isZZ4+utQH9qg9cWxWYHQ9GwI3r5FeO7GnmzKYV+gbjxcptQhh+F99iZXi1Y9AvFUEgy8kRpAdvDlbb3drWFrw==",
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
"vue": ">= 3.0.0 < 4"
}
},
"node_modules/@googlemaps/js-api-loader": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.2.tgz",
"integrity": "sha512-psGw5u0QM6humao48Hn4lrChOM2/rA43ZCm3tKK9qQsEj1/VzqkCqnvGfEOshDbBQflydfaRovbKwZMF4AyqbA==",
"dependencies": {
"fast-deep-equal": "^3.1.3"
}
},
"node_modules/@googlemaps/markerclusterer": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.1.tgz",
"integrity": "sha512-TLASLyWPoJiTdbuPtqIzQS8t/+JIvd+whoNkeSxWpP2eIoTscgsWt72Is3Cu5I68GuX3qn+oUSxqagFYc1O+wg==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"supercluster": "^8.0.1"
}
},
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15", "version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
}, },
"node_modules/@kurkle/color": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"optional": true
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.1.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.1.tgz",
@@ -525,6 +624,17 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
}, },
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/axios": { "node_modules/axios": {
"version": "1.6.2", "version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
@@ -535,6 +645,14 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -554,6 +672,53 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/canvg": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/canvg/node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"optional": true
},
"node_modules/chart.js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz",
"integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=7"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -591,6 +756,25 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/core-js": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz",
"integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==",
"hasInstallScript": true,
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
@@ -604,6 +788,17 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/dompurify": {
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz",
"integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==",
"optional": true
},
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.18.20", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
@@ -646,6 +841,16 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
}, },
"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=="
},
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -713,6 +918,28 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/html2pdf.js": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.10.1.tgz",
"integrity": "sha512-3onwwhOWsZfNjIZwV6YIJ6FVhXk+X9YxHSqzeS6hup+1dGi2DHI+zZYUJ+iFnvtaYcjlhyrILL1fvRCUOa8Fcg==",
"dependencies": {
"es6-promise": "^4.2.5",
"html2canvas": "^1.0.0",
"jspdf": "^2.3.1"
}
},
"node_modules/immutable": { "node_modules/immutable": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz",
@@ -756,6 +983,28 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/jspdf": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz",
"integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==",
"dependencies": {
"@babel/runtime": "^7.14.0",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.4.8"
},
"optionalDependencies": {
"canvg": "^3.0.6",
"core-js": "^3.6.0",
"dompurify": "^2.2.0",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/kdbush": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.5", "version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
@@ -811,6 +1060,17 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/perfect-scrollbar": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.5.tgz",
"integrity": "sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g=="
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"optional": true
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -909,6 +1169,30 @@
"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",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
}, },
"node_modules/qalendar": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/qalendar/-/qalendar-3.7.0.tgz",
"integrity": "sha512-V85kX4D+aKhbpvsm/4iYRXNvLFlbxUarJiJylkfH3bNOXt3mtkrToRKCAZt4Io70zcr94OA/ERpT1BlA9MMW+A==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-regular-svg-icons": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/vue-fontawesome": "^3.0.3",
"perfect-scrollbar": "^1.5.5"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -920,6 +1204,20 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.29.4", "version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
@@ -960,6 +1258,49 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/stackblur-canvas": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz",
"integrity": "sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/supercluster": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
"dependencies": {
"kdbush": "^4.0.2"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/sweetalert2": {
"version": "11.10.1",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.10.1.tgz",
"integrity": "sha512-qu145oBuFfjYr5yZW9OSdG6YmRxDf8CnkgT/sXMfrXGe+asFy2imC2vlaLQ/L/naZ/JZna1MPAY56G4qYM0VUQ==",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/limonte"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -971,6 +1312,14 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
@@ -1046,6 +1395,15 @@
} }
} }
}, },
"node_modules/vue-chartjs": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.0.tgz",
"integrity": "sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==",
"peerDependencies": {
"chart.js": "^4.1.1",
"vue": "^3.0.0-0 || ^2.7.0"
}
},
"node_modules/vue-multiselect": { "node_modules/vue-multiselect": {
"version": "3.0.0-beta.3", "version": "3.0.0-beta.3",
"resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.0.0-beta.3.tgz", "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.0.0-beta.3.tgz",
@@ -1068,6 +1426,22 @@
"peerDependencies": { "peerDependencies": {
"vue": "^3.2.0" "vue": "^3.2.0"
} }
},
"node_modules/vue3-google-map": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/vue3-google-map/-/vue3-google-map-0.18.0.tgz",
"integrity": "sha512-dhDlXK5XxKxH3Mj6n6C7y99M5LRwPDHlgGWgfLkmzVMpwlxBhaSxDhSTaduAFUjeHVusEfYdPxJSSP7yPJr8sg==",
"dependencies": {
"@googlemaps/js-api-loader": "^1.16.2",
"@googlemaps/markerclusterer": "^2.4.0",
"fast-deep-equal": "^3.1.3"
},
"engines": {
"node": ">=16.11.0"
},
"peerDependencies": {
"vue": "^3"
}
} }
} }
} }

View File

@@ -9,11 +9,17 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.6.2", "axios": "^1.6.2",
"chart.js": "^4.4.1",
"html2pdf.js": "^0.10.1",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"qalendar": "^3.7.0",
"sass": "^1.69.5", "sass": "^1.69.5",
"sweetalert2": "^11.10.1",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-chartjs": "^5.3.0",
"vue-multiselect": "^3.0.0-beta.3", "vue-multiselect": "^3.0.0-beta.3",
"vue-router": "^4.2.5" "vue-router": "^4.2.5",
"vue3-google-map": "^0.18.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",

View File

@@ -11,6 +11,9 @@ body {
background-color: #fdfcfc !important; background-color: #fdfcfc !important;
} }
.radius-sm {
border-radius: 8px !important;
}
.radius-1 { .radius-1 {
border-radius: 1rem !important; border-radius: 1rem !important;
} }
@@ -64,6 +67,12 @@ body {
transition: background-color 300ms ease; transition: background-color 300ms ease;
} }
.error-msg {
color: red;
font-size: 12px;
font-weight: 300;
}
.btn-primary-sm { .btn-primary-sm {
background-color: #FBBA33; background-color: #FBBA33;
padding: 8px 16px; padding: 8px 16px;
@@ -173,6 +182,14 @@ td {
border: none; border: none;
} }
.btn-row {
margin-top: 2rem;
display: flex;
flex-direction: row;
justify-content: end;
gap: 1rem;
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
th { th {
font-size: 13px; font-size: 13px;
@@ -182,6 +199,9 @@ td {
font-size: 12px; font-size: 12px;
font-weight: 300; font-weight: 300;
} }
.clear-md {
display: none;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -189,6 +209,11 @@ td {
padding: 16px 16px; padding: 16px 16px;
} }
.card-fixed {
padding: 16px 16px;
}
.card-info h2{ .card-info h2{
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 700; font-weight: 700;
@@ -206,7 +231,7 @@ td {
.btn-primary-lg { .btn-primary-lg {
padding: 8px 15px; padding: 8px 15px;
border: none; border: none;
border-radius: 13px; border-radius: 8px;
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
} }
@@ -214,9 +239,13 @@ td {
padding: 8px 12px; padding: 8px 12px;
font-size: 14px; font-size: 14px;
border: none; border: none;
border-radius: 13px; border-radius: 8px;
font-weight: 700; font-weight: 700;
} }
.clear-sm {
display: none !important;
}
} }
@media (max-width: 568px) { @media (max-width: 568px) {
@@ -235,4 +264,8 @@ td {
font-size: 12px; font-size: 12px;
font-weight: 300; font-weight: 300;
} }
.clear-xsm {
display: none !important;
}
} }

View File

@@ -0,0 +1,99 @@
<script setup>
import { onMounted } from 'vue';
import useAttachments from '../composables/useAttachments';
import Spiner from './ui/Spiner.vue';
import { useLoadsStore } from '../stores/loads';
const loadStore = useLoadsStore();
const { getAttachmentLoad, loading, attachments } = useAttachments();
onMounted(() => {
console.log('se ejcyta attach');
getAttachmentLoad();
})
const clearLoad = () => {
loadStore.openAttachmentsModal = false;
loadStore.currentLoad = null;
}
</script>
<template>
<div class="modal fade" id="attachmentModal" tabindex="-1" role="dialog" aria-labelledby="attachmentModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">Evidencias adjuntas</h2>
<button
id="btnCloseAttachmentModal"
type="button"
class="close bg-white"
data-dismiss="modal"
@click="clearLoad"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<Spiner v-if="loading"/>
<div v-else>
<div v-if="!attachments || attachments.total == 0" class="card-body">
<p class="empty">No hay evidencias subidas</p>
</div>
<div v-else class="card-body">
<div class="attachment" v-for="data in attachments.data">
<p v-if="data.type == 'Loading'">Evidencia de carga</p>
<p v-else>Evidencia de descarga</p>
<img
:src="`https://api.etaviaporte.com/api/v1/public-load-attachments/download/${data._id}`"
:alt="data.type"
/>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-dark"
@click="clearLoad"
data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.attachment {
width: 100%;
/* border: 1px #e9e9e9 solid; */
display: flex;
flex-direction: column;
padding: 20px;
margin-bottom: 20px;
border-radius: 13px;
align-content: center;
align-items: center;
box-shadow: 3px 5px 10px 5px rgba(0,0,0,0.10);
-webkit-box-shadow: 3px 5px 10px 5px rgba(0,0,0,0.10);
-moz-box-shadow: 3px 5px 10px 5px rgba(0,0,0,0.10);
}
.attachment p {
font-size: 1rem;
font-weight: 900;
color: black;
}
.attachment img {
width: 50%;
justify-content: center;
align-items: center;
margin: 0 auto;
align-content: center;
}
</style>

View File

@@ -0,0 +1,86 @@
<script setup>
import { computed, onMounted, ref } from 'vue';
import { Bar } from 'vue-chartjs'
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, LinearScale, CategoryScale, ArcElement } from 'chart.js'
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement)
const props = defineProps({
label: {
type: String,
required: true
},
data: {
type: Array,
required: true
},
dataModel: {
type: Array,
},
targetFind: {
type: String
},
targetLabel: {
type: String
}
})
const chartData = ref(null);
const dataMap = ref([]);
onMounted(() => {
props.data.forEach(item => {
const index = dataMap.value.findIndex((e) => e.label === item);
if(index === -1) {
if(props.dataModel) {
const itemModel = props.dataModel.find((e) => e[props.targetFind] === item);
dataMap.value.push({
label: (props.targetLabel) ? itemModel[props.targetLabel] : item,
data: 1,
...itemModel
})
} else {
dataMap.value.push({
label: item,
data: 1,
color: 'green'
});
}
} else {
dataMap.value[index].data += 1;
}
});
chartData.value = {
labels: dataMap.value.map((e) => (e.label.length > 12) ? e.label.substring(0, 11) : e.label),
position: 'bottom',
datasets: [{
label: props.label,
data: dataMap.value.map((e) => e.data),
backgroundColor: dataMap.value.map((e) => e.color),
}],
}
})
const id = computed(() => {
return `my-chart-${props.label}`
})
const chartOptions = {
responsive: true,
}
</script>
<template>
<Bar
:id=id
v-if="chartData"
:options="chartOptions"
:data="chartData"
/>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,126 @@
<script setup>
import Swal from 'sweetalert2';
import { useCompanyStore } from '../stores/company';
const props = defineProps({
budget: {
type: Object,
required: true
}
})
defineEmits(['set-budget'])
const companyStore = useCompanyStore();
const handleDeleteBudget = async() => {
Swal.fire({
title: 'Eliminar Presupuesto!',
text: '¿Estás seguro de eliminar este presupuesto?',
icon: 'warning',
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonText: 'Eliminar',
cancelButtonText: 'Cancelar',
}).then(async(result) => {
if(result.isConfirmed) {
Swal.fire({
title: 'Por favor espere!',
html: 'Eliminando presupuesto...',// add html attribute if you want or remove
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading()
},
});
const resp = await companyStore.deleteBudgetCompany(props.budget._id);
Swal.close();
if(resp !== null) {
Swal.fire({
title: "Presupuesto eliminado!",
text: "Tu presupuesto ha sido eliminado exitosamente.",
icon: "success"
});
} else {
Swal.fire({
title: "No eliminado!",
text: "Tu presupuesto no se pudo eliminar, intente más tarde.",
icon: "error"
});
}
}
});
}
</script>
<template>
<div class="card-fixed card-budget">
<div>
<div class="row">
<div class="col-lg-6 col-md-6 col-sm-12">
<p><span>Cliente:</span> {{budget.client}}</p>
<p v-if="budget.material"><span>Material:</span> {{budget.material.name}}</p>
<p><span>Origen:</span> {{budget.origin}}</p>
<p><span>Destino:</span> {{budget.destination}}</p>
<p><span>Tipo de camión:</span> {{budget.truck_type}}</p>
<p><span>Total KM recorridos:</span> {{budget.total_km_travel}}</p>
</div>
<div class="col-lg-6 col-md-6 col-sm-12">
<p><span>Total Litros De Diesel Consumidos: </span> {{parseFloat( budget.total_fuel_consumed).toFixed(2)}}</p>
<p><span>Total Costo Del Diesel:</span> {{"$" + parseFloat( budget.total_cost_fuel).toFixed(2)}}</p>
<p><span>Total Antes De Iva:</span> ${{budget.total_before_tax}}</p>
<p><span>Total Utilidad Por Kilometro:</span> {{"$" + parseFloat( budget.total_utility_per_km).toFixed(2)}}</p>
<p><span>Total Utilidad:</span> {{"$" + parseFloat( budget.total_profit).toFixed(2)}}</p>
<!-- <p>{{ $t('CALCULATOR.PROFIT_PERCENTAGE') }}: {{budget.profit_percentage}}%</p> -->
<p><span>Porcentaje De Utilidad:</span> {{parseFloat(budget.profit_percentage).toFixed(2) + "%"}}</p>
</div>
</div>
</div>
<div class="card-footer">
<button
class="btn btn-danger radius-sm"
@click="handleDeleteBudget"
>
<i class="fa-solid fa-trash" /> <span class="clear-xsm">Eliminar</span>
</button>
<button
class="btn-primary-sm radius-sm"
@click="$emit('set-budget', {budget: budget, print: false})"
data-toggle="modal" data-target="#budgetModal"
>
<i class="fa-solid fa-pen-to-square" /> <span class="clear-xsm">Editar</span>
</button>
<button
type="button"
class="btn btn-dark"
@click="$emit('set-budget', {budget: budget, print: true})"
data-toggle="modal" data-target="#budgetModal"
>
<i class="fa-solid fa-print" /> <span class="clear-xsm">Imprimir</span>
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.card-budget {
flex-direction: column;
}
p {
font-size: 1rem;
font-weight: normal;
}
p span {
font-weight: bold;
}
.card-footer {
display: flex;
justify-content: end;
gap: 1rem;
}
</style>

View File

@@ -30,12 +30,12 @@
<p><span>Información general de la empresa: </span>{{company.company_description}}</p> <p><span>Información general de la empresa: </span>{{company.company_description}}</p>
</div> </div>
</div> </div>
<!-- <div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<RouterLink <RouterLink
class="btn-primary-sm" class="btn-primary-sm"
:to="{name: 'empresa', params: {id: company._id}}" :to="{name: 'public-users', params: {id: company._id}}"
>Ver perfil</RouterLink> >Ver perfil</RouterLink>
</div> --> </div>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,35 @@
<script setup>
defineProps({
text: {
type: String,
required: true
}
})
</script>
<template>
<div class="card-info card-empty">
<img src="/images/logo.png" alt="logo" class="img-empty">
<p class="message">{{ text }}</p>
</div>
</template>
<style scoped>
.card-empty {
flex-direction: column;
justify-content: center;
align-items: center;
}
.img-empty {
width: 200px;
}
.message {
margin-top: 2rem;
font-size: 1.4rem;
color: #323032;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,86 @@
<script setup>
import { ref } from 'vue';
defineProps({
faq: {
type: Object,
required: true
}
})
const open = ref(false);
const toggle = () => {
open.value = !open.value;
}
</script>
<template>
<div class="card-faq">
<div class="question-box" @click="toggle">
<h3 class="question">{{ faq.question }}</h3>
<i v-if="!open" class="fa-solid fa-chevron-down icon-indicator"></i>
<i v-else class="fa-solid fa-chevron-up icon-indicator"></i>
</div>
<div v-if="open">
<p
v-if="faq.answer"
class="answer">{{ faq.answer }}</p>
<ol>
<li class="step" v-for="step in faq.steps" v-html="step"></li>
</ol>
<p
class="asnwer"
v-if="faq.notes"
>
{{ faq.notes }}
</p>
</div>
</div>
</template>
<style scoped>
.card-faq {
width: 100%;
padding: 12px 20px;
background-color: white;
/* background-color: rgb(184, 236, 234); */
margin-bottom: 15px;
filter: drop-shadow(0px 4px 4px rgba(0, 255, 255,0.3));
border-radius: 13px;
}
.question-box {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.card-faq:hover {
background-color: aqua;
}
.icon-indicator {
font-size: 20px;
color: #FBBA33;
}
.question {
font-size: 1.3rem;
font-weight: 500;
color: #323030;
}
.answer {
margin-top: 1rem;
font-size: 1.1rem;
font-weight: normal;
color: #323030;
}
.step {
margin-top: 0.5rem;
font-size: 1.1rem;
font-weight: normal;
color: #323030;
}
</style>

250
src/components/CardLoad.vue Normal file
View File

@@ -0,0 +1,250 @@
<script setup>
import { useRouter } from 'vue-router';
import { getDateMonthDay } from '../helpers/date_formats';
import { getStatusPublished } from '../helpers/status';
import { getStatusLoad } from '../helpers/status';
import { useLoadsStore } from '../stores/loads';
import Swal from 'sweetalert2'
import { useAuthStore } from '../stores/auth';
const router = useRouter();
const loadsStore = useLoadsStore();
const authStore = useAuthStore();
const props = defineProps({
load: {
type: Object,
required: true,
},
readOnly: {
type: Boolean,
required: false,
default: false
}
});
defineEmits(['set-load'])
const openAttachmentsModal = () => {
loadsStore.currentLoad = props.load;
loadsStore.openAttachmentsModal = true;
}
const handleDeleteLoad = async() => {
Swal.fire({
title: 'Eliminar carga!',
text: '¿Estás seguro de eliminar esta carga?',
icon: 'warning',
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonText: 'Eliminar',
cancelButtonText: 'Cancelar',
}).then(async(result) => {
if(result.isConfirmed) {
Swal.fire({
title: 'Por favor espere!',
html: 'Eliminando carga...',// add html attribute if you want or remove
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading()
},
});
const resp = await loadsStore.deleteLoad(props.load._id);
if(resp != null) {
loadsStore.loads = loadsStore.loads.filter(load => load._id !== props.load._id);
Swal.fire({
title: "Carga eliminada!",
text: "Tu carga ha sido eliminada exitosamente.",
icon: "success"
});
} else {
Swal.fire({
title: "No eliminado!",
text: "Tu carga no se pudo eliminar, intente más tarde.",
icon: "error"
});
}
}
});
}
const openEditModal = () => {
loadsStore.currentLoad = props.load
loadsStore.openModalEdit = true;
}
const openProposalsModal = () => {
loadsStore.currentLoad = props.load
loadsStore.openProposalsModal = true;
}
const handleTracking = () => {
let code = props.load.shipment_code;
router.push({
name: 'tracking-load',
params: {code}
});
}
</script>
<template>
<div class="card-fixed card-load mt-4">
<div class="row">
<div class="col-lg-6 col-sm-12">
<p>
<span>Origen: </span>
<template v-if="load.origin.company_name"> {{ load.origin.company_name }}, </template>
<template v-if="load.origin.street_address1">{{ load.origin.street_address1 }}, </template>
<template v-if="load.origin.city">{{ load.origin.city }}, </template>
<template v-if="load.origin.state">{{ load.origin.state }}, </template>
<template v-if="load.origin.country">{{ load.origin.country }}, </template>
<template v-if="load.origin.zipcode">{{ load.origin.zipcode }} </template>
</p>
<p>
<span>Destino: </span>
<template v-if="load.destination.company_name"> {{ load.destination.company_name }}, </template>
<template v-if="load.destination.street_address1">{{ load.destination.street_address1 }}, </template>
<template v-if="load.destination.city">{{ load.destination.city }}, </template>
<template v-if="load.destination.state">{{ load.destination.state }}, </template>
<template v-if="load.destination.country">{{ load.destination.country }}, </template>
<template v-if="load.destination.zipcode">{{ load.destination.zipcode }} </template>
</p>
</div>
<div class="col-lg-6 col-sm-12" v-if="authStore.user?.permissions.includes('role_shipper')">
<p><span>Status de la publicación:</span> <span>{{ getStatusPublished(load) }}</span></p>
<p :style="{color: getStatusLoad(load).color}"><span>Status de la carga:</span> <span>{{ getStatusLoad(load).status }}</span></p>
</div>
</div>
<div class="divider"></div>
<br>
<div class="row">
<div class="col-lg-4 col-sm-12">
<p><span>Tipo de camión: </span> {{ load.truck_type }}</p>
<p><span>Peso: </span> {{ load.weight }} KG</p>
<p><span>Fecha de carga: </span> {{ getDateMonthDay(load.est_loading_date) }}</p>
</div>
<div class="col-lg-4 col-sm-12">
<p><span>Producto: </span> {{ load?.product?.name }}</p>
<p><span>Costo real: </span> {{ load.actual_cost }}</p>
<p><span>Fecha de descarga: </span> {{getDateMonthDay(load.est_unloading_date) }}</p>
</div>
<div class="col-lg-4 col-sm-12">
<p><span>Segmento: </span> {{ load.categories?.map((e) => e.name).join(', ') }}</p>
<p><span>Código de carga: </span> {{ load.shipment_code }}
<span v-if="load.load_status !== 'Draft' && !readOnly" class="tracking-icon" @click="handleTracking">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-geo-alt-fill" viewBox="0 0 16 16">
<path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"></path>
</svg>
</span>
</p>
<p><span>Publicación hecha por: </span> {{ load.posted_by_name }}</p>
</div>
</div>
<p v-if="load.notes"><span>Notas adicionales:</span></p>
<div v-if="load.notes" class="box-note">
{{ load.notes }}
</div>
<div class="btn-row" v-if="!readOnly && authStore.user?.permissions.includes('role_shipper')">
<button
class="btn-primary-sm bg-dark"
@click="handleDeleteLoad"
><i class="fa-solid fa-ban clear-sm"></i> Cancelar</button>
<button v-if="load.status !== 'Draft' && load.load_status !== 'Published' && load.load_status !== 'Loading'"
type="button"
data-toggle="modal" data-target="#attachmentModal"
class="btn-primary-sm"
@click="openAttachmentsModal"
>
<i class="fa-solid fa-image"></i>
Evidencias
</button>
<button
v-if="load.load_status !== 'Delivered'"
class="btn-primary-sm"
data-toggle="modal" data-target="#formLoadModal"
@click="openEditModal"
><i class="fa-solid fa-pen-to-square clear-sm"></i> Editar carga</button>
<button
v-if="load.status !== 'Draft'"
class="btn-primary-sm"
@click="openProposalsModal"
data-toggle="modal"
data-target="#proposalsModal"
>#{{ load.no_of_proposals }} Ofertas</button>
</div>
<div class="btn-row" v-if="!readOnly && authStore.user?.permissions.includes('role_carrier')">
<button
class="btn-primary-sm bg-dark"
data-toggle="modal"
data-target="#makeProposalModal"
@click="event => $emit('set-load')"
>Hacer oferta</button>
<button
class="btn-primary-sm"
@click=""
data-toggle="modal"
data-target="#proposalsModal"
><i class="fa-solid fa-phone clear-sm"></i> Llamar ahora</button>
</div>
</div>
</template>
<style scoped>
.card-load {
flex-direction: column;
width: 100% !important;
}
.tracking-icon {
cursor: pointer;
color: #f2a23f;
}
.tracking-icon svg{
height: 30px;
}
.tracking-icon:hover {
color: #ddb380;;
height: 150px;
}
.tracking-icon svg:hover{
height: 33px;
}
p {
font-size: 1rem;
font-weight: 400;
color: #323032;
}
p span {
color: #323032;
font-weight: 700;
}
.btn-row {
margin-top: 2rem;
display: flex;
flex-direction: row;
justify-content: end;
gap: 1rem;
}
.box-note {
padding: 12px 16px;
background-color: aqua;
border-radius: 13px;
}
@media (max-width: 768px) {
.btn-row {
gap: 0.2rem;
}
}
</style>

View File

@@ -0,0 +1,116 @@
<script setup>
import Swal from 'sweetalert2';
import { useCompanyStore } from '../stores/company';
const props = defineProps({
location: {
type: Object,
required: true
}
})
defineEmits(['set-location'])
const companyStore = useCompanyStore();
const handleDeleteLocation = async() => {
Swal.fire({
title: 'Eliminar Locación!',
text: '¿Estás seguro de eliminar este locación?',
icon: 'warning',
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonText: 'Eliminar',
cancelButtonText: 'Cancelar',
}).then(async(result) => {
if(result.isConfirmed) {
Swal.fire({
title: 'Por favor espere!',
html: 'Eliminando locación...',// add html attribute if you want or remove
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading()
},
});
const resp = await companyStore.deleteLocationCompany(props.location._id)
Swal.close();
if(resp != null) {
Swal.fire({
title: "Locación eliminado!",
text: "Tu locación ha sido eliminado exitosamente.",
icon: "success"
});
} else {
Swal.fire({
title: "No eliminado!",
text: "Tu locación no se pudo eliminar, intente más tarde.",
icon: "error"
});
}
}
});
}
</script>
<template>
<div class="card-fixed card-location">
<div>
<p><span>Nombre de la locación de carga:</span> {{location.branch_name}}</p>
<p><span>Dirección:</span> <template v-if="location.address">{{location.address}}, </template><template v-if="location.city">{{location.city}}, </template><template v-if="location.state">{{location.state}}</template></p>
<p><span>Teléfono:</span> {{location.phone}}</p>
<p><span>Tipos de camiones que se necesitan:</span> {{location.truck_type?.map((e) => e).join(', ')}}</p>
<p><span>Segmento:</span> {{location.categories.map((e) => e.name).join(', ')}}</p>
<p v-if="location.description"><span>Información adicional de la locación de carga:</span></p>
<div v-if="location.description" class="box-note mb-4">
{{ location.description }}
</div>
</div>
<div class="card-footer">
<button
class="btn btn-dark radius-sm"
@click="handleDeleteLocation"
>
<i class="fa-solid fa-trash" /> <span class="clear-xsm">Eliminar</span>
</button>
<button
class="btn-primary-sm radius-sm"
@click="$emit('set-location')"
data-toggle="modal" data-target="#locationFormModal"
>
<i class="fa-solid fa-pen-to-square" /> <span class="clear-xsm">Editar</span>
</button>
</div>
</div>
</template>
<style scoped>
.card-location {
flex-direction: column;
}
.card-footer {
display: flex;
justify-content: end;
gap: 1rem;
}
.box-note {
padding: 12px 16px;
background-color: aqua;
border-radius: 13px;
}
p {
font-size: 1rem;
font-weight: normal;
}
p span {
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,204 @@
<script setup>
import Swal from 'sweetalert2';
import { getDateMonthDay } from '../helpers/date_formats';
import { getStatusLoad } from '../helpers/status';
import { useCompanyStore } from '../stores/company';
const props = defineProps({
proposal: {
type: Object,
required: true,
}
})
const companyStore = useCompanyStore();
const handleWithdrawnProposal = async() => {
Swal.fire({
title: 'Retirar oferta!',
text: '¿Estás seguro de retirar esta oferta?',
icon: 'warning',
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonText: 'Si, Retirar',
cancelButtonText: 'No',
}).then(async(result) => {
if(result.isConfirmed) {
Swal.fire({
title: 'Por favor espere!',
html: 'Retirando oferta...',// add html attribute if you want or remove
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading()
},
});
let formData = {
is_withdrawn : true
}
let localData = {
vehicle: props.proposal.vehicle,
load: props.proposal.load
}
const resp = await companyStore.updatePropsalLoad(props.proposal._id, formData, localData);
Swal.close();
if(resp != null) {
Swal.fire({
title: "Oferta retirada!",
text: "Tu oferta ha sido retirada exitosamente.",
icon: "success"
});
} else {
Swal.fire({
title: "Oferta no retirada!",
text: "Tu oferta no se pudo retirar, intente más tarde.",
icon: "error"
});
}
}
});
}
defineEmits(['set-proposal']);
</script>
<template>
<div class="card-fixed card-load mt-4">
<br>
<div class="box-proposal">
<div class="">
<p v-if="proposal.vehicle"><span>Código:</span> {{proposal.vehicle.vehicle_code}}</p>
<p v-if="proposal.vehicle"><span>Segmento:</span> {{proposal._categories}}</p>
<p v-if="proposal.vehicle"><span>Tipo de transporte:</span> {{proposal.vehicle.truck_type}}</p>
<p v-if="proposal.vehicle"><span>Fecha de publicación:</span> {{ getDateMonthDay(proposal.vehicle.published_date) }}</p>
<p v-if="proposal.vehicle"><span>Fecha dispobible:</span> {{ getDateMonthDay(proposal.vehicle.available_date) }}</p>
<p v-if="proposal.vehicle"><span>Disponible en:</span> {{proposal.vehicle.city}}<template v-if="proposal.vehicle.state">, {{proposal.vehicle.state}}</template></p>
<p v-if="proposal.vehicle"><span>Destino:</span> {{proposal.vehicle.destino}}</p>
<p v-if="proposal.vehicle"><span>Placas remolque 1:</span> {{proposal.vehicle.trailer_plate_1}}</p>
<p v-if="proposal.vehicle"><span>Placas remolque 2:</span> {{proposal.vehicle.trailer_plate_2}}</p>
<p v-if="proposal.vehicle" :style="{color: getStatusLoad(proposal.load).color}"><span>Status de la carga:</span> {{ getStatusLoad(proposal.load).status }}</p>
</div>
<div class="">
<p v-if="proposal.load"> Código de carga:
<span
class="code-enruta"
@click="$emit('set-proposal', {proposal: proposal, modal: 'detail'})"
data-toggle="modal" data-target="#loadDetailModal"
>{{proposal.load.shipment_code}}</span></p>
<p v-if="proposal._driver">Operator: {{proposal._driver}}</p>
</div>
</div>
<div class="btn-row">
<!-- <button
class="btn-primary-sm"
data-toggle="modal" data-target="#editcompanymodal"
><i class="fa-solid fa-ban"></i> Retirar</button> -->
<div v-if="proposal.is_withdrawn" class="indicator-cancel">
<i class="fa-solid fa-ban"></i>
Retirado
</div>
<button v-else
type="button"
class="btn btn-danger radius-sm"
@click="handleWithdrawnProposal"
>
<i class="fa-solid fa-ban"></i>
Retirar
</button>
<button
class="btn-primary-sm radius-sm"
@click="$emit('set-proposal', {proposal: proposal, modal: 'edit'})"
data-toggle="modal" data-target="#makeProposalModal"
><i class="fa-solid fa-pen-to-square"></i> Editar</button>
</div>
</div>
</template>
<style scoped>
.card-load {
flex-direction: column;
width: 100% !important;
}
.box-proposal {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.code-enruta {
cursor: pointer;
color: rgb(107, 107, 227);
}
.code-enruta:hover{
color: rgb(75, 75, 228);
}
.tracking-icon {
cursor: pointer;
color: #f2a23f;
}
.tracking-icon svg{
height: 30px;
}
.tracking-icon:hover {
color: #ddb380;;
height: 150px;
}
.indicator-cancel {
width: 120px;
padding: 10px 12px;
background: #FFF;
/* border: 1px solid red; */
border-radius: 50px;
color: red;
}
.tracking-icon svg:hover{
height: 33px;
}
p {
font-size: 1rem;
font-weight: 400;
color: #323032;
}
p span {
color: #323032;
font-weight: 700;
}
.btn-row {
margin-top: 2rem;
display: flex;
flex-direction: row;
justify-content: end;
gap: 1rem;
}
.box-note {
padding: 8px 16px;
background-color: aqua;
border-radius: 13px;
}
@media (max-width: 768px) {
.box-proposal {
flex-direction: column-reverse;
}
.btn-row {
gap: 0.2rem;
}
}
</style>

133
src/components/CardUser.vue Normal file
View File

@@ -0,0 +1,133 @@
<script setup>
import Swal from 'sweetalert2';
import { getDateMonthDay } from '../helpers/date_formats';
import { useCompanyStore } from '../stores/company';
const props = defineProps({
user: {
type: Object,
required: true
},
readonly: {
type: Boolean,
required: false,
default: true
}
})
defineEmits(['set-user'])
const companyStore = useCompanyStore();
const handleDelete = async() => {
Swal.fire({
title: 'Eliminación de usuario!',
text: '¿Estás seguro de eliminar este usuario?',
icon: 'warning',
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonText: 'Eliminar',
cancelButtonText: 'Cancelar',
}).then(async(result) => {
if(result.isConfirmed) {
Swal.fire({
title: 'Por favor espere!',
html: 'Elimininando usuario...',// add html attribute if you want or remove
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading()
},
});
const resp = await companyStore.deleteUserCompany(props.user._id)
Swal.close()
if(resp === 'success') {
Swal.fire({
title: "Usuario eliminado!",
text: "El usuario ha sido eliminado exitosamente.",
icon: "success"
});
} else {
Swal.fire({
title: "No eliminado!",
text: "El usuario no se pudo eliminar, intente más tarde.",
icon: "error"
});
}
}
});
}
</script>
<template>
<div class="card-fixed flex-column">
<div class="row">
<div class="col-lg-6 col-sm-12">
<p><span>Nombre de usuario:</span> {{user.name}}</p>
<p v-if="user.employee_id"><span class="font-weight-bold mr-1">Número de registro:</span> {{user.employee_id}}</p>
<p><span>Teléfono 1: </span>{{user.phone}}</p>
<p><span>Teléfono 2: </span>{{user.phone2}}</p>
<p><span>Email: </span>{{user.email}}</p>
</div>
<div class="col-lg-6 col-sm-12" v-if="readonly">
<p><span>Segmento: </span>{{user.categories?.map((e) => e.name).join(', ')}}</p>
<p><span>Locaciones de carga por municipio: </span>{{user._user_city}}</p>
<p><span>Locaciones de carga por estado: </span>{{user._user_state}}</p>
<p v-if="user.company.truck_type"><span>Tipos de transporte que utiliza: </span> {{user._truck_type}}</p>
<p ><span>Información adicional del usuario: </span> {{user.user_description}}</p>
<p ><span>Miembro desde: </span>{{getDateMonthDay(user.createdAt)}}</p>
<p v-if="readonly" ><span>Tipo de afiliación: </span> {{user.company.membership}}</p>
<p><span>Rol del usuario: </span>{{user.job_role}}</p>
</div>
<div class="col-lg-6 col-sm-12" v-else>
<p><span>Segmento: </span>{{user.categories?.map((e) => e.name).join(', ')}}</p>
<p><span>Locaciones de carga por municipio: </span>{{user.user_city?.join(', ')}}</p>
<p><span>Locaciones de carga por estado: </span>{{user.user_state?.join(', ')}}</p>
<p v-if="user.truck_type"><span>Tipos de transporte que utiliza: </span> {{user.truck_type?.join(', ')}}</p>
<p ><span>Información adicional del usuario: </span> {{user.user_description}}</p>
<p ><span>Miembro desde: </span>{{getDateMonthDay(user.createdAt)}}</p>
<p v-if="readonly" ><span>Tipo de afiliación: </span> {{user.company.membership}}</p>
<p><span>Rol del usuario: </span>{{user.job_role}}</p>
</div>
</div>
<div class="btn-row" v-if="readonly === false">
<button
class="btn-primary-sm radius-sm"
data-toggle="modal"
data-target="#userModal"
@click="$emit('set-user')"
><i class="fa-solid fa-pen-to-square"></i> Editar</button>
<button
class="btn btn-dark radius-sm"
@click="handleDelete"
><i class="fa-solid fa-trash"></i> Eliminar</button>
</div>
</div>
</template>
<style scoped>
p {
font-size: 1rem;
font-weight: 400;
color: #323032;
}
p span {
font-weight: 700;
}
@media (max-width: 768px) {
p {
font-size: 0.9rem;
font-weight: 400;
color: #323032;
}
p span {
font-weight: 700;
}
}
</style>

View File

@@ -0,0 +1,158 @@
<script setup>
import Swal from 'sweetalert2';
import { getDateMonthDayEs } from '../helpers/date_formats';
import { useVehiclesStore } from '../stores/vehicles';
const props = defineProps({
vehicle: {
type: Object,
required: true
}
})
defineEmits(['set-vehicle']);
const vehicleStore = useVehiclesStore();
const handleDeleteVehicle = async() => {
Swal.fire({
title: `Eliminar vehiculo "${props.vehicle.vehicle_code}""`,
text: '¿Estás seguro de eliminar este vehiculo?',
icon: 'warning',
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonText: 'Eliminar',
cancelButtonText: 'Cancelar',
}).then(async(result) => {
if(result.isConfirmed) {
Swal.fire({
title: 'Por favor espere!',
html: 'Eliminando vehiculo...',// add html attribute if you want or remove
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading()
},
});
const resp = await vehicleStore.deleteVehicleCompany(props.vehicle._id);
Swal.close();
if(resp != null) {
Swal.fire({
title: "vehiculo eliminado!",
text: "Tu vehiculo ha sido eliminado exitosamente.",
icon: "success"
});
} else {
Swal.fire({
title: "Error!",
text: "Tu vehiculo no se pudo eliminar, intente más tarde.",
icon: "error"
});
}
}
});
}
</script>
<template>
<div class="card-fixed card-vehicle">
<div class="row my-2">
<div class="col-lg-6">
<p>Código: <span>{{ vehicle.vehicle_code }}</span></p>
<p>Tipo de transporte: <span>{{ vehicle.truck_type }}</span></p>
<p>Número de Serie: <span>{{ vehicle.vehicle_number }}</span></p>
<p>Segmento: <span>{{vehicle.categories?.map((e) => e.name).join(', ')}}</span></p>
<p>Conductor:
<span>
<span v-if="vehicle?.driver">{{ vehicle?.driver?.first_name }} {{ vehicle?.driver?.last_name }} </span>
<span v-else>No asignado </span>
<i
class="fa-solid fa-pen-to-square icon-btn"
data-toggle="modal"
data-target="#editDriverVehicle"
@click="$emit('set-vehicle', {vehicle: vehicle, modal: 'driver'})">
</i>
</span>
</p>
<p v-if="vehicle.is_available">Disponible en: <span>{{ vehicle.destino }}</span></p>
</div>
<div class="col-lg-6">
<p>Placas Tracto Camión: <span>{{ vehicle.circulation_serial_number }}</span></p>
<p>Placas Remolque 1: <span>{{ vehicle.trailer_plate_1 }}</span></p>
<p>Placas Remolque 2: <span>{{ vehicle.trailer_plate_2 }}</span></p>
<p>Base de carga: <span>{{ vehicle.city }}, {{ vehicle.state }}</span></p>
<p>Status:
<span>{{ vehicle.is_available ? 'Disponible' : 'Reservado'}}
<i
class="fa-solid fa-pen-to-square icon-btn"
data-toggle="modal"
data-target="#editStatusVehicle"
@click="$emit('set-vehicle', {vehicle: vehicle, modal: 'status'})">
</i>
</span>
</p>
<p v-if="vehicle.is_available">Fecha Disponible: <span>{{ getDateMonthDayEs(vehicle.available_date, false) }}</span></p>
</div>
</div>
<p v-if="vehicle.notes">Información Adicional del Transporte:</p>
<div v-if="vehicle.notes" class="box-note mb-4">
{{ vehicle.notes }}
</div>
<div class="card-footer">
<button
class="btn btn-dark radius-sm"
@click="handleDeleteVehicle"
>
<i class="fa-solid fa-trash" /> <span class="clear-xsm">Eliminar</span>
</button>
<button
class="btn-primary-sm radius-sm"
@click="$emit('set-vehicle', {vehicle: vehicle, modal: 'form'})"
data-toggle="modal"
data-target="#createVehicleModal"
>
<i class="fa-solid fa-pen-to-square" /> <span class="clear-xsm">Editar</span>
</button>
</div>
</div>
</template>
<style scoped>
.card-vehicle {
flex-direction: column;
width: 100% !important;
}
.card-footer {
display: flex;
justify-content: end;
gap: 1rem;
}
.icon-btn {
cursor: pointer;
font-size: 20px;
margin-left: 8px;
color: #FBBA33;
}
p {
font-size: 1rem;
font-weight: 400;
color: #323032;
font-weight: 700;
}
p span {
color: #323032;
font-weight: normal;
}
.box-note {
padding: 12px 16px;
background-color: aqua;
border-radius: 13px;
}
</style>

View File

@@ -0,0 +1,52 @@
<script setup>
import { Doughnut } from 'vue-chartjs'
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, LinearScale, CategoryScale, ArcElement } from 'chart.js'
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement)
const chartData = {
labels: [ 'Entregadas - 10', 'Descargando - 8', 'En Transito - 8', 'Cargando - 8', 'Publicadas - 1' ],
position: 'bottom',
datasets: [{
data: [10, 8, 8, 8, 1],
backgroundColor: [
'Blue',
'#428502',
'#ffd22b',
'#F44336',
'#000000',
],
}],
}
const chartOptions = {
responsive: true,
}
</script>
<template>
<div class="card-fixed card-dashboard">
<h3>Cargas activas</h3>
<Doughnut
id="my-chart-loads"
:options="chartOptions"
:data="chartData"
/>
</div>
</template>
<style scoped>
.card-dashboard {
width: 33%;
display: flex;
flex-direction: column;
}
@media (max-width: 768px) {
.card-dashboard {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,66 @@
<script setup>
import {Doughnut } from 'vue-chartjs'
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, LinearScale, CategoryScale, ArcElement } from 'chart.js'
import { onMounted, ref } from 'vue';
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement)
import segments from '../data/segments.json';
const segmentsFine = ['Agricola', 'Cemento', 'Intermoadal'];
const segmentsMoreUsed = ref([]);
const chartDataSegments = ref(null);
const loading = ref(false);
const segmentsMap = () => {
loading.value = true;
segmentsFine.forEach(element => {
const seg = segments.find((e) => e.name === element);
segmentsMoreUsed.value.push(seg);
});
chartDataSegments.value = {
labels: segmentsMoreUsed.value.map(e => e.name),
position: 'bottom',
datasets: [{
data: [4, 3, 1],
backgroundColor: segmentsMoreUsed.value.map(e => e.color),
}],
}
loading.value = false
}
onMounted(() => {
segmentsMap()
})
const chartOptions = {
responsive: true,
}
</script>
<template>
<div class="card-fixed card-dashboard">
<h3>Cargas activas</h3>
<Doughnut
v-if="chartDataSegments"
id="my-chart-segments"
:options="chartOptions"
:data="chartDataSegments"
/>
</div>
</template>
<style scoped>
.card-dashboard {
width: 33%;
display: flex;
flex-direction: column;
}
@media (max-width: 768px) {
.card-dashboard {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,603 @@
<script setup>
import { computed, onMounted, reactive, ref } from 'vue';
import CustomInput from './ui/CustomInput.vue';
import Products from './ui/Products.vue';
import TruckTypes from './ui/TruckTypes.vue';
import { useAuthStore } from '../stores/auth';
import { useCompanyStore } from '../stores/company';
import Swal from 'sweetalert2';
import Spiner from './ui/Spiner.vue';
import Cities from './ui/Cities.vue';
import html2pdf from 'html2pdf.js';
const props = defineProps({
budget: {
type: Object,
required: false
},
isPrint: {
type: Boolean,
default: false
}
})
const authStore = useAuthStore();
const companyStore = useCompanyStore();
const loading = ref(false);
onMounted(() => {
console.log(props.budget);
if(props.budget) {
budgetForm.budget_id = props.budget._id;
budgetForm.client = props.budget.client;
budgetForm.material = props.material?._id;
budgetForm.origin = {
city_name: props.budget.origin
};
budgetForm.destination = {
city_name: props.budget.destination
};
budgetForm.budgetproduct = {
name: props.budget.material?.name
};
budgetForm.budgetTrucktypes = {
meta_value: props.budget.truck_type
}
budgetForm.num_tons = props.budget.num_tons;
budgetForm.price_per_ton = props.budget.price_per_ton;
budgetForm.tonnage = props.budget.tonnage;
budgetForm.pickup_distance = props.budget.pickup_distance;
budgetForm.delivery_distance = props.budget.delivery_distance;
budgetForm.warehouse_distance = props.budget.warehouse_distance;
budgetForm.cost_per_liter = props.budget.cost_per_liter;
budgetForm.fuel_price_per_liter = props.budget.fuel_price_per_liter;
budgetForm.other_fuel_expenses = props.budget.other_fuel_expenses;
budgetForm.driver_salary = props.budget.driver_salary;
budgetForm.accomadation_allowance = props.budget.accomadation_allowance;
budgetForm.other_administrative_expenses = props.budget.other_administrative_expenses;
budgetForm.total_administrative_expenses = props.budget.total_administrative_expenses;
budgetForm.total_before_tax = props.budget.total_before_tax;
budgetForm.total_utility_per_km = props.budget.total_utility_per_km;
budgetForm.total_profit = props.budget.total_profit;
budgetForm.profit_percentage = props.budget.profit_percentage;
} else {
Object.assign(budgetForm, initalState);
}
})
defineEmits(['reset-budget']);
const initalState = {
client: '',
material: '',
origin: '',
destination: '',
num_tons: 0.0,
price_per_ton: 0.0,
tonnage: 0.0,
pickup_distance: 0.0,
delivery_distance: 0.0,
warehouse_distance: 0.0,
cost_per_liter: 0.0,
fuel_price_per_liter: 0.0,
other_fuel_expenses: 0.0,
driver_salary: 0.0,
accomadation_allowance: 0.0,
other_administrative_expenses: 0.0,
total_administrative_expenses: 0.0,
total_before_tax: 0.0,
total_utility_per_km: 0.0,
total_profit: 0.0,
profit_percentage: 0.0,
total_travel: 0.0,
total_fuel: 0.0,
total_fuel_cost: 0.0,
budgetproduct:[],
budgetTrucktypes:[],
}
const budgetForm = reactive({
...initalState
})
const pdfContent = ref(null);
const errors = ref({
client: null
})
// const total = computed(() => {
// });
const totalTravel = computed(() => {
budgetForm.total_travel = budgetForm.warehouse_distance * 1 + budgetForm.delivery_distance * 1 + budgetForm.pickup_distance * 1;
budgetForm.total_travel = isNaN(budgetForm.total_travel) ? 0.0 : budgetForm.total_travel;
return budgetForm.total_travel;
});
const totalFuel = computed(() => {
budgetForm.total_fuel = isNaN(budgetForm.total_travel / budgetForm.cost_per_liter) ? 0.0 : (budgetForm.total_travel / budgetForm.cost_per_liter);
return isNaN(budgetForm.total_fuel) ? '0.0' : parseFloat(budgetForm.total_fuel).toFixed(2);
});
const totalFuelCost = computed(() => {
budgetForm.total_fuel_cost = isNaN(budgetForm.total_fuel * budgetForm.fuel_price_per_liter) ? 0.0 : budgetForm.total_fuel * budgetForm.fuel_price_per_liter;
return "$" + (isNaN(budgetForm.total_fuel_cost) ? '0.0' : parseFloat(budgetForm.total_fuel_cost).toFixed(2));
});
const totalAdminExpenses = computed(() => {
budgetForm.total_administrative_expenses = budgetForm.driver_salary*1 + budgetForm.accomadation_allowance*1 + budgetForm.other_administrative_expenses*1;
budgetForm.total_administrative_expenses = isNaN(budgetForm.total_administrative_expenses) ? 0.0 : budgetForm.total_administrative_expenses;
return "$" + (isNaN(budgetForm.total_administrative_expenses) ? '0.0' : parseFloat(budgetForm.total_administrative_expenses).toFixed(2));
});
const totalBeforeTax = computed(() => {
budgetForm.total_before_tax = budgetForm.tonnage * budgetForm.price_per_ton;
budgetForm.total_before_tax = isNaN(budgetForm.total_before_tax) ? 0.0 : budgetForm.total_before_tax;
return "$" + (isNaN(budgetForm.total_before_tax) ? '0.0' : parseFloat(budgetForm.total_before_tax).toFixed(2));
});
const totalUtilityPerKm = computed(() => {
budgetForm.total_utility_per_km = isNaN(budgetForm.total_before_tax / budgetForm.total_travel) ? 0.0 : (budgetForm.total_before_tax / budgetForm.total_travel);
return "$" + (isNaN(budgetForm.total_utility_per_km) ? '0.0' : parseFloat(budgetForm.total_utility_per_km).toFixed(2));
});
const totalProfit = computed(() => {
budgetForm.total_profit = budgetForm.total_before_tax - budgetForm.total_fuel_cost - budgetForm.other_fuel_expenses - budgetForm.driver_salary - budgetForm.accomadation_allowance - budgetForm.other_administrative_expenses;
return "$" + (isNaN(budgetForm.total_profit) ? '0.0' : parseFloat(budgetForm.total_profit).toFixed(2));
});
const totalPercentage = computed(() => {
budgetForm.profit_percentage = budgetForm.total_profit / budgetForm.total_before_tax * 100;
budgetForm.profit_percentage = isNaN(budgetForm.profit_percentage) ? 0.0 : budgetForm.profit_percentage;
return (isNaN(budgetForm.profit_percentage) ? '0.0' : parseFloat(budgetForm.profit_percentage).toFixed(2)) + "%";
});
const saveBudget = async() => {
validations();
if(errors.value.client) return;
let budgetData ={
client: budgetForm.client,
material: budgetForm.budgetproduct ? budgetForm.budgetproduct._id : null,
origin: budgetForm.origin ? budgetForm.origin.city_name : null,
destination: budgetForm.destination ? budgetForm.destination.city_name : null,
truck_type: budgetForm.budgetTrucktypes ? budgetForm.budgetTrucktypes.meta_value : null,
num_tons: budgetForm.num_tons,
price_per_ton: budgetForm.price_per_ton,
tonnage: budgetForm.tonnage,
pickup_distance: budgetForm.pickup_distance,
delivery_distance: budgetForm.delivery_distance,
warehouse_distance: budgetForm.warehouse_distance,
total_km_travel: budgetForm.total_travel,
cost_per_liter: budgetForm.cost_per_liter,
fuel_price_per_liter: budgetForm.fuel_price_per_liter,
other_fuel_expenses: budgetForm.other_fuel_expenses,
total_fuel_consumed: budgetForm.total_fuel,
total_cost_fuel: budgetForm.total_fuel_cost,
driver_salary: budgetForm.driver_salary,
accomadation_allowance: budgetForm.accomadation_allowance,
other_administrative_expenses: budgetForm.other_administrative_expenses,
total_administrative_expenses: budgetForm.total_administrative_expenses,
total_before_tax: budgetForm.total_before_tax,
total_utility_per_km: budgetForm.total_utility_per_km,
total_profit: budgetForm.total_profit,
profit_percentage: (isNaN(budgetForm.profit_percentage) || isFinite(budgetForm.profit_percentage)) ? 0.0 : budgetForm.profit_percentage,
company: authStore.user.company,
}
let result = 'error';
let action = 'Creado';
loading.value = true;
const localData = {
material: budgetForm.budgetproduct
}
if(props.budget !== null) { // update
result = await companyStore.updateBudgetCompany(props.budget._id, budgetData, localData);
action = 'actualizado';
} else { // create
result = await companyStore.createBudgetCompany(budgetData);
action = 'agregado';
}
loading.value = false;
if(result === 'success') {
document.getElementById('btnClosebudgetModal').click();
Swal.fire({
title: `<strong>Presupuesto ${action} con éxito!</strong>`,
icon: 'success'
})
} else {
Swal.fire({
title: result,
icon: 'error'
})
}
}
const validations = () => {
errors.value = {
client: budgetForm.client?.length < 2 ? 'Ingrese nombre del cliente' : null,
};
}
const downloadPdf = () => {
const options = {
margin: 10,
// filename: 'mi_seccion.pdf',
filename: `presupuesto_${props.budget.client.replaceAll(' ', '_')}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
};
html2pdf(pdfContent.value, options);
}
</script>
<template>
<div class="modal fade" id="budgetModal" tabindex="-1" role="dialog" aria-labelledby="editcompany" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">Calculadora</h2>
<button
id="btnClosebudgetModal"
type="button"
@click="$emit('reset-budget')"
class="close bg-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body form-content">
<div class="container-fluid">
<form action="" @submit.prevent="saveBudget" autocomplete="off" method="post" ref="pdfContent">
<div class="calculator-card">
<div class="calculator-card__title">
<h2>CALCULADOR DE TARIFAS</h2>
<img src="/images/logo-eta.png" alt="Eta viaporte" width="110">
</div>
<div class="calculator-card__subtitle">
<h4>DATOS DE CARGA</h4>
</div>
<div class="calculator-card__body">
<div class="calculator-card__loads_data">
<CustomInput
label="CLIENTE*"
type="text"
:filled="false"
name="client"
:error="errors.client"
v-model:field="budgetForm.client"
:readonly="isPrint"
/>
<div class="mb-4 mt-3">
<label class="custom-label">MATERIAL</label>
<Products
v-model="budgetForm.budgetproduct"
:disabled="isPrint"
/>
</div>
<div class="mb-4 mt-3">
<label class="custom-label">ORIGEN</label>
<Cities
v-model="budgetForm.origin"
:disabled="isPrint"
/>
</div>
<div class="mb-4 mt-3">
<label class="custom-label">DESTINO</label>
<Cities
v-model="budgetForm.destination"
:disabled="isPrint"
/>
</div>
<div class="mb-4 mt-3">
<label class="custom-label">TIPO DE CAMIÓN</label>
<TruckTypes
v-model="budgetForm.budgetTrucktypes"
:disabled="isPrint"
/>
</div>
<CustomInput
label="TONELADAS"
type="number"
:filled="false"
name="num_tons"
v-model:field="budgetForm.num_tons"
:readonly="isPrint"
/>
<CustomInput
label="PRECIO POR TONELADA"
type="number"
:filled="false"
name="price_per_ton"
v-model:field="budgetForm.price_per_ton"
:readonly="isPrint"
/>
</div>
</div>
<div class="calculator-card__subtitle">
<h4>TARIFA / KILOMETRAJE</h4>
</div>
<div class="calculator-card__body">
<div class="calculator-card__loads_data">
<CustomInput
label="TONELAJE APROXIMADO DE CARGA"
type="number"
:filled="false"
name="tonnage"
v-model:field="budgetForm.tonnage"
:readonly="isPrint"
/>
<CustomInput
label="PRECIO POR TONELADA"
type="number"
:filled="false"
name="price_per_ton"
v-model:field="budgetForm.price_per_ton"
:readonly="isPrint"
/>
<CustomInput
label="ORIGEN DEL CAMION AL ORIGEN DE CARGA(KM)"
type="number"
:filled="false"
name="pickup_distance"
v-model:field="budgetForm.pickup_distance"
:readonly="isPrint"
/>
<CustomInput
label="ORIGEN DE CARGA A DESTINO DE DESCARGA(KM)"
type="number"
:filled="false"
name="delivery_distance"
v-model:field="budgetForm.delivery_distance"
:readonly="isPrint"
/>
<CustomInput
label="DESTINO DE DESCARGA AL PATIO-BASE (KM)"
type="number"
:filled="false"
name="warehouse_distance"
v-model:field="budgetForm.warehouse_distance"
:readonly="isPrint"
/>
<div class="calculator-card__totals">
<div>
<h6>TOTAL KM RECORRIDOS</h6>
</div>
<div>
{{ totalTravel }}
</div>
</div>
</div>
</div>
<div class="calculator-card__subtitle">
<h4>COSTOS DIRECTOS CAMION</h4>
</div>
<div class="calculator-card__body">
<div class="calculator-card__loads_data">
<CustomInput
label="RENDIMIENTO CAMION - LITROS POR KILOMETRO RECORRIDO"
type="number"
:filled="false"
name="cost_per_liter"
v-model:field="budgetForm.cost_per_liter"
:readonly="isPrint"
/>
<CustomInput
label="PRECIO DEL DIESEL POR LITRO"
type="number"
:filled="false"
name="fuel_price_per_liter"
v-model:field="budgetForm.fuel_price_per_liter"
:readonly="isPrint"
/>
<CustomInput
label="OTROS GASTOS"
type="number"
:filled="false"
name="other_fuel_expenses"
v-model:field="budgetForm.other_fuel_expenses"
:readonly="isPrint"
/>
<div class="calculator-card__totals">
<div>
<h6>TOTAL LITROS DE DIESEL CONSUMIDOS</h6>
</div>
<div>
{{ totalFuel }}
</div>
</div>
<div class="calculator-card__totals">
<div>
<h6>TOTAL COSTO DEL DIESEL</h6>
</div>
<div>
{{ totalFuelCost }}
</div>
</div>
</div>
</div>
<div class="calculator-card__subtitle">
<h4>SUELDO OPERADOR</h4>
</div>
<div class="calculator-card__body">
<div class="calculator-card__loads_data">
<CustomInput
label="GASTOS ADMINISTRATIVOS"
type="number"
:filled="false"
name="driver_salary"
v-model:field="budgetForm.driver_salary"
:readonly="isPrint"
/>
<CustomInput
label="CASETAS"
type="number"
:filled="false"
name="accomadation_allowance"
v-model:field="budgetForm.accomadation_allowance"
:readonly="isPrint"
/>
<CustomInput
label="OTROS GASTOS"
type="number"
:filled="false"
name="other_administrative_expenses"
v-model:field="budgetForm.other_administrative_expenses"
:readonly="isPrint"
/>
<div class="calculator-card__totals">
<div>
<h6>GASTOS ADMINISTRATIVOS TOTALES</h6>
</div>
<div>
{{ totalAdminExpenses }}
</div>
</div>
</div>
</div>
<div class="calculator-card__subtitle">
<h4>UTILIDAD / PERDIDA</h4>
</div>
<div class="calculator-card__body">
<div class="calculator-card__loads_data">
<div class="calculator-card__totals">
<div>
<h6>TOTAL ANTES DE IVA</h6>
</div>
<div>
{{ totalBeforeTax }}
</div>
</div>
<div class="calculator-card__totals">
<div>
<h6>TOTAL UTILIDAD POR KILOMETRO</h6>
</div>
<div>
{{ totalUtilityPerKm }}
</div>
</div>
<div class="calculator-card__totals">
<div>
<h6>TOTAL UTILIDAD</h6>
</div>
<div>
{{ totalProfit }}
</div>
</div>
<div class="calculator-card__totals">
<div>
<h6>PORCENTAJE DE UTILIDAD</h6>
</div>
<div>
{{ totalPercentage }}
</div>
</div>
</div>
</div>
</div>
<div class="mt-4 text-center" v-if="!isPrint">
<Spiner v-if="loading"/>
<button v-else class="btn-primary-sm radius-sm" type="submit">Guardar</button>
</div>
</form>
<div class="mt-4 text-center" v-if="isPrint">
<button
class="btn-primary-sm radius-sm"
@click="downloadPdf"
type="button">
Descargar
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.form-content {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
align-items: center;
margin: 0 auto;
border: 1px solid grey;
}
.header-style {
display: flex;
justify-content: space-between;
}
.calculator-card {
justify-content: center;
margin: 0 auto;
width: 85%;
border: 1px solid grey;
}
.calculator-card__title {
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: #323030;
align-items: center;
padding: 10px 20px;
color: #FFF;
text-align: center;
font-size: 1.2rem;
font-weight: 700;
}
.calculator-card__subtitle {
// background-color: #50b1e5;
background-color: #FBBA33;
padding: 8px 20px;
color: #FFF;
text-align: center;
font-size: 1.2rem;
font-weight: 700;
}
.calculator-card__loads_data {
padding: 20px 24px;
}
.calculator-card__totals {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 8px 0px;
}
@media (max-width: 1024px) {
.calculator-card {
width: 90%;
}
}
@media (max-width: 768px) {
.calculator-card {
width: 95%;
}
}
</style>

View File

@@ -0,0 +1,248 @@
<script setup>
import { computed, onMounted, reactive, ref } from 'vue';
import CustomInput from './ui/custominput.vue';
import Segments from './ui/Segments.vue';
import TruckTypes from './ui/TruckTypes.vue';
import States from './ui/States.vue';
import Cities from './ui/Cities.vue';
import Spiner from './ui/Spiner.vue';
import { useAuthStore } from '../stores/auth';
import { useCompanyStore } from '../stores/company';
import Swal from 'sweetalert2';
const props = defineProps({
location: {
type: Object,
required: false
}
});
defineEmits(['reset-location']);
const authStore = useAuthStore();
const companyStore = useCompanyStore()
const loading = ref(false);
const title = computed(() => {
return (props.location) ? 'Editar Locación' : 'Crear Locación';
});
const initState = {
branch_name: "",
phone: "",
categories: [],
city: "",
state: "",
truck_type: [],
address: "",
description: "",
zipcode: ""
}
const errors = ref({
branch_name: null,
address: null,
city: null,
state: null,
zipcode: null
})
onMounted(() => {
if(props.location) {
locationForm.branch_name = props.location.branch_name;
locationForm.phone = props.location.phone;
locationForm.categories = props.location.categories;
locationForm.city = {
city_name: props.location.city
};
locationForm.state = {
state_name: props.location.state
};
locationForm.truck_type = props.location.truck_type?.map((e) => {
return {
meta_value: e
}
});
locationForm.address = props.location.address;
locationForm.description = props.location.description;
locationForm.zipcode = "";
} else {
Object.assign(locationForm, initState);
}
})
const locationForm = reactive({
...initState
});
const saveLocation = async() => {
validations();
if(errors.value.branch_name || errors.value.address || errors.value.city || errors.value.state || errors.value.zipcode) {
return;
} else {
const branchData ={
branch_name: locationForm.branch_name,
phone: locationForm.phone,
categories: locationForm.categories?.length <= 0 ? null : locationForm.categories?.map((e) => e._id),
city: locationForm.city.city_name,
state: locationForm.state.state_name,
address: locationForm.address,
truck_type: locationForm.truck_type?.length <= 0 ? null : locationForm.truck_type?.map((e) => e.meta_value),
description: locationForm.description,
company: authStore.user.company,
}
let result = 'error';
let action = 'Creado';
loading.value = true;
const localData = {
categories: locationForm.categories,
}
if(props.location !== null) {
// Se actualiza
result = await companyStore.updateLocationCompany(props.location._id, branchData, localData);
action = 'actualizada';
} else {
// Se crea
result = await companyStore.createLocationCompany(branchData, localData);
action = 'creada';
}
loading.value = false;
if(result === 'success') {
document.getElementById('btnCloseLocationFormModal').click();
Swal.fire({
title: `<strong>Locación ${action} con éxito!</strong>`,
icon: 'success'
})
} else {
Swal.fire({
title: result,
icon: 'error'
})
}
}
}
const validations = () => {
errors.value = {
branch_name: locationForm.branch_name.length < 2 ? 'Ingrese nombre' : null,
address: locationForm.address.length <= 4 ? 'Ingrese dirección valida' : null,
city: locationForm.city.length <= 0 ? 'Seleccione municipio' : null,
state: locationForm.state.length < 0 ? 'Seleccione estado' : null,
zipcode: locationForm.zipcode.length < 5 ? 'Ingrese código postal valido' : null,
};
}
</script>
<template>
<div class="modal fade" id="locationFormModal" tabindex="-1" role="dialog" aria-labelledby="locationFormModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">{{ title }}</h2>
<button
id="btnCloseLocationFormModal"
type="button"
@click="$emit('reset-location')"
class="close bg-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body form-content">
<form @submit.prevent="saveLocation" autocomplete="off" method="post" ref="formRef">
<CustomInput
label="Nombre de la locación*"
name="name"
v-model:field="locationForm.branch_name"
:filled="false"
:error="errors.branch_name"
/>
<CustomInput
label="Dirección(s)*"
name="address"
v-model:field="locationForm.address"
:filled="false"
:error="errors.address"
/>
<div class="mb-4 mt-3">
<label class="custom-label">Estado de la locación*</label>
<States
v-model="locationForm.state"
/>
<span class="error-msg" v-if="errors.state">{{ errors.state }}</span>
</div>
<div class="mb-4 mt-3">
<label class="custom-label">Municipio de la locación*</label>
<Cities
v-model="locationForm.city"
/>
<span class="error-msg" v-if="errors.city">{{ errors.city }}</span>
</div>
<CustomInput
label="Código postal"
name="zipcode"
type="number"
v-model:field="locationForm.zipcode"
:filled="false"
:error="errors.zipcode"
/>
<CustomInput
label="Teléfono"
name="phone"
type="number"
v-model:field="locationForm.phone"
:filled="false"
/>
<div class="mb-4 mt-3">
<label class="custom-label">Segmento</label>
<Segments
v-model="locationForm.categories"
:multiple="true"
/>
</div>
<div class="mb-4 mt-3">
<label class="custom-label">Tipo de transporte que se necesita</label>
<TruckTypes
v-model="locationForm.truck_type"
:multiple="true"
/>
</div>
<div class="d-flex flex-column">
<label class="custom-label" for="description">Información adicional del usuario:</label>
<textarea
class="custom-input-light"
name="description"
id="description"
placeholder="Escribe aqui..."
v-model="locationForm.description"
></textarea>
</div>
<div class="mt-4 text-center">
<Spiner v-if="loading"/>
<button
v-else
class="btn btn-dark" type="submit">Guardar</button>
</div>
</form>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-dark"
@click="$emit('reset-location')"
data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,281 @@
<script setup>
import { computed, onMounted, reactive, ref } from 'vue';
import CustomInput from './ui/CustomInput.vue';
import TruckTypes from './ui/TruckTypes.vue';
import Segments from './ui/Segments.vue';
import States from './ui/States.vue';
import Cities from './ui/Cities.vue';
import Spiner from './ui/Spiner.vue';
import { validateEmail } from '../helpers/validations';
import { useAuthStore } from '../stores/auth';
import { useCompanyStore } from '../stores/company';
import Swal from 'sweetalert2';
const props = defineProps({
user: {
type: Object,
required: false
}
});
onMounted(() => {
if(props.user) {
console.log(props.user)
userForm.name = props.user.first_name;
userForm.last_name = props.user.last_name;
userForm.email = props.user.email;
userForm.phone = props.user.phone;
userForm.phone2 = props.user.phone2;
userForm.job_role = props.user.job_role;
userForm.categories = props.user.categories;
userForm.user_city = props.user.user_city?.map(m =>{
return { city_name: m };
});
userForm.user_state = props.user.user_state?.map(m =>{
return { state_name:m };
});
userForm.truck_type = props.user.truck_type?.map(m =>{
return { meta_value:m };
});
userForm.user_description = props.user.user_description;
} else {
Object.assign(userForm, initState);
}
})
const authStore = useAuthStore();
const companyStore = useCompanyStore();
const initState = {
name: '',
last_name: '',
email: '',
phone: '',
phone2: '',
job_role: '',
categories: [],
user_city: [],
user_state: [],
truck_type: [],
user_description: '',
};
const userForm = reactive({
...initState
})
const errors = ref({
name: null,
last_name: null,
email: null,
phone: null,
})
const formRef = ref(null);
const loading = ref(false);
const title = computed(() => {
return (props.user) ? 'Editar usuario' : 'Crear usuario';
});
defineEmits(['reset-user']);
const saveUser = async() => {
validations()
if(errors.value.name || errors.value.last_name || errors.value.email || errors.value.phone){
return;
} else {
let userData ={
first_name: userForm.name,
last_name: userForm.last_name,
email: userForm.email,
phone: userForm.phone,
phone2: userForm.phone2,
job_role: userForm.job_role,
permissions: authStore.user.permissions,
company: authStore.user.company,
categories: userForm.categories.length <= 0 ? null : userForm.categories?.map((e) => e._id),
user_city: userForm.user_city?.length <= 0 ? null : userForm.user_city?.map((e) => e.city_name),
user_state: userForm.user_state?.length <= 0 ? null : userForm.user_state?.map((e) => e.state_name),
truck_type: userForm.truck_type?.length <= 0 ? null : userForm.truck_type?.map((e) => e.meta_value),
user_description: userForm.user_description
}
const dataUpdate = {
categories: userForm.categories,
name: userForm.name + ' ' + userForm.last_name
}
let result = 'error';
let action = 'Creado';
loading.value = true;
if(props.user !== null) {
// Se actualiza
result = await companyStore.updateUserCompany(props.user._id, userData, dataUpdate);
action = 'actualizado';
} else {
// Se crea
result = await companyStore.createUserCompany(userData, dataUpdate);
action = 'creado';
}
loading.value = false;
if(result === 'success') {
document.getElementById('btnCloseuserModal').click();
Swal.fire({
title: `<strong>Usuario ${action} con éxito!</strong>`,
icon: 'success'
})
} else {
Swal.fire({
title: result,
icon: 'error'
})
}
}
}
const validations = () => {
errors.value = {
name: userForm.name.length < 4 ? 'Ingrese nombre' : null,
last_name: userForm.last_name.length <= 1 ? 'Ingrese apellido' : null,
email: !validateEmail(userForm.email) ? 'Ingrese email valido' : null,
phone: userForm.phone.length < 10 ? 'Ingrese numero teléfonico valido' : null,
};
}
</script>
<template>
<div class="modal fade" id="userModal" tabindex="-1" role="dialog" aria-labelledby="userModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">{{ title }}</h2>
<button
id="btnCloseuserModal"
type="button"
@click="$emit('reset-user')"
class="close bg-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body form-content">
<form @submit.prevent="saveUser" autocomplete="off" method="post" ref="formRef">
<CustomInput
label="Nombre(s)*"
name="name"
v-model:field="userForm.name"
:filled="false"
:error="errors.name"
/>
<CustomInput
label="Apellido(s)*"
name="lastname"
v-model:field="userForm.last_name"
:filled="false"
:error="errors.last_name"
/>
<CustomInput
label="Teléfono 1*"
name="phone1"
type="number"
v-model:field="userForm.phone"
:filled="false"
:error="errors.phone"
/>
<CustomInput
label="Teléfono 2"
name="phone2"
type="number"
v-model:field="userForm.phone2"
:filled="false"
/>
<CustomInput
label="Correo electronico*"
name="email"
type="email"
v-model:field="userForm.email"
:filled="false"
:error="errors.email"
/>
<div class="d-flex flex-column">
<label class="custom-label" for="role">Rol de usuario:</label>
<select
class="custom-input-light"
name="role"
id="role"
v-model="userForm.job_role"
>
<option disabled value="">-- Seleccionar rol --</option>
<option value="owner">Dueño</option>
<option value="manager">Gerente</option>
<option v-if="authStore.user?.permissions.includes('role_carrier')" value="driver">Conductor</option>
</select>
</div>
<div class="mb-4 mt-3">
<label class="custom-label">Segmento</label>
<Segments
v-model="userForm.categories"
:multiple="true"
/>
<!-- <span class="error-msg" v-if="submited && errors.segment">{{ errors.segment }}</span> -->
</div>
<div class="mb-4 mt-3">
<label class="custom-label">Tipo de transporte que utiliza</label>
<TruckTypes
v-model="userForm.truck_type"
:multiple="true"
/>
<!-- <span class="error-msg" v-if="submited && errors.truckType">{{ errors.truckType }}</span> -->
</div>
<div class="mb-4 mt-3">
<label class="custom-label">Locaciones de carga por estado</label>
<States
v-model="userForm.user_state"
:multiple="true"
/>
<!-- <span class="error-msg" v-if="submited && errors.stateDestination">{{ errors.stateDestination }}</span> -->
</div>
<div class="mb-4 mt-3">
<label class="custom-label">Locaciones de carga por municipio</label>
<Cities
v-model="userForm.user_city"
:multiple="true"
/>
<!-- <span class="error-msg" v-if="submited && errors.cityDestination">{{ errors.cityDestination }}</span> -->
</div>
<div class="d-flex flex-column">
<label class="custom-label" for="description">Información adicional del usuario:</label>
<textarea
class="custom-input-light"
name="description"
id="description"
placeholder="Escribe aqui..."
v-model="userForm.user_description"
></textarea>
</div>
<div class="mt-4 text-center">
<Spiner v-if="loading"/>
<button
v-else
class="btn btn-dark" type="submit">Guardar</button>
</div>
</form>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-dark"
@click="$emit('reset-user')"
data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.form-content {
margin: 0 auto;
width: 80%;
}
</style>

View File

@@ -0,0 +1,267 @@
<script setup>
import { computed, onMounted, reactive, ref } from 'vue';
import CustomInput from './ui/CustomInput.vue';
import TruckTypes from './ui/TruckTypes.vue';
import { useAuthStore } from '../stores/auth';
import Segments from './ui/Segments.vue';
import States from './ui/States.vue';
import Cities from './ui/Cities.vue';
import Spiner from './ui/Spiner.vue';
import Swal from 'sweetalert2';
import { useVehiclesStore } from '../stores/vehicles';
const props = defineProps({
vehicle: {
type: Object,
required: false
}
});
defineEmits(['reset-vehicle']);
const initState = {
truck_type: '',
categories: '',
vehicle_number: '',
city: '',
state: '',
trailer_plate_1: '',
trailer_plate_2: '',
circulation_serial_number: '',
notes: '',
destino: '',
}
const errors = ref({
truck_type: null,
categories: null,
vehicle_number: null,
state: null,
city: null,
destino: null,
})
const vehicleForm = reactive({
...initState
})
const authStore = useAuthStore();
const vehicleStore = useVehiclesStore();
onMounted(() => {
if(props.vehicle) {
vehicleForm.truck_type = {
meta_value: props.vehicle.truck_type
};
vehicleForm.categories = props.vehicle.categories;
vehicleForm.vehicle_number = props.vehicle.vehicle_number,
vehicleForm.city = {city_name: props.vehicle.city},
vehicleForm.state = {state_name: props.vehicle.state},
vehicleForm.trailer_plate_1 = props.vehicle.trailer_plate_1;
vehicleForm.trailer_plate_2 = props.vehicle.trailer_plate_2;
vehicleForm.circulation_serial_number = props.vehicle.circulation_serial_number;
vehicleForm.notes = props.vehicle.notes;
vehicleForm.destino = props.vehicle.destino;
}
})
const loading = ref(false);
const title = computed(() => {
return (props.vehicle) ? 'Editar vehiculo' : 'Agregar vehiculo';
});
const handleSaveVehicle = async() => {
validations();
if(errors.value.vehicle_number || errors.value.truck_type || errors.value.categories || errors.value.city || errors.value.state || errors.value.destino ) return;
let vehicleData ={
vehicle_number : vehicleForm.vehicle_number,
city : vehicleForm.city?.city_name,
state : vehicleForm.state?.state_name,
truck_type : vehicleForm?.truck_type.meta_value,
trailer_plate_1: vehicleForm.trailer_plate_1,
trailer_plate_2: vehicleForm.trailer_plate_2,
circulation_serial_number: vehicleForm.circulation_serial_number,
notes: vehicleForm.notes,
company: authStore.user.company,
categories: vehicleForm.categories.length <= 0 ? null : vehicleForm.categories?.map((e) => e._id),
destino: vehicleForm.destino,
available_date: new Date()
};
let localData = {};
let result = 'error';
let action = 'Creado';
loading.value = true;
if(props.vehicle) {
localData = {
categories: vehicleForm.categories,
driver: props.vehicle.driver
};
action = 'actualizado';
result = await vehicleStore.updateVehicleCompany(props.vehicle._id, vehicleData, localData);
} else {
localData = {
categories: vehicleForm.categories
}
action = 'añadido';
result = await vehicleStore.createVehicleCompany(vehicleData, localData);
}
loading.value = false;
if(result === 'success') {
document.getElementById('btnClosecreateVehicleModal').click();
Swal.fire({
title: `<strong>Vehiculo ${action} con éxito!</strong>`,
icon: 'success'
})
} else {
Swal.fire({
title: result,
icon: 'error'
})
}
}
const validations = () => {
errors.value = {
truck_type: (!vehicleForm.truck_type) ? 'Seleccione tipo de transporte' : null,
categories: (!vehicleForm.categories) ? 'Seleccione al menos un segmento' : null,
vehicle_number: (!vehicleForm.vehicle_number) ? 'Campo es requerido' : null,
state: (!vehicleForm.state) ? 'Seleccione estado' : null,
city: (!vehicleForm.city) ? 'Seleccione municipio' : null,
destino: (!vehicleForm.destino) ? 'Ingrese destino' : null,
};
}
</script>
<template>
<div class="modal fade" id="createVehicleModal" tabindex="-1" role="dialog" aria-labelledby="createVehicleModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">{{ title }}</h2>
<button
id="btnClosecreateVehicleModal"
type="button"
@click="$emit('reset-vehicle')"
class="close bg-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body form-content">
<form @submit.prevent="handleSaveVehicle" autocomplete="off" class="vehicle-form">
<div class="row mt-4">
<div class="col-lg-6 col-12">
<label class="custom-label">Tipo de Transporte*</label>
<TruckTypes
v-model="vehicleForm.truck_type"
/>
<span class="error-msg" v-if="errors.truck_type">{{ errors.truck_type }}</span>
</div>
<div class="col-lg-6 col-12">
<label class="custom-label">Segmento del transporte*</label>
<Segments
v-model="vehicleForm.categories"
:multiple="true"
/>
<span class="error-msg" v-if="errors.categories">{{ errors.categories }}</span>
</div>
</div>
<div class="row mt-4">
<div class="col-lg-6 col-12">
<CustomInput
label="Número de Serie*"
name="vehicle_number"
v-model:field="vehicleForm.vehicle_number"
:filled="false"
:error="errors.vehicle_number"
/>
</div>
<div class="col-lg-6 col-12">
<CustomInput
label="Placas Tracto Camión"
name="circulation_serial_number"
v-model:field="vehicleForm.circulation_serial_number"
:filled="false"
/>
</div>
</div>
<div class="row">
<div class="col-lg-6 col-12">
<CustomInput
label="Placas Remolque 1"
name="trailer_plate_1"
v-model:field="vehicleForm.trailer_plate_1"
:filled="false"
/>
</div>
<div class="col-lg-6 col-12">
<CustomInput
label="Placas Remolque 1"
name="trailer_plate_2"
v-model:field="vehicleForm.trailer_plate_2"
:filled="false"
/>
</div>
</div>
<div class="row">
<div class="col-lg-6 col-12">
<label class="custom-label">Base de carga por Estado*</label>
<States
v-model="vehicleForm.state"
/>
<span class="error-msg" v-if="errors.state">{{ errors.state }}</span>
</div>
<div class="col-lg-6 col-12">
<label class="custom-label">Base de Carga por Municipio*</label>
<Cities
v-model="vehicleForm.city"
/>
<span class="error-msg" v-if="errors.city">{{ errors.city }}</span>
</div>
</div>
<div class="col-lg-6 col-12 mt-4">
<CustomInput
label="Destino*"
type="text"
:filled="false"
name="destino"
:error="errors.destino"
v-model:field="vehicleForm.destino"
/>
</div>
<div class="d-flex flex-column mt-4">
<label class="custom-label" for="notes">Información Adicional del Transporte:</label>
<textarea
class="custom-input-light"
name="notes"
id="notes"
placeholder="Escribe aqui..."
v-model="vehicleForm.notes"
></textarea>
</div>
<div class="mt-4 text-center">
<Spiner v-if="loading"/>
<button
v-else
class="btn-primary-sm radius-sm" type="submit">Guardar</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.vehicle-form {
padding: 0px 24px;
}
@media (max-width: 768px) {
.vehicle-form {
padding: 0px 12px;
}
}
</style>

View File

@@ -0,0 +1,77 @@
<script setup>
import { onMounted, ref } from 'vue';
import { Doughnut } from 'vue-chartjs'
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, LinearScale, CategoryScale, ArcElement } from 'chart.js'
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, ArcElement)
const props = defineProps({
data: {
type: Array,
required: true
},
dataModel: {
type: Array,
},
targetFind: {
type: String
},
targetLabel: {
type: String
}
})
const chartData = ref(null);
const dataMap = ref([]);
onMounted(() => {
props.data.forEach(item => {
const index = dataMap.value.findIndex((e) => e.label === item);
if(index === -1) {
if(props.dataModel) {
const itemModel = props.dataModel.find((e) => e[props.targetFind] === item);
dataMap.value.push({
label: (props.targetLabel) ? itemModel[props.targetLabel] : item,
data: 1,
...itemModel
})
} else {
dataMap.value.push({
label: item,
data: 1,
color: 'green'
});
}
} else {
dataMap.value[index].data += 1;
}
});
chartData.value = {
labels: dataMap.value.map((e) => e.label),
datasets: [{
data: dataMap.value.map((e) => e.data),
backgroundColor: dataMap.value.map((e) => e.color),
}],
}
})
const chartOptions = {
responsive: true,
}
</script>
<template>
<Doughnut
id="my-chart-statictics"
v-if="chartData"
:options="chartOptions"
:data="chartData"
/>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,156 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useCompanyStore } from '../stores/company';
import Spiner from './ui/Spiner.vue';
import { useVehiclesStore } from '../stores/vehicles';
import Swal from 'sweetalert2';
const props = defineProps({
vehicle: {
type: Object,
required: true
}
});
defineEmits(['reset-vehicle']);
const companyStore = useCompanyStore();
const vehicleStore = useVehiclesStore();
const driverSelected = ref(null);
const drivers = ref([]);
const error = ref(null)
const loading = ref(false);
onMounted(() => {
drivers.value = companyStore.drivers;
if(props?.vehicle?.driver) {
const index = drivers.value.findIndex((d) => d._id === props.vehicle.driver?._id);
driverSelected.value = drivers.value[index];
}
});
const handleSetDriver = async() => {
if(driverSelected.value === null) {
error.value = 'Seleccione un conductor';
return
}
let vehicle_id = props.vehicle._id;
let driver_id = driverSelected.value._id;
let vehicleData ={
driver : driverSelected.value
}
let localData ={
driver : driverSelected.value,
categories: props.vehicle.categories
}
loading.value = true;
const result = await vehicleStore.updateVehicleCompany(vehicle_id, vehicleData, localData);
if( result === 'success' ) {
//Actualizamos el vehiculo
let userData = {
vehicle : vehicle_id
}
let localUser = {
categories: driverSelected.value.categories,
}
const result2 = await companyStore.updateUserCompany(driver_id, userData, localUser);
if(result2 === 'success' ){
document.getElementById('btnCloseeditDriverVehicle').click();
Swal.fire({
title: `<strong>Driver asignado con éxito!</strong>`,
icon: 'success'
});
} else {
Swal.fire({
title: result2,
icon: 'error'
})
}
} else {
Swal.fire({
title: result,
icon: 'error'
})
}
loading.value = false;
//Continua en la lina 568 web_main
}
</script>
<template>
<div class="modal fade" id="editDriverVehicle" tabindex="-1" role="dialog" aria-labelledby="editDriverVehicle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">Cambiar conductor</h2>
<button
id="btnCloseeditDriverVehicle"
type="button"
class="close bg-white"
data-dismiss="modal"
aria-label="Close"
@click="$emit('reset-vehicle')"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body view-proposals">
<div class="custom-selected-field">
<label class="custom-label my-2" for="driver">Conductor asignado:</label>
<select
class="custom-input-light"
name="driver"
id="driver"
v-model="driverSelected"
>
<option disabled value="">-- Seleccionar conductor --</option>
<option v-for="driver in drivers" :value="driver">{{driver.name}}</option>
</select>
<span class="error-msg" v-if="error">{{ error }}</span>
</div>
</div>
<div class="modal-footer">
<Spiner v-if="loading"/>
<div v-else class="btns-footer">
<button
type="button"
class="btn btn-dark radius-sm"
data-dismiss="modal"
@click="$emit('reset-vehicle')"
>Cancelar</button>
<button
class="btn-primary-sm radius-sm"
@click="handleSetDriver"
>
<span class="clear-xsm">Guardar</span>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.custom-selected-field {
display: flex;
flex-direction: column;
margin-bottom: 16px;
}
.btns-footer {
display: flex;
gap: 1rem;
}
</style>

View File

@@ -19,8 +19,9 @@
</div> </div>
</div> </div>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<a class="btn-links mb-1" href="">Aviso de privacidad</a> <RouterLink class="btn-links mb-1" :to="{name: 'notice-privacy'}" target="_blank">Aviso de privaciadad</RouterLink>
<a class="btn-links" href="">Terminos y condiciones</a> <RouterLink class="btn-links" :to="{name: 'terms-conditions'}" target="_blank">Términos y condiciones</RouterLink>
<RouterLink class="btn-links" :to="{name: 'faqs'}" target="_blank">Faqs</RouterLink>
</div> </div>
</div> </div>
<h4 class="copy"><i class="fa fa-copyright" aria-hidden="true"></i> 2023 ETA VIAPORTE | TODOS LOS DERECHOS RESERVADOS</h4> <h4 class="copy"><i class="fa fa-copyright" aria-hidden="true"></i> 2023 ETA VIAPORTE | TODOS LOS DERECHOS RESERVADOS</h4>

View File

@@ -0,0 +1,683 @@
<script setup>
import { reactive, ref, onMounted, watch } from 'vue';
import { useLoadsStore } from '../stores/loads';
import Custominput from './ui/custominput.vue';
import Segments from './ui/Segments.vue';
import TruckTypes from './ui/TruckTypes.vue';
import Cities from './ui/Cities.vue';
import States from './ui/States.vue';
import Spiner from './ui/Spiner.vue';
import Products from './ui/Products.vue';
import { GoogleMap, Marker, Polyline } from "vue3-google-map";
import useDirectionsRender from '../composables/useDirectionRender';
import { useAuthStore } from '../stores/auth';
import Swal from 'sweetalert2';
import { useNotificationsStore } from '../stores/notifications';
import { useCompanyStore } from '../stores/company';
const loadStore = useLoadsStore();
const notyStore = useNotificationsStore();
const auth = useAuthStore();
const companyStore = useCompanyStore()
const windowWidth = ref(window.innerWidth);
const zoom = ref(6);
const heightMap = ref(768);
const originCoords = ref(null);
const destinationCoords = ref(null);
const startLocation = ref(null);
const endLocation = ref(null);
const isLoading = ref(false);
const loadingLocations = ref(false);
const submited = ref(false);
const { geocodeAddress } = useDirectionsRender();
const formRef = ref(null);
const filterQueryVehicles = ref([]);
const checkLocationLoad = ref(false);
const checkLocationDownload = ref(false);
const locationLoadSelected = ref(null)
const locationDownloadSelected = ref(null)
const errors = ref({
segment: null,
truckType: null,
cityOrigin: null,
stateOrigin: null,
cityDestination: null,
stateDestination: null,
});
const clearLoad = () => {
loadStore.currentLoad = null;
loadStore.openModalEdit = false;
}
const handleResize = () => {
windowWidth.value = window.innerWidth
if(windowWidth.value <= 1024){
zoom.value = 4
heightMap.value = 420;
} else {
zoom.value = 6;
heightMap.value = 768;
}
}
onMounted(() => {
window.addEventListener('resize', handleResize);
// mapRef.value = this.$refs.myMap;
if(window.innerWidth <= 1024) {
zoom.value = 4;
heightMap.value = 420;
}
if(companyStore.locationsLoads.length <= 0) {
getLocations();
}
formLoad.owner = auth.user?.first_name + ' ' + auth.user?.last_name;
//origin_formatted_address
if(loadStore.currentLoad){
formLoad.price = loadStore.currentLoad.actual_cost;
formLoad.segment = loadStore.currentLoad.categories?.length <= 0 ? [] : loadStore.currentLoad.categories.map(m =>{
return m;
});
// formLoad.segment = loadStore.currentLoad.categories?.length <= 0 ? null : loadStore.currentLoad.categories.map(m =>{
// return m;
// });
startLocation.value = loadStore.currentLoad.origin_formatted_address;
endLocation.value = loadStore.currentLoad.destination_formatted_address;
formLoad.terms = loadStore.currentLoad.product;
formLoad.owner = loadStore.currentLoad.posted_by_name;
formLoad.notes = loadStore.currentLoad.notes;
formLoad.weight = loadStore.currentLoad.weight;
formLoad.dateLoad = loadStore.currentLoad.est_loading_date?.substring(0, 10);
formLoad.dateDownload = loadStore.currentLoad.est_unloading_date?.substring(0, 10);
formLoad.truckType = loadStore.currentLoad.truck_type ? {meta_value: loadStore.currentLoad.truck_type} : null;
origin.locationName = loadStore.currentLoad.origin.company_name;
origin.address = loadStore.currentLoad.origin.street_address1;
origin.state = loadStore.currentLoad.origin?.state ? { state_name: loadStore.currentLoad.origin.state } : null;
origin.city = loadStore.currentLoad.origin?.city ? { city_name: loadStore.currentLoad.origin.city } : null;
origin.country = loadStore.currentLoad.origin.country;
origin.postalCode = loadStore.currentLoad.origin.zipcode;
origin.ref = loadStore.currentLoad.origin.landmark;
destination.locationName = loadStore.currentLoad.destination.company_name;
destination.address = loadStore.currentLoad.destination.street_address1;
destination.state = loadStore.currentLoad.destination?.state ? { state_name: loadStore.currentLoad.destination.state } : null;
destination.city = loadStore.currentLoad.destination?.city ? { city_name: loadStore.currentLoad.destination.city } : null;
destination.country = loadStore.currentLoad.destination.country;
destination.postalCode = loadStore.currentLoad.destination.zipcode;
destination.ref = loadStore.currentLoad.destination.landmark;
getCoordsMap();
}
watch(origin, async() => {
if(origin.city && origin.state) {
startLocation.value = origin.address + ', ' + origin.city.city_name + ', ' + origin.state.state_name + ', ' + origin.country + ', ' +origin.postalCode;
originCoords.value = await geocodeAddress(startLocation.value);
}
})
watch(destination, async() => {
if(destination.city && destination.state) {
endLocation.value = destination.address + ', ' + destination.city.city_name + ', ' + destination.state.state_name + ', ' + destination.country + ', ' +destination.postalCode;
destinationCoords.value = await geocodeAddress(endLocation.value);
}
})
})
watch(locationLoadSelected, () => {
origin.locationName = locationLoadSelected.value.branch_name;
origin.address = locationLoadSelected.value.address;
origin.state = { state_name: locationLoadSelected.value.state };
origin.city = { city_name: locationLoadSelected.value.city };
origin.ref = locationLoadSelected.value.description;
});
watch(locationDownloadSelected, () => {
destination.locationName = locationDownloadSelected.value.branch_name;
destination.address = locationDownloadSelected.value.address;
destination.state = { state_name: locationDownloadSelected.value.state };
destination.city = { city_name: locationDownloadSelected.value.city };
destination.ref = locationDownloadSelected.value.description;
});
const getLocations = async() => {
loadingLocations.value = true;
// filterQueryVehicles.value.company = "company="+ localStorage.getItem('id');
await companyStore.getLocationsLoads()
loadingLocations.value = false;
}
const getCoordsMap = async() => {
if(loadStore.currentLoad.origin_formatted_address) {
originCoords.value = await geocodeAddress(loadStore.currentLoad.origin_formatted_address);
}
if(loadStore.currentLoad.destination_formatted_address){
destinationCoords.value = await geocodeAddress(loadStore.currentLoad.destination_formatted_address);
}
}
const formLoad = reactive({
dateLoad: '',
dateDownload: '',
segment: [],
truckType: '',
terms: '',
price: 0,
weight: 0,
notes: '',
owner: '',
});
const origin = reactive({
locationName: '',
address: '',
state: '',
city: '',
country: '',
postalCode: '',
ref: '',
});
const destination = reactive({
locationName: '',
address: '',
state: '',
city: '',
country: '',
postalCode: '',
ref: '',
});
const setLoadData = () => {
let loadData = {
actual_cost: formLoad.price,
truck_type: formLoad.truckType?.meta_value || null,
est_loading_date : formLoad.dateLoad,
est_unloading_date : formLoad.dateDownload,
notes : formLoad.notes,
weight : formLoad.weight,
product: formLoad.terms?.length <= 0 ? null : formLoad.terms,
categories: formLoad.segment || null,
origin:{
company_name : origin?.locationName,
street_address1 : origin?.address,
state : origin.state?.state_name,
city : origin.city?.city_name,
country : origin?.country,
landmark : origin?.ref,
zipcode : origin?.postalCode,
lat : originCoords.value?.lat,
lng : originCoords.value?.lng
},
destination:{
company_name : destination?.locationName,
street_address1 : destination?.address,
state : destination.state?.state_name,
city : destination.city?.city_name,
country : destination?.country,
landmark : destination?.ref,
zipcode : destination?.postalCode,
lat : destinationCoords.value?.lat,
lng : destinationCoords.value?.lng
},
company: auth.user.company,
posted_by: auth.user._id,
posted_by_name: formLoad.owner
};
return loadData;
}
const updateLoad = async(loadData) => {
isLoading.value = true;
if(loadStore.currentLoad){
const resp = await loadStore.updateLoad(loadStore.currentLoad._id, loadData);
isLoading.value = false;
if(resp) {
const index = loadStore.loads.findIndex((load) => load._id === resp._id);
loadStore.loads[index] = {
...loadStore.loads[index],
...resp
};
return 'success';
} else {
return 'error';
}
} else{
const resp = await loadStore.saveLoad(loadData);
isLoading.value = false;
if(resp) {
const load = {
...resp,
...loadData,
categories: [loadData.categories]
}
loadStore.loads.push(load);
return 'success';
} else {
return 'error';
}
}
}
const handleSave = async() => {
submited.value = false;
const loadData = setLoadData();
const resp = await updateLoad(loadData);
if(resp === 'success') {
notyStore.show = 'true';
notyStore.text = 'Carga guardada!'
document.getElementById('btnCloseFormLoadModal').click();
} else {
Swal.fire({
title: "Error!",
text: "No se pudo guardar carga, intente más tarde",
icon: "error"
});
}
}
const validations = () => {
errors.value = {
segment: (!formLoad.segment || formLoad.segment?.length <= 0) ? 'Seleccione segmento' : null,
truckType: formLoad.truckType ? null : 'Seleccione tipo de transporte',
cityOrigin: origin.city ? null : 'Seleccione ciudad',
stateOrigin: origin.state ? null : 'Seleccione estado',
cityDestination: destination.city ? null : 'Seleccione ciudad',
stateDestination: destination.state ? null : 'Seleccione estado',
};
}
const handlePostLoad = async() => {
submited.value = true;
setTimeout(async() => {
const formValid = formRef.value.checkValidity();
validations()
if(formValid){
const hasError = Object.values(errors.value).some(val => val != null);
if(!hasError) {
let loadData = setLoadData();
loadData = {
...loadData,
status: "Published",
load_status: "Published"
}
const resp = await updateLoad(loadData);
if(resp === 'success') {
notyStore.show = 'true';
notyStore.text = 'Carga publicada!'
document.getElementById('btnCloseFormLoadModal').click();
} else {
Swal.fire({
title: "Error!",
text: "No se pudo publicar carga, intente más tarde",
icon: "error"
});
}
}
}
}, 200)
}
</script>
<template>
<div class="modal fade" id="formLoadModal" tabindex="-1" role="dialog" aria-labelledby="formLoadModal" aria-hidden="true">
<div class="modal-dialog modal-fullscreen modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">Crear nueva carga</h2>
<button
id="btnCloseFormLoadModal"
type="button"
class="close bg-white"
data-dismiss="modal"
@click="clearLoad"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form class="form-content" ref="formRef">
<div class="form-box">
<div class="form-section">
<div class="mb-4 mt-3">
<label class="custom-label">Segmento*</label>
<Segments
v-model="formLoad.segment"
/>
<span class="error-msg" v-if="submited && errors.segment">{{ errors.segment }}</span>
</div>
<div class="mb-4 mt-3">
<label class="custom-label">Tipo de transporte*</label>
<TruckTypes
v-model="formLoad.truckType"
/>
<span class="error-msg" v-if="submited && errors.truckType">{{ errors.truckType }}</span>
</div>
<Custominput
label="Fecha de carga*"
type="date"
:filled="false"
name="date-load"
:required="submited ? true : false"
:error="(submited && !formLoad.dateLoad) ? 'Fecha es requerida' : null"
v-model:field="formLoad.dateLoad"
/>
<Custominput
label="Fecha de descarga*"
type="date"
:filled="false"
name="date-download"
:required="submited ? true : false"
:error="(submited && !formLoad.dateDownload) ? 'Fecha es requerida' : null"
v-model:field="formLoad.dateDownload"
/>
<Custominput
label="Peso de la carga*"
type="number"
:filled="false"
name="weight"
:required="submited ? true : false"
:error="(submited && !formLoad.weight) ? 'Ingrese peso en KG' : null"
v-model:field="formLoad.weight"
/>
</div>
<div class="form-section">
<div class="mb-4 mt-3">
<label class="custom-label">Terminos</label>
<Products
v-model="formLoad.terms"
/>
</div>
<Custominput
label="Precio"
type="Number"
:filled="false"
name="price"
v-model:field="formLoad.price"
/>
<Custominput
label="Notas"
type="text"
:filled="false"
name="notes"
v-model:field="formLoad.notes"
/>
<Custominput
label="Publicación hecha por"
type="text"
:filled="false"
:readonly="true"
name="owner"
v-model:field="formLoad.owner"
/>
</div>
</div>
<div class="form-box">
<div class="form-section">
<h2>Dirección de origen</h2>
<div class="form-check my-4" v-if="loadingLocations === false">
<input class="form-check-input chekmark" type="checkbox" id="flexCheckDefault" v-model="checkLocationLoad">
<label class="form-check-label custom-label" for="flexCheckDefault">
Usar locaciones registradas
</label>
</div>
<div class="d-flex flex-column mb-4" v-if="checkLocationLoad">
<label class="custom-label mb-2" for="locationLoad">Locaciones registradas:</label>
<select
class="custom-input-light"
name="locationLoad"
id="locationLoad"
v-model="locationLoadSelected"
>
<option disabled value="">-- Seleccionar locación --</option>
<option v-for="loc in companyStore.locationsLoads" :value="loc">{{ loc.branch_name }}</option>
</select>
</div>
<Custominput
label="Nombre locación de carga"
type="text"
:filled="false"
name="name-location-origin"
v-model:field="origin.locationName"
/>
<Custominput
label="Dirección"
type="text"
:filled="false"
name="address-origin"
v-model:field="origin.address"
/>
<div class="mb-4 mt-3">
<label class="custom-label">Ciudad*</label>
<Cities
v-model="origin.city"
/>
<span class="error-msg" v-if="submited && errors.cityOrigin">{{ errors.cityOrigin }}</span>
</div>
<div class="mb-4 mt-3">
<label class="custom-label">Estado*</label>
<States
v-model="origin.state"
/>
<span class="error-msg" v-if="submited && errors.stateOrigin">{{ errors.stateOrigin }}</span>
</div>
<Custominput
label="País"
type="text"
:filled="false"
name="country-origin"
v-model:field="origin.country"
/>
<Custominput
label="Código Postal"
type="text"
:filled="false"
name="postalCode-origin"
v-model:field="origin.postalCode"
/>
<Custominput
label="Punto de referencia"
type="text"
:filled="false"
name="ref-origin"
v-model:field="origin.ref"
/>
</div>
<div class="form-section">
<h2>Dirección de destino</h2>
<div class="form-check my-4" v-if="loadingLocations === false">
<input class="form-check-input chekmark" type="checkbox" id="flexCheckDefault" v-model="checkLocationDownload">
<label class="form-check-label custom-label" for="flexCheckDefault">
Usar locaciones registradas
</label>
</div>
<div class="d-flex flex-column mb-4" v-if="checkLocationDownload">
<label class="custom-label mb-2" for="locationDownload">Locaciones registradas:</label>
<select
class="custom-input-light"
name="locationDownload"
id="locationDownload"
v-model="locationDownloadSelected"
>
<option disabled value="">-- Seleccionar locación --</option>
<option v-for="loc in companyStore.locations" :value="loc">{{ loc.branch_name }}</option>
</select>
</div>
<Custominput
label="Nombre locación de descarga"
type="text"
:filled="false"
name="name-location-destination"
v-model:field="destination.locationName"
/>
<Custominput
label="Dirección"
type="text"
:filled="false"
name="address-destination"
v-model:field="destination.address"
/>
<div class="mb-4 mt-3">
<label class="custom-label">Ciudad*</label>
<Cities
v-model="destination.city"
/>
<span class="error-msg" v-if="submited && errors.cityDestination">{{ errors.cityDestination }}</span>
</div>
<div class="mb-4 mt-3">
<label class="custom-label">Estado*</label>
<States
v-model="destination.state"
/>
<span class="error-msg" v-if="submited && errors.stateDestination">{{ errors.stateDestination }}</span>
</div>
<Custominput
label="País"
type="text"
:filled="false"
name="country-destination"
v-model:field="destination.country"
/>
<Custominput
label="Código Postal"
type="text"
:filled="false"
name="postalCode-destination"
v-model:field="destination.postalCode"
/>
<Custominput
label="Punto de referencia"
type="text"
:filled="false"
name="ref-destination"
v-model:field="destination.ref"
/>
</div>
</div>
</form>
<GoogleMap
api-key="AIzaSyAJtfvrAKy7vnUSv2nzk4dYQkOs3OP4MMs"
:center="{lat:19.432600, lng:-99.133209}"
:zoom="zoom"
:min-zoom="2"
:max-zoom="12"
:style="{width: 100 + '%', height: heightMap + 'px'}"
:options="{
zoomControl: true,
mapTypeControl: true,
scaleControl: true,
streetViewControl: true,
rotateControl: true,
fullscreenControl: true
}"
>
<Marker v-if="originCoords" :options="{position: originCoords, label: 'O', title: 'Destino'}" />
<Marker v-if="destinationCoords" :options="{position: destinationCoords, label: 'D', title: 'Origen'}" />
<!-- <Polyline :options="{
path: polyline,
// geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 2
}" /> -->
</GoogleMap>
</div>
<div class="modal-footer custom-footer">
<Spiner v-if="isLoading"/>
<div v-else class="btns-footer">
<button
type="button"
class="btn btn-dark"
@click="clearLoad"
data-dismiss="modal">Cerrar</button>
<button
type="button"
class="btn btn-dark"
@click="handleSave"
>Guardar</button>
<button
type="submit"
@click.prevent="handlePostLoad"
class="btn-primary-sm radius-sm"
>Publicar</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.form-content {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
align-items: center;
margin: 0 auto;
}
.form-box {
width: 80%;
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 4rem;
}
.form-section {
width: 100%;
}
.custom-footer {
display: flex;
justify-content: center;
}
.error-msg {
color: red;
font-size: 12px;
font-weight: 300;
}
.btns-footer {
display: flex;
gap: 1rem;
}
.chekmark {
height: 25px;
width: 25px;
margin-right: 10px;
}
.radius-sm{
border-radius: 5px;
}
@media (max-width: 1024px) {
.form-box {
width: 95%;
gap: 2rem;
}
}
@media (max-width: 568px) {
.form-box {
width: 100%;
flex-direction: column;
gap: 2rem;
}
}
</style>

View File

@@ -5,7 +5,11 @@
<template> <template>
<div class="header-content"> <div class="header-content">
<div class="box-content"> <div class="box-content">
<img src="/images/logo-eta.png" class="logo" alt="Eta Viaporte" > <RouterLink
:to="{name: 'home'}"
>
<img src="/images/logo-eta.png" class="logo" alt="Eta Viaporte" >
</RouterLink>
<div class="box-register"> <div class="box-register">
<p class="title-header">Tablero de <span class="title-main">Embarques</span> y <span class="title-main">Transportes</span></p> <p class="title-header">Tablero de <span class="title-main">Embarques</span> y <span class="title-main">Transportes</span></p>
</div> </div>

View File

@@ -0,0 +1,188 @@
<script setup>
import { ref, onMounted } from 'vue';
import CardEmpty from './CardEmpty.vue';
import Spiner from './ui/Spiner.vue';
import { GoogleMap, Marker, CustomMarker } from 'vue3-google-map';
import useDirectionsRender from '../composables/useDirectionRender';
import Cardload from './cardload.vue';
import { useLoadsStore } from '../stores/loads';
const zoom = ref(6);
const heightMap = ref(768);
const originCoords = ref(null);
const destinationCoords = ref(null);
const isLoading = ref(false);
const windowWidth = ref(window.innerWidth);
const load = ref(null);
const vehicleLastLocation = ref(null);
const isLoadActive = ref(false);
const { geocodeAddress } = useDirectionsRender()
const props = defineProps({
proposal: {
type: Object,
required: true,
}
})
defineEmits(['reset-proposal'])
const loadStore = useLoadsStore();
onMounted(() => {
window.addEventListener('resize', handleResize);
if(window.innerWidth <= 1024) {
zoom.value = 4;
heightMap.value = 420;
}
initData()
});
const initData = async() => {
isLoading.value = true;
const code = props.proposal.load.shipment_code;
const filter = "?shipment_code[$in]=" + code;
const resp = await loadStore.getLoad(filter);
if(resp.total > 0) {
load.value = resp.data[0];
originCoords.value = await geocodeAddress(load.value.origin_formatted_address);
destinationCoords.value = await geocodeAddress(load.value.destination_formatted_address);
if(load.value.vehicle) {
vehicleLastLocation.value = {
lat: parseFloat(load.value.vehicle.last_location_lat),
lng: parseFloat(load.value.vehicle.last_location_lng)
}
}
switch (load.value.load_status) {
case 'Loading':
isLoadActive.value = true;
break;
case 'Transit':
isLoadActive.value = true;
break;
case 'Downloading':
isLoadActive.value = true;
break;
default:
isLoadActive.value = false;
break;
}
}
isLoading.value = false;
}
const handleResize = () => {
windowWidth.value = window.innerWidth
if(windowWidth.value <= 1024){
zoom.value = 4
heightMap.value = 420;
} else {
zoom.value = 6;
heightMap.value = 768;
}
}
</script>
<template>
<div class="modal fade" id="loadDetailModal" tabindex="-1" role="dialog" aria-labelledby="editcompany" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">Detalles de carga</h2>
<button
id="btnCloseloadDetailModal"
type="button"
class="close bg-white"
data-dismiss="modal"
aria-label="Close"
@click="$emit('reset-proposal')"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body view-proposals">
<Spiner v-if="isLoading"/>
<div v-else>
<div v-if="load">
<Cardload :load="load" :read-only="true"/>
<GoogleMap
api-key="AIzaSyAJtfvrAKy7vnUSv2nzk4dYQkOs3OP4MMs"
:center="{lat:19.432600, lng:-99.133209}"
:zoom="zoom"
:min-zoom="2"
:max-zoom="12"
:style="{width: 100 + '%', height: heightMap + 'px'}"
:options="{
zoomControl: true,
mapTypeControl: true,
scaleControl: true,
streetViewControl: true,
rotateControl: true,
fullscreenControl: true
}"
>
<Marker v-if="originCoords" :options="{position: originCoords, label: 'O', title: 'Destino'}" />
<Marker v-if="destinationCoords" :options="{position: destinationCoords, label: 'D', title: 'Origen'}" />
<CustomMarker
v-if="vehicleLastLocation && load.vehicle.background_tracking && isLoadActive"
:options="{position: vehicleLastLocation}"
:clickable="false"
:draggable="false"
>
<div style="text-align: center">
<!-- <img src="/images/freeTruck.png" width="25" height="25" /> -->
<i class="fa-solid fa-truck" :style="{fontSize: 25 + 'px', color: 'green'}"></i>
</div>
</CustomMarker>
<!-- <Polyline :options="{
path: polyline,
// geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 2
}" /> -->
</GoogleMap>
</div>
<CardEmpty v-else text="No hay información disponible"/>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-dark"
data-dismiss="modal"
@click="$emit('reset-proposal')"
>Cerrar</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.box-form {
width: 90%;
align-items: center;
align-content: center;
justify-content: center;
margin: 0 auto;
}
.custom-selected-field {
display: flex;
flex-direction: column;
margin-bottom: 16px;
}
.box-btns {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,268 @@
<script setup>
import { ref, onMounted, reactive, computed } from 'vue';
import CardEmpty from './CardEmpty.vue';
import Spiner from './ui/Spiner.vue';
import BadgeError from './ui/BadgeError.vue';
import { GoogleMap, Marker } from 'vue3-google-map';
import useDirectionsRender from '../composables/useDirectionRender';
import { useAuthStore } from '../stores/auth';
import { useCompanyStore } from '../stores/company';
import { useVehiclesStore } from '../stores/vehicles';
import { saveProposal } from '../services/vehicles'
import Swal from 'sweetalert2';
const zoom = ref(6);
const heightMap = ref(768);
const originCoords = ref(null);
const destinationCoords = ref(null);
const isLoading = ref(false);
const loadingSubmit = ref(false);
const windowWidth = ref(window.innerWidth);
const authStore = useAuthStore();
const vehiclesStore = useVehiclesStore();
const msgError = ref('');
const form = reactive({
vehicle: "",
comments: '',
});
const { geocodeAddress } = useDirectionsRender()
const props = defineProps({
load: {
type: Object,
required: true,
},
proposal: {
type: Object,
}
})
const companyStore = useCompanyStore();
defineEmits(['reset-load'])
onMounted(() => {
window.addEventListener('resize', handleResize);
if(window.innerWidth <= 1024) {
zoom.value = 4;
heightMap.value = 420;
}
initData();
});
const initData = async() => {
isLoading.value = true;
let filterQuery = [];
filterQuery.company = "company="+ authStore.user.company
await vehiclesStore.fetchVehicles(filterQuery);
originCoords.value = await geocodeAddress(props.load.origin_formatted_address);
destinationCoords.value = await geocodeAddress(props.load.destination_formatted_address);
isLoading.value = false;
console.log(props.proposal);
if(props.proposal) {
form.vehicle = props.proposal.vehicle._id;
form.comments = props.proposal.comment;
}
}
const handleResize = () => {
windowWidth.value = window.innerWidth
if(windowWidth.value <= 1024){
zoom.value = 4
heightMap.value = 420;
} else {
zoom.value = 6;
heightMap.value = 768;
}
}
const handleSumit = async() => {
if(form.vehicle === ""){
msgError.value = 'Selecciona vehiculo para continuar';
setTimeout(() => {
msgError.value = '';
}, 5000);
return;
} else if (form.comments.trim().length <= 0) {
msgError.value = 'Agrega un comentario';
setTimeout(() => {
msgError.value = '';
}, 5000);
return;
}
msgError.value = '';
let result;
let action;
if(!props.proposal) {
let formData = {
carrier: authStore.user.company,
bidder : authStore.user._id,
comment: form.comments,
vehicle : form.vehicle,
load : props.load._id
}
loadingSubmit.value = true;
action = 'creada';
result = await companyStore.createPropsal(formData);
} else {
let formData = {
comment: form.comments,
vehicle : form.vehicle,
}
const index = vehiclesStore.vehicles.findIndex((prop) => prop._id === form.vehicle);
const vehicleSelected = vehiclesStore.vehicles[index];
console.log(vehicleSelected);
const localData = {
vehicle: vehicleSelected,
load: props.load,
_driver: vehicleSelected?.driver.first_name + ' ' + vehicleSelected?.driver.last_name
}
loadingSubmit.value = true;
action = 'actualizada'
result = await companyStore.updatePropsalLoad(props.proposal._id, formData, localData);
}
if(result === 'success') {
document.getElementById('btnClosemakeProposalModal').click();
Swal.fire({
title: `<strong>Oferta ${action} con éxito!</strong>`,
icon: 'success'
})
} else {
Swal.fire({
title: result,
icon: 'error'
})
}
loadingSubmit.value = false;
}
const title = computed(() => (props.proposal) ? 'Modificar oferta' : 'Realizar oferta');
const btnSubmit = computed(() => (props.proposal) ? 'Guardar' : 'Enviar');
</script>
<template>
<div class="modal fade" id="makeProposalModal" tabindex="-1" role="dialog" aria-labelledby="editcompany" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">{{ title }}</h2>
<button
id="btnClosemakeProposalModal"
type="button"
class="close bg-white"
data-dismiss="modal"
aria-label="Close"
@click="$emit('reset-load')"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body view-proposals">
<Spiner v-if="isLoading"/>
<div v-else>
<div v-if="load">
<form @submit.prevent="handleSumit" class="box-form mb-4">
<BadgeError :msg="msgError"/>
<div class="custom-selected-field">
<label class="custom-label" for="vehicle">Vehiculo:</label>
<select
class="custom-input-light"
name="vehicle"
id="vehicle"
v-model="form.vehicle"
>
<option disabled value="">-- Seleccionar vehículo --</option>
<option v-for="vehicle in vehiclesStore.vehicles" :value="vehicle._id">{{vehicle.vehicle_code}}</option>
</select>
</div>
<div class="custom-selected-field">
<label class="custom-label" for="comment">Comentario:</label>
<textarea
class="custom-input-light"
name="comment"
id="comment"
placeholder="Escribe aqui..."
v-model="form.comments"
></textarea>
</div>
<div class="box-btns">
<Spiner v-if="loadingSubmit"/>
<input
v-else
type="submit"
class="btn-primary-sm"
:value="btnSubmit"
/>
</div>
</form>
<GoogleMap
v-if="!proposal"
api-key="AIzaSyAJtfvrAKy7vnUSv2nzk4dYQkOs3OP4MMs"
:center="{lat:19.432600, lng:-99.133209}"
:zoom="zoom"
:min-zoom="2"
:max-zoom="12"
:style="{width: 100 + '%', height: heightMap + 'px'}"
:options="{
zoomControl: true,
mapTypeControl: true,
scaleControl: true,
streetViewControl: true,
rotateControl: true,
fullscreenControl: true
}"
>
<Marker v-if="originCoords" :options="{position: originCoords, label: 'O', title: 'Destino'}" />
<Marker v-if="destinationCoords" :options="{position: destinationCoords, label: 'D', title: 'Origen'}" />
<!-- <Polyline :options="{
path: polyline,
// geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 2
}" /> -->
</GoogleMap>
</div>
<CardEmpty v-else text="No hay coincidencia"/>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-dark"
data-dismiss="modal"
@click="$emit('reset-load')"
>Cerrar</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.box-form {
width: 90%;
align-items: center;
align-content: center;
justify-content: center;
margin: 0 auto;
}
.custom-selected-field {
display: flex;
flex-direction: column;
margin-bottom: 16px;
}
.box-btns {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,112 @@
<script setup>
import { onMounted, ref } from "vue";
import { GoogleMap, Marker, Polyline } from "vue3-google-map";
const windowWidth = ref(window.innerWidth);
const zoom = ref(6);
const heightMap = ref(768);
const originCoords = ref(null);
const destinationCoords = ref(null);
const polyline = ref([]);
// const startLocation = ref({ lat: 37.7749, lng: -122.4194 }); // San Francisco
// const endLocation = ref({ lat: 34.0522, lng: -118.2437 }); // Los Angeles
onMounted(async() => {
window.addEventListener('resize', handleResize);
// mapRef.value = this.$refs.myMap;
if(window.innerWidth <= 1024) {
zoom.value = 4;
heightMap.value = 420;
}
originCoords.value = await geocodeAddress('C. 40 370, San Román, 97540 Izamal, Yuc.');
destinationCoords.value = await geocodeAddress('Izamal-Valladolid, 97557 Sudzal, Yuc.');
// Trazar la ruta entre el origen y el destino
// await getDirections();
});
const handleResize = () => {
windowWidth.value = window.innerWidth
if(windowWidth.value <= 1024){
zoom.value = 4
heightMap.value = 420;
} else {
zoom.value = 6;
heightMap.value = 768;
}
}
const geocodeAddress = async (address) => {
// Utiliza la API de geocodificación de Google Maps para obtener las coordenadas
const apiKey = 'AIzaSyAJtfvrAKy7vnUSv2nzk4dYQkOs3OP4MMs'; // Reemplaza con tu clave de API
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
address
)}&key=${apiKey}`
);
const data = await response.json();
const location = data.results[0].geometry.location;
console.log('location: ', location);
return location;
};
const getDirections = async () => {
const apiKey = 'AIzaSyAJtfvrAKy7vnUSv2nzk4dYQkOs3OP4MMs';
const originLatLng = `${originCoords.value.lat},${originCoords.value.lng}`;
const destinationLatLng = `${destinationCoords.value.lat},${destinationCoords.value.lng}`;
try {
const response = await fetch(
`https://maps.googleapis.com/maps/api/directions/json?origin=${originLatLng}&destination=${destinationLatLng}&key=${apiKey}`
);
const data = await response.json();
if (data.routes && data.routes.length > 0) {
const steps = data.routes[0].legs[0].steps;
const routePolyline = steps.map((step) => ({
lat: step.end_location.lat,
lng: step.end_location.lng,
}));
polyline.value = routePolyline;
}
} catch (error) {
console.log(error);
}
};
</script>
<template>
<GoogleMap
api-key="AIzaSyAJtfvrAKy7vnUSv2nzk4dYQkOs3OP4MMs"
:center="{lat:19.432600, lng:-99.133209}"
:zoom="zoom"
:min-zoom="2"
:max-zoom="12"
:style="{width: 100 + '%', height: heightMap + 'px'}"
:options="{
zoomControl: true,
mapTypeControl: true,
scaleControl: true,
streetViewControl: true,
rotateControl: true,
fullscreenControl: true
}"
>
<Marker :options="{position: originCoords, label: 'O', title: 'Destino'}" />
<Marker :options="{position: destinationCoords, label: 'D', title: 'Origen'}" />
<Polyline :options="{
path: polyline,
// geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 2
}" />
</GoogleMap>
</template>
<style scoped>
</style>

View File

@@ -6,12 +6,13 @@
$(document).ready(function() { $(document).ready(function() {
$('#sidebarCollapse').on('click', function () { $('#sidebarCollapse').on('click', function () {
$('#sidebar').toggleClass('active'); $('#sidebar').toggleClass('active');
$('#custom-navbar').toggleClass('active');
}); });
}); });
</script> </script>
<template> <template>
<nav class="navbar navbar-expand-lg navbar-light custom-navbar"> <nav class="navbar navbar-expand-lg navbar-light custom-navbar" id="custom-navbar">
<div class="nav-items"> <div class="nav-items">
<button type="button" id="sidebarCollapse" class="btn btn-info btn-menu"> <button type="button" id="sidebarCollapse" class="btn btn-info btn-menu">
<i class="fas fa-align-left"></i> <i class="fas fa-align-left"></i>
@@ -20,11 +21,15 @@
<RouterLink <RouterLink
v-if="auth.user?.permissions.includes('role_shipper')" v-if="auth.user?.permissions.includes('role_shipper')"
active-class="router-link-active" active-class="router-link-active"
class="nav-link" :to="{name: 'carriers'}">Transporte</RouterLink> class="nav-link" :to="{name: 'carriers'}">Transportistas</RouterLink>
<RouterLink <RouterLink
v-if="auth.user?.permissions.includes('role_carrier')" v-if="auth.user?.permissions.includes('role_carrier')"
active-class="router-link-active" active-class="router-link-active"
class="nav-link" :to="{name: 'shippers'}">Cargas</RouterLink> class="nav-link" :to="{name: 'search-loads'}">Cargas</RouterLink>
<RouterLink
v-if="auth.user?.permissions.includes('role_carrier')"
active-class="router-link-active"
class="nav-link" :to="{name: 'shippers'}">Embarcadores</RouterLink>
</div> </div>
</div> </div>
</nav> </nav>
@@ -70,6 +75,12 @@
color: #282727; color: #282727;
} }
#custom-navbar.active {
margin-left: 220px;
display: block;
width: calc(100% - 220px) !important;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.custom-navbar { .custom-navbar {

View File

@@ -0,0 +1,91 @@
<script setup>
import { onMounted, ref } from 'vue';
const props = defineProps({
total: {
type: Number,
required: true,
},
limit: {
type: Number,
default: 10
},
currentPage: {
type: Number,
default: 1
}
})
const emits = defineEmits(['get-elements'])
const currentPage = ref(1);
const totalPage = ref(0)
onMounted(() => {
currentPage.value = props.currentPage;
totalPage.value = Math.ceil(props.total / props.limit)
})
const setPage = (p) => {
currentPage.value = p
const skip = (p - 1) * props.limit;
emits('get-elements', {skip: skip, page: p});
}
</script>
<template>
<div class="pagination" v-if="totalPage > 1">
<h4>Paginación</h4>
<div class="box-pages" v-if="totalPage > 1">
<div
v-for="p in totalPage"
:class="[currentPage === p ? 'page page-active' : 'page']"
@click="setPage(p)"
>
{{ p }}
</div>
</div>
</div>
</template>
<style scoped>
.pagination {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 2rem;
}
.pagination h4 {
font-size: 1.2rem;
font-weight: 900;
}
.box-pages {
/* left: 50%; */
display: flex;
/* flex-direction: column; */
justify-content: center;
}
.page {
cursor: pointer;
display: flex;
width: 30px;
height: 30px;
border-radius: 100%;
align-items: center;
justify-content: center;
align-content: center;
background-color: #dab977;
color: #323030;
margin: 4px;
font-size: 1.1rem;
font-weight: 500;
}
.page-active {
width: 33px;
height: 33px;
font-size: 1.2rem;
font-weight: 700;
background-color: #FBBA33;
}
</style>

View File

@@ -0,0 +1,263 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useLoadsStore } from '../stores/loads';
import { useAuthStore } from '../stores/auth';
import Spiner from './ui/Spiner.vue';
import { getDateMonthDay } from '../helpers/date_formats';
import VehicleInfo from './VehicleInfo.vue';
import Swal from 'sweetalert2'
import CardEmpty from './CardEmpty.vue';
const loadsStore = useLoadsStore();
const authStore = useAuthStore();
const isLoading = ref(false);
const isLoadingActions = ref(false);
onMounted(() => {
getProposalsData()
});
const getProposalsData = async() => {
isLoading.value = true;
await loadsStore.getProposalsOfLoads(loadsStore.currentLoad._id);
isLoading.value = false;
}
const clearMoal = () => {
loadsStore.openProposalsModal = false;
loadsStore.currentLoad = false;
}
const handleAceptedProposal = async(proposal) => {
const load_id = proposal.load._id;
let loadData = {
status : "Completed",
contract_start_date: new Date(),
carrier: proposal.carrier._id,
vehicle: proposal.vehicle._id,
}
isLoadingActions.value = true;
let load = await loadsStore.updateLoad(load_id, loadData);
if(load != null) {
const index = loadsStore.loads.findIndex((load) => load._id === load_id);
loadsStore.loads[index] = {
...loadsStore.loads[index],
...load
};
const proposal_id = proposal._id;
let formData = {
accepted_by : authStore.user,
accepted_date : new Date(),
is_accepted : true,
}
const resp = await loadsStore.updateProposal(proposal_id, formData);
if(resp){
const index = loadsStore.proposalsOfLoads.findIndex((p) => p._id === proposal_id);
loadsStore.proposalsOfLoads[index] = {
...proposal,
...formData
};
Swal.fire({
title: "Oferta aceptada!",
text: "La oferta fue aceptada exitosamente.",
icon: "success"
});
} else {
Swal.fire({
title: "Error!",
text: "No se pudo actualizar oferta, intente más tarde.",
icon: "error"
});
}
} else {
Swal.fire({
title: "Error!",
text: "No se pudo aceptar oferta, intente más tarde.",
icon: "error"
});
}
isLoadingActions.value = false;
}
const handleCancelProposal = async(proposal) => {
const proposal_id = proposal._id;
const load_id = proposal.load._id;
const {isConfirmed} = await Swal.fire({
title: 'Cancelar oferta!',
text: '¿Estás seguro de cancelar esta oferta?',
icon: 'warning',
cancelButtonColor: "#d33",
showCancelButton: true,
confirmButtonText: 'Si, cancelar',
cancelButtonText: 'No'
})
if( isConfirmed ) {
let loadData = {
status : "Published",
contract_start_date: null,
carrier: null,
vehicle: null,
}
isLoadingActions.value = true;
let load = await loadsStore.updateLoad(load_id, loadData);
if(load) {
const index = loadsStore.loads.findIndex((load) => load._id === load_id);
loadsStore.loads[index] = {
...loadsStore.loads[index],
...load
};
let formData = {
accepted_by : null,
accepted_date : null,
is_accepted : false,
}
const resp = await loadsStore.updateProposal(proposal_id, formData);
if(resp) {
const index = loadsStore.proposalsOfLoads.findIndex((p) => p._id === proposal._id);
loadsStore.proposalsOfLoads[index] = {
...proposal,
...formData
};
Swal.fire({
title: "Oferta cancelada!",
text: "La oferta fue retirada exitosamente.",
icon: "success"
});
} else {
Swal.fire({
title: "Error!",
text: "No se pudo retirar oferta, intente más tarde",
icon: "error"
});
}
} else {
Swal.fire({
title: "Error!",
text: "Algo salio mal, intente más tarde",
icon: "error"
});
}
isLoadingActions.value = false;
}
}
</script>
<template>
<div class="modal fade" id="proposalsModal" tabindex="-1" role="dialog" aria-labelledby="editcompany" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">Ofertas</h2>
<button
id="btnCloseProposalsModal"
type="button"
@click="clearMoal"
class="close bg-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body view-proposals">
<Spiner v-if="isLoading"/>
<div v-else>
<div v-if="loadsStore.proposalsOfLoads.length > 0" v-for="proposal in loadsStore.proposalsOfLoads" class="card-fixed card-proposal">
<div class="row">
<div class="col-lg-6 col-md-12">
<p>Empresa: <span>{{ proposal.carrier.company_name }}</span></p>
<p>Licitador: <span>{{ proposal.bidder.first_name }} {{ proposal.bidder.last_name }}</span></p>
<p># de registro del transportista: <span v-if="proposal.vehicle">{{proposal.vehicle.vehicle_code}}</span></p>
</div>
<div class="col-lg-6 col-md-12">
<p>Fecha: <span>{{ getDateMonthDay(proposal.createdAt) }}</span></p>
<p>Tipo de Transporte: <span v-if="proposal.vehicle">{{proposal.vehicle.truck_type}}</span></p>
<p>Transportista: <span v-if="proposal._driver">{{proposal._driver}}</span></p>
</div>
</div>
<div v-if="proposal.comment" class="box-note">
{{ proposal.comment }}
</div>
<VehicleInfo v-if="proposal.vehicle" :vehicle="proposal.vehicle"/>
<Spiner v-if="isLoadingActions"/>
<div class="d-flex justify-content-end gap-3" v-else>
<div v-if="proposal.is_accepted" class="indicator-check">
<i class="fa-solid fa-check"></i>
Aceptado
</div>
<button v-if="!proposal.is_accepted"
type="button"
class="btn-primary-sm"
@click="handleAceptedProposal(proposal)"
>
<i class="fa-solid fa-check"></i>
Aceptar
</button>
<button
v-if="proposal.load.load_status !== 'Delivered' && proposal.is_accepted"
class="btn-primary-sm"
@click="handleCancelProposal(proposal)"
>
<i class="fa-solid fa-ban clear-sm"></i>
Cancelar
</button>
</div>
</div>
<CardEmpty v-else text="No hay ofertas"/>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-dark"
@click="clearMoal"
data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.view-proposals {
width: 100%;
}
.card-proposal {
flex-direction: column;
width: 100% !important;
}
p {
font-size: 1rem;
font-weight: 400;
color: #323032;
font-weight: 700;
}
p span {
color: #323032;
font-weight: normal;
}
.indicator-check {
width: 120px;
padding: 10px 12px;
background: #FFF;
border: 1px solid green;
border-radius: 50px;
color: green;
}
.box-note {
padding: 12px 16px;
background-color: aqua;
border-radius: 13px;
}
</style>

View File

@@ -7,13 +7,14 @@
const auth = useAuthStore(); const auth = useAuthStore();
const handleLogout = () => { const handleLogout = () => {
auth.$patch({ // auth.$patch({
sesion: '', // sesion: '',
token: '', // token: '',
user: {}, // user: {},
}); // });
localStorage.removeItem('session'); // localStorage.removeItem('session');
router.push({name: 'login'}); // router.push({name: 'login'});
auth.logout();
} }
</script> </script>
@@ -70,12 +71,20 @@
class="nav-link" :to="{name: 'vehicles'}">Vehiculos</RouterLink> class="nav-link" :to="{name: 'vehicles'}">Vehiculos</RouterLink>
</div> </div>
</li> </li>
<li :class="[route.name === 'published' ? 'bg-nav-active' : '']"> <li v-if="auth.user?.permissions.includes('role_shipper')" :class="[route.name === 'published-loads' ? 'bg-nav-active' : '']">
<div> <div>
<i class="fa-solid fa-bullhorn" :class="[route.name === 'published' ? 'router-link-active' : '']"></i> <i class="fa-solid fa-bullhorn" :class="[route.name === 'published-loads' ? 'router-link-active' : '']"></i>
<RouterLink <RouterLink
active-class="" active-class=""
class="nav-link" :to="{name: 'published'}">Publicaciones</RouterLink> class="nav-link" :to="{name: 'published-loads'}">Publicaciones</RouterLink>
</div>
</li>
<li v-if="auth.user?.permissions.includes('role_carrier')" :class="[route.name === 'published-trucks' ? 'bg-nav-active' : '']">
<div>
<i class="fa-solid fa-bullhorn" :class="[route.name === 'published-trucks' ? 'router-link-active' : '']"></i>
<RouterLink
active-class=""
class="nav-link" :to="{name: 'published-trucks'}">Publicaciones</RouterLink>
</div> </div>
</li> </li>
<li :class="[route.name === 'calendar' ? 'bg-nav-active' : '']"> <li :class="[route.name === 'calendar' ? 'bg-nav-active' : '']">
@@ -96,20 +105,20 @@
class="nav-link" :to="{name: 'calculator'}">Calculadora</RouterLink> class="nav-link" :to="{name: 'calculator'}">Calculadora</RouterLink>
</div> </div>
</li> </li>
<li :class="[route.name === 'reports' ? 'bg-nav-active' : '']"> <!-- <li :class="[route.name === 'reports' ? 'bg-nav-active' : '']">
<div> <div>
<i class="fa-solid fa-chart-simple" :class="[route.name === 'reports' ? 'router-link-active' : '']"></i> <i class="fa-solid fa-chart-simple" :class="[route.name === 'reports' ? 'router-link-active' : '']"></i>
<RouterLink <RouterLink
active-class="router-link-active" active-class="router-link-active"
class="nav-link" :to="{name: 'reports'}">Reportes</RouterLink> class="nav-link" :to="{name: 'reports'}">Reportes</RouterLink>
</div> </div>
</li> </li> -->
</ul> </ul>
<div class="eta-info"> <div class="eta-info">
<div class="divider"></div> <div class="divider"></div>
<a class="link-eta" href="">Aviso de privaciadad</a> <RouterLink class="link-eta" :to="{name: 'notice-privacy'}" target="_blank">Aviso de privaciadad</RouterLink>
<a class="link-eta" href="">Terminos y condiciones</a> <RouterLink class="link-eta" :to="{name: 'terms-conditions'}" target="_blank">Términos y condiciones</RouterLink>
<a class="link-eta" href="">FAQS</a> <RouterLink class="link-eta" :to="{name: 'faqs'}" target="_blank">Faqs</RouterLink>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<i class="fa-solid fa-right-from-bracket"></i> <i class="fa-solid fa-right-from-bracket"></i>
<a @click="handleLogout" <a @click="handleLogout"
@@ -124,10 +133,17 @@
<style lang="scss" scoped> <style lang="scss" scoped>
#sidebar { #sidebar {
position: fixed;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
top: 0;
height: 100%;
// min-height: 100vh;
overflow-y: scroll;
bottom: 0;
width: 220px; width: 220px;
min-height: 100vh; left: 0;
z-index: 1030;
background: #323030; background: #323030;
color: #FFF; color: #FFF;
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.10)); filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.10));
@@ -154,6 +170,11 @@
font-weight: 900; font-weight: 900;
} }
#sidebar.active {
position: fixed;
}
#sidebar ul li { #sidebar ul li {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -163,6 +184,11 @@
background-color: #323030; background-color: #323030;
} }
.bg-nav-active {
opacity: 0.4;
transition: opacity 500ms ease-in-out;
}
#sidebar ul li div{ #sidebar ul li div{
display: flex; display: flex;
align-items: center; align-items: center;
@@ -191,6 +217,10 @@
margin-right: 1.2rem; margin-right: 1.2rem;
font-weight: 500; font-weight: 500;
} }
.router-link-active{
color: #FFF;
}
.nav-link:hover{ .nav-link:hover{
/* color: #413f3c; */ /* color: #413f3c; */
color: #FFF; color: #FFF;
@@ -223,11 +253,12 @@
@media (max-width: 768px) { @media (max-width: 768px) {
#sidebar { #sidebar {
margin-left: -220px; position: relative;
height: 100vh;
overflow: auto;
width: 220px;
z-index: 4;
} }
#sidebar.active {
margin-left: 0;
}
} }
</style> </style>

View File

@@ -0,0 +1,189 @@
<script setup>
import { onMounted, reactive, ref } from 'vue';
import Spiner from './ui/Spiner.vue';
import CustomRadioInput from './ui/CustomRadioInput.vue';
import { useVehiclesStore } from '../stores/vehicles';
import CustomInput from './ui/CustomInput.vue';
import States from './ui/States.vue';
import Cities from './ui/Cities.vue';
import Swal from 'sweetalert2';
const props = defineProps({
vehicle: {
type: Object
}
});
const statusSelected = ref(null);
const loading = ref(false);
const vehicleStore = useVehiclesStore();
onMounted(() => {
statusSelected.value = props.vehicle.is_available === true ? 'Availiable' : 'Booked'
formAvailiable.state = {state_name: props.vehicle.state};
formAvailiable.destino = props.vehicle.destino;
formAvailiable.city = {city_name: props.vehicle.city};
formAvailiable.available_date = props.vehicle.available_date?.substring(0, 10);
});
defineEmits(['reset-vehicle']);
const formAvailiable = reactive({
available_date: new Date(),
destino: '',
city : '',
state : '',
});
const errors = ref({
destino: null,
city : null,
state : null,
})
const handleSetStatusVehicle = async() => {
let vehicleData;
console.log(statusSelected.value);
if(statusSelected.value === 'Availiable') {
console.log('check validations');
validations();
if(errors.value.city || errors.value.state || errors.value.destino ) return;
vehicleData = {
available_date : formAvailiable.available_date,
destino: formAvailiable.destino,
city : formAvailiable.city.city_name,
state : formAvailiable.state.state_name,
is_available : true
}
} else {
vehicleData = {
available_date : null,
is_available : false
}
}
let localData = {
driver: props.vehicle.driver,
categories: props.vehicle.categories
}
loading.value = true;
const result = await vehicleStore.updateVehicleCompany(props.vehicle._id, vehicleData, localData);
loading.value = false;
if(result === 'success') {
document.getElementById('btnCloseeditStatusVehicle').click();
Swal.fire({
title: `<strong>Status del vehiculo actualizado con éxito!</strong>`,
icon: 'success'
})
} else {
Swal.fire({
title: result,
icon: 'error'
})
}
}
const validations = () => {
errors.value = {
state: (!formAvailiable.state) ? 'Seleccione estado' : null,
city: (!formAvailiable.city) ? 'Seleccione municipio' : null,
destino: (!formAvailiable.destino) ? 'Ingrese destino' : null,
};
}
</script>
<template>
<div class="modal fade" id="editStatusVehicle" tabindex="-1" role="dialog" aria-labelledby="editStatusVehicle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="title mt-2 mb-3">Cambiar status vehiculo</h2>
<button
id="btnCloseeditStatusVehicle"
type="button"
class="close bg-white"
data-dismiss="modal"
@click="$emit('reset-vehicle')"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body view-proposals">
<form @submit.prevent="handleSetStatusVehicle">
<div class="custom-selected-field">
<h4 class="custom-label my-3">Status del vehiculo</h4>
<div class="d-flex">
<CustomRadioInput
value="Booked"
label="Reservado"
:name="'status-vehicle' + vehicle._id"
v-model:typeselected="statusSelected"
/>
<CustomRadioInput
value="Availiable"
label="Disponible"
:name="'status-vehicle' + vehicle._id"
v-model:typeselected="statusSelected"
/>
</div>
</div>
<div v-if="statusSelected === 'Availiable'">
<CustomInput
label="Fecha de carga*"
type="date"
:filled="false"
name="date-load"
v-model:field="formAvailiable.available_date"
/>
<div class="mb-4 mt-3">
<label class="custom-label">Base de carga por Estado</label>
<States
v-model="formAvailiable.state"
/>
<span class="error-msg" v-if="errors.state">{{ errors.state }}</span>
</div>
<div class="mb-4 mt-3">
<label class="custom-label">Base de Carga por Municipio</label>
<Cities
v-model="formAvailiable.city"
/>
<span class="error-msg" v-if="errors.city">{{ errors.city }}</span>
</div>
<CustomInput
label="Destino*"
type="text"
:filled="false"
name="destino"
v-model:field="formAvailiable.destino"
:error="errors.destino"
/>
</div>
<div class="mt-4 text-center">
<Spiner v-if="loading === true"/>
<button
v-else
class="btn-primary-sm radius-sm"
type="submit"
>
<span class="clear-xsm">Cuardar</span>
</button>
</div>
</form>
</div>
<!-- <div class="modal-footer">
<button
type="button"
class="btn btn-dark radius-sm"
data-dismiss="modal"
@click="$emit('reset-vehicle')"
>Cerrar</button>
</div> -->
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,64 @@
<script setup>
import { ref } from 'vue';
defineProps({
vehicle: {
type: Object,
required: true
}
})
const isShow = ref(false);
const toogle = () => {
isShow.value = !isShow.value;
}
</script>
<template>
<a
@click="toogle"
class="btn-text mt-4 mb-2"
>Información del vehiculo <i class="fa-solid" :class="[isShow ? 'fa-chevron-up' : 'fa-chevron-down']"></i></a>
<div v-if="isShow">
<div class="divider"></div>
<!-- <h2 class="my-3">Información del vehiculo</h2> -->
<div class="row my-2">
<div class="col-lg-6">
<p>Código: <span>{{ vehicle.vehicle_code }}</span></p>
<p>Tipo de transporte: <span>{{ vehicle.truck_type }}</span></p>
<p>Número de Serie: <span>{{ vehicle.vehicle_number }}</span></p>
<p>Segmento: <span>{{ vehicle._categories }}</span></p>
</div>
<div class="col-lg-6">
<p>Placas Tracto Camión: <span>{{ vehicle.circulation_serial_number }}</span></p>
<p>Placas Remolque 1: <span>{{ vehicle.trailer_plate_1 }}</span></p>
<p>Placas Remolque 2: <span>{{ vehicle.trailer_plate_2 }}</span></p>
<p>Base de carga: <span>{{ vehicle.city }}, {{ vehicle.state }}</span></p>
</div>
</div>
<div class="col-12">
<p>Información Adicional del Transporte: <span>{{ vehicle.notes }}</span></p>
</div>
</div>
</template>
<style scoped>
h2 {
font-size: 1.2rem;
font-weight: 500;
}
p {
font-size: 1rem;
font-weight: 400;
color: #323032;
font-weight: 700;
}
p span {
color: #323032;
font-weight: normal;
}
</style>

View File

@@ -0,0 +1,43 @@
// import { MapElementFactory } from "vue2-google-map";
// export default MapElementFactory({
// name: "directionsRenderer",
// ctr() {
// return google.maps.DirectionsRenderer;
// },
// events: [],
// mappedProps: {},
// props: {
// origin: { type: Object },
// destination: { type: Object },
// travelMode: { type: String }
// },
// afterCreate(directionsRenderer) {
// let directionsService = new google.maps.DirectionsService();
// this.$watch(
// () => [this.origin, this.destination, this.travelMode],
// () => {
// let { origin, destination, travelMode } = this;
// if (!origin || !destination || !travelMode) return;
// directionsService.route(
// {
// origin,
// destination,
// travelMode
// },
// (response, status) => {
// if (status !== "OK") return;
// directionsRenderer.setDirections(response);
// }
// );
// }
// );
// }
// });

View File

@@ -0,0 +1,56 @@
<script setup>
defineProps({
msg: {
type: String,
required: true,
},
isError: {
type: Boolean,
default: true,
},
showIcon: {
type: Boolean,
default: true,
}
})
</script>
<template>
<div v-if="msg.length > 0" class="badge" :class="[isError ? 'badge-error' : 'badge-success']">
<span>
<i v-if="showIcon && isError" class="fa-solid fa-circle-exclamation me-2"></i>
<i v-if="showIcon && !isError" class="fa-solid fa-circle-check"></i>
{{ msg }}
</span>
</div>
</template>
<style scoped>
.badge {
display: flex;
width: 100%;
color: #FFF;
font-size: 1rem;
font-weight: 700;
border-radius: 8px;
justify-content: center;
padding: 10px 16px;
margin-bottom: 12px;
}
.badge-error {
background-color: rgb(238, 101, 101);
}
.badge-success {
background-color: rgb(29, 162, 113);
}
@media (max-width: 768px) {
.badge {
font-size: 0.8rem;
font-weight: 400;
border-radius: 8px;
padding: 10px 12px;
}
}
</style>

View File

@@ -14,6 +14,10 @@
multiple: { multiple: {
type: Boolean, type: Boolean,
default: false default: false
},
disabled: {
type: Boolean,
default: false
} }
}); });
defineEmits(['update:selectedCities', 'clear-option']) defineEmits(['update:selectedCities', 'clear-option'])
@@ -35,6 +39,7 @@
:searchable="true" :searchable="true"
:loading="isLoading" :loading="isLoading"
:close-on-select="true" :close-on-select="true"
:disabled="disabled"
@search-change="searchState" @search-change="searchState"
@remove="$emit('clear-option')" @remove="$emit('clear-option')"
placeholder="Busca por ciudad" placeholder="Busca por ciudad"

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
defineProps({ const props = defineProps({
field: { field: {
type: String type: [String, Number, Date]
}, },
label: { label: {
type: String, type: String,
@@ -12,6 +12,11 @@
required: false, required: false,
default: true, default: true,
}, },
readonly: {
type: Boolean,
required: false,
default: false,
},
name: { name: {
type: String, type: String,
required: true, required: true,
@@ -23,10 +28,18 @@
helpText: { helpText: {
type: String, type: String,
default: '', default: '',
},
required: {
type: Boolean,
default: false,
},
error: {
type: String,
} }
}) })
defineEmits(['update:field']) defineEmits(['update:field'])
</script> </script>
<template> <template>
@@ -38,9 +51,15 @@
:type="type" :type="type"
:id="name" :id="name"
:name="name" :name="name"
:min="0"
:step="0.1"
:value="field" :value="field"
:disabled="readonly"
:readonly="readonly"
:required="required"
@input="$event => $emit('update:field', $event.target.value)"> @input="$event => $emit('update:field', $event.target.value)">
<span class="help" v-if="helpText.length > 0">{{ helpText }}</span> <span class="help" v-if="helpText.length > 0">{{ helpText }}</span>
<span class="error-msg" v-if="error">{{ error }}</span>
</div> </div>
</template> </template>
@@ -50,4 +69,10 @@
font-weight: 300; font-weight: 300;
color: rgb(108, 92, 92); color: rgb(108, 92, 92);
} }
.error-msg {
color: red;
font-size: 12px;
font-weight: 300;
}
</style> </style>

View File

@@ -17,18 +17,14 @@
const loading = ref(false); const loading = ref(false);
const msgError = ref(''); const msgError = ref('');
const msgSuccess = ref('');
const companyCategories = ref([]); const companyCategories = ref([]);
const companyStates = ref([]); const companyStates = ref([]);
const companyCity = ref([]); const companyCity = ref([]);
const companyTruckType = ref([]); const companyTruckType = ref([]);
onMounted(() => { onMounted(() => {
console.log('EditCompanyModal');
if(companyStore.company){ if(companyStore.company){
companyCategories.value = companyStore.company.categories.map(m =>{ companyCategories.value = companyStore.company.categories;
return { name: m.name };
});
} }
if(companyStore.company){ if(companyStore.company){
companyStates.value = companyStore.company.company_state.map(m =>{ companyStates.value = companyStore.company.company_state.map(m =>{
@@ -54,17 +50,34 @@
description: companyStore.company?.company_description ?? '', description: companyStore.company?.company_description ?? '',
}); });
const handleSave = () => { const handleSave = async() => {
// const resp = editCompany() // const resp = editCompany()
const resp = validations(); const resp = validations();
if(resp !== '') { if(resp !== '') {
msgError.value = resp; msgError.value = resp;
clearMessages(); clearMessages();
} else { } else {
// $('#editcompanymodal').modal('toggle'); loading.value = true;
document.getElementById('btnCloseEditCompany').click(); let companyData = {
notifications.show = true; is_company: companyStore.company._id,
notifications.text = 'Empresa actualizada'; company_type: companyStore.company.company_type,
meta_data: companyStore.company.meta_data,
categories: company.segments,
company_city: company.cities.map((e) => e.city_name),
company_state: company.states.map((e) => e.state_name),
truck_type: company.truckTypes.map((e) => e.meta_value),
company_description: company.description
};
const result = await companyStore.editCompany(companyData);
loading.value = false;
if(result === 'success') {
document.getElementById('btnCloseEditCompany').click();
notifications.show = true;
notifications.text = 'Empresa actualizada';
} else {
msgError.value === result;
clearMessages();
}
} }
} }
@@ -89,6 +102,7 @@
} }
</script> </script>
<template> <template>
@@ -139,8 +153,11 @@
:filled="false" :filled="false"
v-model:field="company.description" v-model:field="company.description"
/> />
<Spiner v-if="loading"/>
<input type="submit" value="Cuardar cambios" class="btn-primary-lg btn-lg-block my-4"> <input
v-else
type="submit"
value="Cuardar cambios" class="btn-primary-lg btn-lg-block my-4">
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@@ -17,6 +17,7 @@
<style scoped> <style scoped>
.noty-fixed { .noty-fixed {
position: fixed; position: fixed;
z-index: 2000;
right: 0px; right: 0px;
top: 0px; top: 0px;
left: 0px; left: 0px;

View File

@@ -0,0 +1,66 @@
<script setup>
import { ref } from 'vue';
import VueMultiselect from 'vue-multiselect'
import { searchProducts } from '../../services/public';
const options = ref([]);
const isLoading = ref(false);
// defineProps(['selectedCategory']);
defineProps({
selectedProduct: {
type: Array
},
multiple: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
});
defineEmits(['update:selectedProduct', 'clear-option'])
const searchProductFn = async(query) => {
isLoading.value = true;
const resp = await searchProducts(query);
options.value = resp;
isLoading.value = false;
// truckTypes.value = resp;
}
</script>
<template>
<VueMultiselect
:value="selectedProduct"
@input="$event => $emit('update:selectedProduct', $event.target.value)"
:options="options"
:multiple="multiple"
:searchable="true"
:loading="isLoading"
:close-on-select="true"
:disabled="disabled"
@search-change="searchProductFn"
@remove="$emit('clear-option')"
placeholder="Busca por producto"
label="name"
track-by="name"
selectLabel="Presione para seleccionar"
selectedLabel="Selecionado"
deselectLabel="Presione para remover seleción"
>
<template #noResult>
Oops! No se encontro coincidencias.
</template>
<template #noOptions>
Lista vacia.
</template>
</VueMultiselect>
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style scoped>
</style>

View File

@@ -14,6 +14,10 @@
multiple: { multiple: {
type: Boolean, type: Boolean,
default: false default: false
},
disabled: {
type: Boolean,
default: false
} }
}); });
defineEmits(['update:selectedTruckType', 'clear-option']) defineEmits(['update:selectedTruckType', 'clear-option'])
@@ -38,6 +42,7 @@
:multiple="multiple" :multiple="multiple"
:loading="isLoading" :loading="isLoading"
:searchable="true" :searchable="true"
:disabled="disabled"
:close-on-select="true" :close-on-select="true"
@search-change="getTruckTypesQuery" @search-change="getTruckTypesQuery"
@remove="$emit('clear-option')" @remove="$emit('clear-option')"

View File

@@ -0,0 +1,30 @@
import { ref } from "vue";
import { useLoadsStore } from "../stores/loads";
import api from "../lib/axios";
export default function useAttachments() {
const loading = ref(false);
const attachments = ref(null);
const loadStore = useLoadsStore();
const getAttachmentLoad = async() => {
try {
loading.value = true;
const endpoint = "/v1" + "/load-attachments/load/" + loadStore.currentLoad._id;
const {data} = await api.get(endpoint);
attachments.value = data;
} catch (error) {
attachments.value = null;
console.log(error);
} finally {
loading.value = false;
}
}
return {
getAttachmentLoad,
loading,
attachments
}
}

View File

@@ -0,0 +1,54 @@
import { ref } from "vue";
export default function useDirectionsRender() {
const originCoords = ref(null);
const destinationCoords = ref(null);
const polylines = ref([]);
const geocodeAddress = async (address) => {
// Utiliza la API de geocodificación de Google Maps para obtener las coordenadas
const apiKey = 'AIzaSyAJtfvrAKy7vnUSv2nzk4dYQkOs3OP4MMs'; // Reemplaza con tu clave de API
try {
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
address
)}&key=${apiKey}`
);
const data = await response.json();
const location = data.results[0].geometry.location;
return location;
} catch (error) {
return null;
}
};
const getDirections = async () => {
const apiKey = 'AIzaSyAJtfvrAKy7vnUSv2nzk4dYQkOs3OP4MMs';
const originLatLng = `${originCoords.value.lat},${originCoords.value.lng}`;
const destinationLatLng = `${destinationCoords.value.lat},${destinationCoords.value.lng}`;
try {
const response = await fetch(
`https://maps.googleapis.com/maps/api/directions/json?origin=${originLatLng}&destination=${destinationLatLng}&key=${apiKey}`
);
const data = await response.json();
if (data.routes && data.routes.length > 0) {
const steps = data.routes[0].legs[0].steps;
const routePolyline = steps.map((step) => ({
lat: step.end_location.lat,
lng: step.end_location.lng,
}));
polylines.value = routePolyline;
}
} catch (error) {
console.log(error);
}
};
return {
originCoords,
geocodeAddress,
getDirections,
polylines
}
}

View File

@@ -1,9 +1,13 @@
import { ref } from "vue"; import { ref } from "vue";
import { getCompanies } from '../services/public'; import { getCompanies, getPublicUsersCompany } from '../services/public';
export default function useDirecty() { export default function useDirectory() {
const companies = ref([]); const companies = ref([]);
const loading = ref(false); const loading = ref(false);
const users = ref([]);
const companiesTotal = ref(0);
const currentCompaniesPage = ref(1);
const getCompaniesData = async(filterQuery) => { const getCompaniesData = async(filterQuery) => {
let filterArr = Object.values(filterQuery); let filterArr = Object.values(filterQuery);
@@ -17,13 +21,32 @@ export default function useDirecty() {
loading.value = true; loading.value = true;
const resp = await getCompanies(filterStr); const resp = await getCompanies(filterStr);
companies.value = resp; if(resp !== null) {
companies.value = resp.data;
companiesTotal.value = resp.total;
console.log(companiesTotal.value)
} else {
companies.value = [];
companiesTotal.value = 0;
}
loading.value = false;
}
const getUsersData = async(companyId) => {
const filter = companyId;
loading.value = true;
const resp = await getPublicUsersCompany(filter);
users.value = resp;
loading.value = false; loading.value = false;
} }
return { return {
getCompaniesData, getCompaniesData,
getUsersData,
users,
loading, loading,
companies companies,
companiesTotal,
currentCompaniesPage
} }
} }

View File

@@ -0,0 +1,39 @@
import { ref } from "vue";
import api from "../lib/axios";
export default function useSearchLoads() {
const loads = ref([]);
const loading = ref(false);
const total = ref(0);
const currentPage = ref(1);
const getLoadsPublished = async(filterQuery) => {
loading.value = true;
let filterArr = Object.values(filterQuery);
let cleanfilterArr = filterArr.filter(n=>n);
var filterStr = "";
if(cleanfilterArr.length >0){
filterStr = "?"+cleanfilterArr.join("&");
}
try {
const endpoint = `/loads/${filterStr}&$sort%5BcreatedAt%5D=-1`;
const {data} = await api.get(endpoint);
console.log(data);
total.value = data.total;
loads.value = data.data;
} catch (error) {
loads.value = [];
total.value = 0;
console.log(error);
}
loading.value = false;
}
return {
getLoadsPublished,
loading,
loads,
total,
currentPage,
}
}

62
src/data/events.json Normal file
View File

@@ -0,0 +1,62 @@
[
{
"status": "Delivered",
"date": "2023-01-05T03:24:18.821Z",
"shipment_code": "ETA1010"
},
{
"status": "Delivered",
"date": "2023-12-05T03:24:18.821Z",
"shipment_code": "ETA1010"
},
{
"status": "Delivered",
"date": "2023-12-24T01:00:57.627Z",
"shipment_code": "ETA1007"
},
{
"status": "Delivered",
"date": "2023-12-20T19:36:13.562Z",
"shipment_code": "ETA1014"
},
{
"status": "Published",
"date": "2023-12-17T00:29:10.471Z",
"shipment_code": "ETA1021"
},
{
"status": "Delivered",
"date": "2023-12-11T02:47:28.823Z",
"shipment_code": "ETA1013"
},
{
"status": "Loading",
"date": "2023-12-12T02:47:28.823Z",
"shipment_code": "ETA1023"
},
{
"status": "Transit",
"date": "2023-12-13T02:47:28.823Z",
"shipment_code": "ETA1024"
},
{
"status": "Downloading",
"date": "2023-12-13T02:47:28.823Z",
"shipment_code": "ETA1025"
},
{
"status": "Delivered",
"date": "2023-12-04T02:31:45.679Z",
"shipment_code": "ETA1006"
},
{
"status": "Published",
"date": "2023-12-21T00:42:43.588Z",
"shipment_code": "ETA1022"
},
{
"status": "Published",
"date": "2023-12-26T00:57:26.432Z",
"shipment_code": "ETA1016"
}
]

218
src/data/faqs.json Normal file
View File

@@ -0,0 +1,218 @@
[
{
"section": "Cuenta",
"items": [
{
"question": "¿Cómo puedo registrarme como embarcador o transportista en la plataforma?",
"answer": "",
"steps": [
"Visita la siguiente URL: [URL de registro]",
"En la parte inferior de la página, haz clic en <b>Regístrate aquí </b>.",
"Ingresa tu dirección de correo electrónico válida y crea una contraseña. Se te enviará un código de un solo uso a tu correo electrónico. Ingresa este código y haz clic en <b>Continuar</b>.",
"Serás redirigido a una sección para completar tu registro. Aquí podrás seleccionar si deseas registrarte como embarcador o transportista, y deberás indicar si eres persona física o moral.",
"Completa el perfil de tu empresa, proporcionando detalles como tu RFC, el segmento al que perteneces, estado, ciudad, tipo de transporte más utilizado, entre otros.",
"Luego, completa el perfil del usuario principal y haz clic en <b>Guardar</b>.",
"¡Listo! Ahora puedes iniciar sesión en tu cuenta. ¡Bienvenido a nuestra plataforma!"
],
"notes": ""
},
{
"question": "¿Cómo puedo registrarme como broker en la plataforma?",
"answer": "",
"steps": [
"Visita la siguiente URL: [URL de registro].",
"En la parte inferior de la página, haz clic en <b>Regístrate aquí</b>.",
"Ingresa tu dirección de correo electrónico válida y crea una contraseña. Se te enviará un código de un solo uso a tu correo electrónico. Ingresa este código y haz clic en <b>Continuar</b>.",
"Serás redirigido a una sección para completar tu registro. Aquí podrás seleccionar si deseas registrarte como Broker(embarcador) o Broker(transportista), y deberás indicar si eres persona física o moral.",
"Completa el perfil de tu empresa, proporcionando detalles como tu RFC, el segmento al que perteneces, estado, ciudad, tipo de transporte más utilizado, entre otros.",
"Luego, completa el perfil del usuario principal y haz clic en <b>Guardar</b>.",
"¡Listo! Ahora puedes iniciar sesión en tu cuenta. ¡Bienvenido a nuestra plataforma!"
],
"notes": ""
},
{
"question": "¿Olvide mi contraseña como puedo recuperarla?",
"answer": "Si has olvidado tu contraseña, sigue estos pasos para recuperarla:",
"steps": [
"Visita la siguiente URL: [URL de recuperación de contraseña].",
"Ingresa tu correo electrónico registrado y crea una nueva contraseña. Luego, haz clic en <b>Continuar</b>.",
"Recibirás un correo electrónico con un código de un solo uso para validar tu identidad. Ingresa este código en el campo designado y haz clic en <b>Continuar</b>.",
"Una vez validada tu identidad, serás redirigido al formulario de inicio de sesión. Ingresa tus nuevas credenciales y haz clic en <b>Iniciar sesión</b>."
],
"notes": "¡Listo! Ahora podrás acceder a tu cuenta con tu nueva contraseña."
},
{
"question": "¿Cómo protege la plataforma la privacidad y seguridad de mis datos?",
"answer": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur libero id faucibus nisl tincidunt eget nullam non nisi."
},
{
"question": "¿Cómo puedo mejorar mi perfil y aumentar mi visibilidad en la plataforma?",
"answer": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur libero id faucibus nisl tincidunt eget nullam non nisi."
}
]
},
{
"section": "Precios y planes",
"items": [
{
"question": "¿Cuales son los planes que ofrece ETA VIAPORTE?",
"answer": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur libero id faucibus nisl tincidunt eget nullam non nisi."
},
{
"question": "¿Los costos de los planes es mensual?",
"answer": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur libero id faucibus nisl tincidunt eget nullam non nisi."
},
{
"question": "¿Dónde puedo encontrar más ayuda y soporte si tengo otras preguntas?",
"answer": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur libero id faucibus nisl tincidunt eget nullam non nisi."
}
]
},
{
"section": "Usuarios",
"items": [
{
"question": "¿Cómo puedo dar de alta usuarios como embarcador?",
"answer": "Para dar de alta usuarios como embarcador en nuestra plataforma, sigue estos sencillos pasos:",
"steps": [
"Ingresa a nuestra página a través de la siguiente URL: [URL de la plataforma].",
"Dirígete al menú lateral y selecciona la sección de <b>Usuarios</b>.",
"En la esquina superior derecha, haz clic en el botón <b>Agregar usuarios</b>.",
"Completa todos los campos del formulario, asegurándote de proporcionar información precisa y válida. Recuerda que los campos marcados con un asterisco (*) son obligatorios, pero es recomendable completar todos los datos disponibles.",
"Una vez que hayas completado el formulario, haz clic en el botón <b>Guardar</b>.",
"¡Listo! El usuario que has agregado debería aparecer ahora en la lista de usuarios, ubicado en la primera posición."
],
"notes": ""
},
{
"question": "¿Cómo puedo dar de alta conductores como transportista?",
"answer": "Para dar de alta conductores como transportista en nuestra plataforma, sigue estos sencillos pasos:",
"steps": [
"Ingresa a nuestra página a través de la siguiente URL: [URL de la plataforma].",
"Dirígete al menú lateral y selecciona la sección de <b>Usuarios</b>.",
"En la esquina superior derecha, haz clic en el botón <b>Agregar usuarios<b>.",
"Completa todos los campos del formulario, asegurándote de proporcionar información precisa y válida, en el campo del formulario <b>Rol de usuario</b> selecciona <b>Conductor</b>. Recuerda que los campos marcados con un asterisco (*) son obligatorios, pero es recomendable completar todos los datos disponibles.",
"Una vez que hayas completado el formulario, haz clic en el botón <b>Guardar</b>.",
"¡Listo! El usuario que has agregado debería aparecer ahora en la lista de usuarios, ubicado en la primera posición."
]
},
{
"question": "¿Cómo puedo dar dar de baja un usuario?",
"answer": "Para dar de baja a un usuario en nuestra plataforma, sigue estos sencillos pasos:",
"steps": [
"Ingresa a nuestra página a través de la siguiente URL: [URL de la plataforma].",
"Dirígete al menú lateral y selecciona la sección de <b>Usuarios<b>.",
"Busca al usuario que deseas dar de baja en el listado de usuarios.",
"En la tarjeta de información del usuario, ubicada en el listado, desplázate hacia la parte inferior derecha y haz clic en el botón <b>Eliminar<b>.",
"Se te pedirá confirmar la acción. Si estás de acuerdo con eliminar al usuario, haz clic en <b>Eliminar</b> para confirmar.",
"¡Listo! El usuario será eliminado de nuestra plataforma de manera permanente."
]
}
]
},
{
"section": "Cargas",
"items": [
{
"question": "¿Cómo puedo publicar una carga como embarcador?",
"answer": "Para dar de alta cargas como embarcador en nuestra plataforma, sigue estos sencillos pasos:",
"steps": [
"Ingresa a nuestra página a través de la siguiente URL: [URL de la plataforma].",
"Dirígete al menú lateral y selecciona la sección de <b>Publicaciones</b>.",
"En la esquina superior derecha, haz clic en el botón <b>Crear carga</b>.",
"Completa todos los campos del formulario, asegurándote de proporcionar información precisa y válida. Recuerda que los campos marcados con un asterisco (*) son obligatorios, pero es recomendable completar todos los datos disponibles.",
"Ahora puedes <b>Guardar</b> o <b>Publicar</b>, la acción <b>Guardar</b> agrega la carga pero no se publica, la acción <b>Publicar</b> agrega la carga y la publica al mismo tiempo para que este disponible para los transportista y lista para recibir ofertas.",
"Listo la carga agregada, debería aparecer ahora en la lista de cargas, ubicado en la primera posición."
]
},
{
"question": "¿Cómo puedo encontrar cargas disponibles como transportista?",
"answer": "Para encontrar cargas disponibles como transportista o broker en nuestra plataforma, sigue estos sencillos pasos:",
"steps": [
"Ingresa a nuestra página a través de la siguiente URL: [URL de la plataforma].",
"Dirígete al menú de navegación en la parte superior derecha y selecciona la sección <b>Cargas</b>.",
"En esta sección, encontrarás una lista de todas las cargas disponibles en este momento. Además, puedes utilizar los filtros disponibles para refinar tu búsqueda por tipo de transporte solicitado, segmento, estado y ciudad.",
"Explora las opciones disponibles y encuentra la carga que mejor se adapte a tus necesidades y capacidades de transporte.",
"Una vez que hayas encontrado una carga que te interese, sigue las instrucciones proporcionadas para enviar una cotización o ponerte en contacto con el embarcador."
],
"notes": "¡Listo! Ahora puedes buscar y encontrar fácilmente cargas disponibles en nuestra plataforma para transportar."
},
{
"question": "¿Cómo puedo enviar una oferta como transportista?",
"answer": "Para enviar oferta como transportista sigue estos sencillos pasos:",
"steps": [
"Ingresa a nuestra página a través de la siguiente URL: [URL de la plataforma].",
"En esta sección, encontrarás una lista de todas las cargas disponibles en este momento. Además, puedes utilizar los filtros disponibles para refinar tu búsqueda por tipo de transporte solicitado, segmento, estado y ciudad.",
"Una vez identificada la carga en la que estás interesado, haz clic en la tarjeta de la carga. En la parte inferior derecha de la tarjeta, encontrarás la opción <b>Hacer oferta</b>. Haz clic en este botón para abrir un pequeño formulario.",
"En el formulario de oferta, selecciona el vehículo que deseas ofrecer para transportar la carga y agrega cualquier comentario adicional que desees incluir para el embarcador.",
"Finalmente, haz clic en <b>Enviar</b> para enviar tu oferta. La oferta será enviada al embarcador, quien podrá revisarla y tomar una decisión."
],
"notes": "¡Listo! Has enviado con éxito tu oferta como transportista para la carga deseada. Si tienes alguna pregunta o necesitas ayuda adicional, no dudes en contactarnos."
},
{
"question": "¿Cómo funciona el proceso de aceptación de una oferta como embarcador?",
"answer": "Como embarcador, el proceso de aceptación de una oferta para tu carga publicada es sencillo. Sigue estos pasos:",
"steps": [
"En la sección de <b>Cargas Publicadas</b>, localiza la carga específica en la que deseas revisar las ofertas. En la tarjeta de la carga, en la parte inferior derecha, encontrarás un botón etiquetado como <b>Ofertas</b>.",
"Haz clic en el botón <b>Ofertas</b> para abrir un modal que mostrará un listado de todas las ofertas recibidas para esa carga. Cada oferta incluirá detalles importantes sobre la empresa transportista, como el nombre del conductor, el vehículo, el tipo de transporte, entre otros.",
"Revisa las ofertas recibidas y si alguna te interesa, haz clic en el botón <b>Aceptar</b>. Con este paso, confirmas tu decisión de aceptar la oferta seleccionada y proceder con el transporte de tu carga.",
"¡Listo! Una vez que hayas aceptado una oferta, tu carga ya tiene transporte asignado. Ahora podrás seguir cualquier cambio en el proceso realizado por el transportista, manteniendo una comunicación fluida a lo largo del envío."
]
},
{
"question": "¿Como le doy seguimiento a mi carga como embarcador?",
"steps": [
"Ingresa a la sección de <b>Publicaciones</b> en nuestra plataforma y localiza la carga específica a la que deseas hacer seguimiento.",
"En la tarjeta de carga, podrás observar el proceso en el que se encuentra tu carga, indicado por el <b>Estado de la carga</b>. Nuestra plataforma muestra cinco estados diferentes: </br><b>Publicado:</b> Tu carga ha sido publicada y está recibiendo ofertas de transportistas.<br/><b>Cargando:</b> El transportista está cargando la mercancía en su vehículo.<br/><b>En tránsito:</b> El transportista está transportando tu carga hacia el lugar de destino.<br/><b>Descargando:</b> El transportista está descargando la mercancía en el destino.<br/><b>Completado:</b> El transportista ha completado el proceso y tu carga ha llegado a su destino final.",
"Además, puedes rastrear la ubicación actual de tu carga haciendo clic en el icono de ubicación en la información de la carga, que se muestra como el <b>Código de carga</b>. Al hacer clic en este icono, serás redirigido a una página con un mapa que muestra la ubicación actual del transporte, representado por un ícono de camión."
],
"notes": "Con estos pasos, podrás mantener un seguimiento detallado de tus cargas en nuestra plataforma, asegurando una gestión eficiente y transparente durante todo el proceso de envío."
},
{
"question": "¿Qué debo hacer si tengo un problema con una carga o un transporte?",
"answer": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consectetur libero id faucibus nisl tincidunt eget nullam non nisi."
}
]
},
{
"section": "Vehiculos",
"items": [
{
"question": "¿Como doy de alta vehículos como transportista?",
"answer": "Para dar de alta vehículos como transportista en nuestra plataforma, sigue estos sencillos pasos:",
"steps": [
"Ingresa a nuestra página a través de la siguiente URL: [URL de la plataforma].",
"Dirígete al menú lateral y selecciona la sección de <b>Vehículos</b>.",
"En la esquina superior derecha, haz clic en el botón <b>Agregar Vehículo</b>.",
"Completa todos los campos del formulario, asegurándote de proporcionar información precisa y válida. Recuerda que los campos marcados con un asterisco (*) son obligatorios, pero es recomendable completar todos los datos disponibles.",
"Una vez que hayas completado el formulario, haz clic en el botón <b>Guardar</b>.",
"¡Listo! El Vehículo que has agregado debería aparecer ahora en la lista de Vehículos, ubicado en la primera posición."
]
},
{
"question": "¿Como puedo dar seguimiento de mi vehículo?",
"steps": [
"Ingresa a nuestra página a través de la siguiente URL: [URL de la plataforma].",
"Dirígete al menú lateral y selecciona la sección de <b>Cargas aceptadas</b>.",
"En esta sección, encontrarás una lista de todas las cargas para las cuales has realizado ofertas y han sido aceptadas. En cada tarjeta de carga aceptada, podrás ver detalles importantes como el destino de la carga, el estado actual de la carga, el nombre del conductor encargado del transporte y el código único de la carga, etc. Identifica el vehículo que deseas rastrear utilizando el código del vehículo y haz clic en el enlace <b>Código de carga</b> que está resaltado en azul en la parte superior derecha de la tarjeta.",
"Al hacer clic en el enlace, se abrirá un modal con un mapa que te mostrará la ubicación actual de tu camión en tiempo real, representado por un icono de camión.",
"¡Listo! Ahora podrás rastrear fácilmente la ubicación de tus vehículos y sus cargas activas en todo momento."
],
"notes": "Con estos simples pasos, podrás mantener un seguimiento eficaz de tus vehículos y sus cargas activas, lo que te permitirá gestionar mejor tus operaciones de transporte."
},
{
"question": "¿Como asignar un conductor a un vehículo?",
"answer": "Para asignar un conductor a un vehículo en nuestra plataforma, sigue estos sencillos pasos:",
"steps": [
"Ingresa a nuestra página a través de la siguiente URL: [URL de la plataforma].",
"Dirígete al menú lateral y selecciona la sección de <b>Vehículos</b>.",
"En esta sección, encontrarás un listado de todos tus vehículos. Identifica el vehículo al cual deseas asignar un conductor.",
"En la tarjeta de vehículo, busca el campo llamado <b>Conductor</b> y haz clic en el ícono de lápiz que se encuentra junto a él.",
"Se abrirá un modal con un pequeño formulario donde podrás seleccionar al conductor que deseas asignar a ese vehículo.",
"Una vez seleccionado el conductor, haz clic en <b>Guardar</b>. ¡Listo! Ahora has asignado exitosamente un conductor al vehículo seleccionado."
],
"notes": "Con estos pasos simples y claros, podrás asignar conductores a tus vehículos de manera eficiente y mantener un control adecuado de tus operaciones de transporte en nuestra plataforma."
}
]
}
]

32
src/data/loadsType.json Normal file
View File

@@ -0,0 +1,32 @@
[
{
"name": "Published",
"status": "Publicado",
"color": "#ffd22b"
},
{
"name": "Loading",
"status": "Cargando",
"color": "green"
},
{
"name": "Transit",
"status": "En Transito",
"color": "red"
},
{
"name": "Downloading",
"status": "Descargando",
"color": "#0F5E21"
},
{
"name": "Delivered",
"status": "Entregado",
"color": "blue"
},
{
"name": "Draf",
"status": "Sin publicar",
"color": "#000000"
}
]

50
src/data/segments.json Normal file
View File

@@ -0,0 +1,50 @@
[
{
"id": 1,
"name": "Automotriz",
"img": "AUTOMOTRIZ",
"color": "#E93323"
},
{
"id": 2,
"name": "Agricola",
"img": "AGRICOLA",
"color": "#5E813F"
},
{
"id": 3,
"name": "Cemento",
"img": "CEMENTO",
"color": "#817F83"
},
{
"id": 4,
"name": "Energia",
"img": "ENERGIA",
"color": "#F6C343"
},
{
"id": 5,
"name": "Intermoadal",
"img": "INTERMOADAL",
"color": "#081E5D"
},
{
"id": 6,
"name": "Materiales y Minerales",
"img": "METALESYMINERALES",
"color": "#B86028"
},
{
"id": 7,
"name": "Productos Industriales",
"img": "PRODUCTOSINDUSTRIALES",
"color": "#4F72BF"
},
{
"id": 8,
"name": "Quimicos y Fertilizantes",
"img": "QUIMICOSYFERTILIZANTES",
"color": "#6A339A"
}
]

View File

@@ -1,4 +1,32 @@
const months = [
"Enero",
"Febrero",
"Marzo",
"Abril",
"Mayo",
"Junio",
"Julio",
"Agosto",
"Septiembre",
"Octubre",
"Noviembre",
"Diciembre"
];
const monthsAbr = [
"Ene",
"Feb",
"Mar",
"Abr",
"May",
"Jun",
"Jul",
"Ago",
"Sep",
"Oct",
"Nov",
"Dic"
];
export const getDateMonthDay = (value) => { export const getDateMonthDay = (value) => {
const date = new Date(value) const date = new Date(value)
@@ -10,3 +38,32 @@ export const getDateMonthDay = (value) => {
}) })
} }
export const getDateMonthDayEs = (value, isFull = false) => {
const date = new Date(value)
let month = '';
if(isFull) {
month = months[date.getMonth()];
} else {
month = monthsAbr[date.getMonth()]
}
return `${month} ${date.getDate()}, ${date.getFullYear()}`;
}
export const getDateTime = (value, hour) => {
const date = new Date(value);
date.setHours(date.getHours() + hour);
// Obtener los componentes de la fecha
const year = date.getFullYear();
const month = ('0' + (date.getMonth() + 1)).slice(-2); // Agrega cero al principio si es necesario
const day = ('0' + date.getDate()).slice(-2); // Agrega cero al principio si es necesario
const hours = ('0' + date.getHours()).slice(-2); // Agrega cero al principio si es necesario
const minutes = ('0' + date.getMinutes()).slice(-2); // Agrega cero al principio si es necesario
// Crear la cadena de fecha formateada
const dateFormat = `${year}-${month}-${day} ${hours}:${minutes}`;
return dateFormat;
}

93
src/helpers/status.js Normal file
View File

@@ -0,0 +1,93 @@
export const getStatusLoad = (load) => {
let status;
let color;
switch (load.load_status) {
case 'Published':
status = "Publicado";
color = "#000000";
break;
case 'Loading':
color = "#F44336";
status = "Cargando";
break;
case 'Transit':
status = "En Transito";
color = "#ffd22b"
break;
case 'Downloading':
status = "Descargando";
color = "#428502"
break;
case 'Delivered':
status = "Entregado";
color = "#000000";
break;
default:
status = 'Sin publicar';
color = "#000000";
break;
}
return {
status,
color,
};
}
export const eventStatusLoad = (loadStatus) => {
let color;
let status;
switch (loadStatus) {
case 'Published':
status = "Publicado";
color = "yellow";
break;
case 'Loading':
color = "green";
status = "Cargando";
break;
case 'Transit':
status = "En Transito";
color = "red"
break;
case 'Downloading':
status = "Descargando";
color = "blue"
break;
case 'Delivered':
color = "blue";
status = "Entregado";
break;
default:
color = "yellow";
status = 'Sin publicar';
break;
}
return {
color,
status
};
}
export const getStatusPublished = (load) => {
let status;
switch (load.status) {
case 'Draft':
status = "Guardado";
break;
case 'Published':
status = "Publicado";
break;
case 'Completed':
status = "Conectado";
break;
case 'Closed':
status = "Completado";
break;
default:
status = 'Guardado';
break;
}
return status;
}

View File

@@ -8,7 +8,7 @@
<template> <template>
<div class="wrapper"> <div class="wrapper">
<Sidebar/> <Sidebar/>
<div class="content"> <div class="main-panel" id="main-panel">
<NavBar/> <NavBar/>
<div class="view"> <div class="view">
<RouterView /> <RouterView />
@@ -20,26 +20,39 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.wrapper { .wrapper {
position: relative;
width: 100wv;
top: 0;
height: 100vh;
display: flex; display: flex;
width: 100%;
align-items: stretch;
background-color: #f6f3f3; background-color: #f6f3f3;
} }
.main-panel {
position: relative;
margin-left: 220px;
width: calc(100wv - 220px) !important;
}
.view { .view {
margin: 24px 50px; width: 100%;
// width: calc(100vw - 260px); padding: 24px 50px;
} }
@media (max-width: 1400px) { @media (max-width: 1400px) {
.view { .view {
margin: 24px 24px; padding: 24px 24px;
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.main-panel {
position: relative;
margin-left: 0px;
width: 100wv !important;
}
.view { .view {
margin: 24px 12px; margin: 24px 0px;
} }
} }
</style> </style>

View File

@@ -0,0 +1,28 @@
<script setup>
import Header from '../components/Header.vue';
</script>
<template>
<Header/>
<div class="auth-layout">
<RouterView/>
</div>
</template>
<style scoped>
.auth-layout {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
align-content: center;
/* background-color: red; */
width: 100%;
margin: 0px auto !important;
padding: 0px 5px !important;
overflow: hidden;
min-height: 700px;
}
</style>

View File

@@ -1,9 +1,12 @@
import axios from "axios"; import axios from "axios";
const baseUrl = import.meta.env.VITE_API_URL; const baseUrl = import.meta.env.VITE_API_URL;
const accessToken = localStorage.getItem('access');
const api = axios.create({ const api = axios.create({
baseURL: baseUrl baseURL: baseUrl,
headers: {
'Authorization': 'Bearer ' + accessToken
}
}); });
export default api; export default api;

View File

@@ -1,5 +1,4 @@
import './assets/main.css' import './assets/main.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'

View File

@@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import AuthLayout from '../layouts/AuthLayout.vue' import AuthLayout from '../layouts/AuthLayout.vue'
import PublicLayout from '../layouts/PublicLayout.vue'
// import {useAuthStore} from '../stores/auth'; // import {useAuthStore} from '../stores/auth';
// const authStore = useAuthStore(); // const authStore = useAuthStore();
@@ -31,6 +32,28 @@ const router = createRouter({
path: 'registro-empresa', path: 'registro-empresa',
name: 'register-company', name: 'register-company',
component: () => import('../views/CompleteRegisterView.vue') component: () => import('../views/CompleteRegisterView.vue')
},
]
},
{
path: '/publico',
name: 'public',
component: PublicLayout,
children: [
{
path: 'terminos-y-condiciones',
name: 'terms-conditions',
component: () => import('../views/TermsAndConditionsView.vue')
},
{
path: 'eviso-de-privacidad',
name: 'notice-privacy',
component: () => import('../views/NoticeOfPrivacyView.vue')
},
{
path: 'faqs',
name: 'faqs',
component: () => import('../views/FaqsView.vue')
} }
] ]
}, },
@@ -50,15 +73,20 @@ const router = createRouter({
name: 'company', name: 'company',
component: () => import('../views/MyCompanyView.vue'), component: () => import('../views/MyCompanyView.vue'),
}, },
{
path: 'empresa/:id',
name: 'public-users',
component: () => import('../views/PublicUsersCompanyView.vue'),
},
{ {
path: 'ubicaciones', path: 'ubicaciones',
name: 'locations', name: 'locations',
component: () => import('../views/LocationsView.vue'), component: () => import('../views/LocationsView.vue'),
}, },
{ {
path: 'publicaciones', path: 'camiones',
name: 'published', name: 'published-trucks',
component: () => import('../views/PublishedView.vue'), component: () => import('../views/TrucksPublishedView.vue'),
}, },
{ {
path: 'usuarios', path: 'usuarios',
@@ -82,8 +110,8 @@ const router = createRouter({
}, },
{ {
path: 'cargas', path: 'cargas',
name: 'loads', name: 'published-loads',
component: () => import('../views/LoadsView.vue'), component: () => import('../views/LoadsPublishedView.vue'),
}, },
{ {
path: 'vehiculos', path: 'vehiculos',
@@ -100,6 +128,16 @@ const router = createRouter({
name: 'shippers', name: 'shippers',
component: () => import('../views/ShippersView.vue'), component: () => import('../views/ShippersView.vue'),
}, },
{
path: 'tracking/:code',
name: 'tracking-load',
component: () => import('../views/TrackingLoadView.vue'),
},
{
path: 'buscar-cargas',
name: 'search-loads',
component: () => import('../views/SearchLoadsView.vue'),
},
] ]
} }
] ]
@@ -108,7 +146,6 @@ const router = createRouter({
router.beforeEach( async(to, from, next) => { router.beforeEach( async(to, from, next) => {
const requiresAuth = to.matched.some(url => url.meta.requiresAuth) const requiresAuth = to.matched.some(url => url.meta.requiresAuth)
const session = localStorage.getItem('session'); const session = localStorage.getItem('session');
console.log('Se ejecuta router');
if(requiresAuth) { if(requiresAuth) {
//Comprobamos si el usuario esta authenticado //Comprobamos si el usuario esta authenticado
if(session) { if(session) {

View File

@@ -38,7 +38,7 @@ export const renewToken = async() => {
try { try {
const endpoint = `/v1/account/authorize/${session}`; const endpoint = `/v1/account/authorize/${session}`;
const {data} = await api.get(endpoint); const {data} = await api.get(endpoint);
console.log(data.user); console.log(data);
if(data.accessToken !== null){ if(data.accessToken !== null){
return { return {
msg: 'success', msg: 'success',

View File

@@ -12,7 +12,7 @@ export const getCompany = async(companyId) => {
} }
} }
export const editCompany = async(companyId, formData) => { export const updateCompany = async(companyId, formData) => {
try { try {
const endpoint = `/companies/${companyId}`; const endpoint = `/companies/${companyId}`;
const {data} = await api.patch(endpoint, formData); const {data} = await api.patch(endpoint, formData);
@@ -22,3 +22,150 @@ export const editCompany = async(companyId, formData) => {
return null; return null;
} }
} }
export const getUsers = async(filter) => {
try {
const endpoint = `/users?${filter}`;
const {data} = await api.get(endpoint);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const createUser = async(formData) => {
try {
const endpoint = `/users`;
const {data} = await api.post(endpoint, formData);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const updateUser = async(user_id, formData) => {
try {
const endpoint = `/users/${user_id}`;
const {data} = await api.patch(endpoint, formData);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const deleteUser = async(user_id) => {
try {
const endpoint = `/users/${user_id}`;
const {data} = await api.delete(endpoint);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const getBudgets = async(filter) => {
try {
const endpoint = `/budgets/${filter}`;
const {data} = await api.get(endpoint);
// console.log(data);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const updateBudget = async(id, formData) => {
try {
const endpoint = `/budgets/${id}`;
const {data} = await api.patch(endpoint, formData);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const createBudget = async(formData) => {
try {
const endpoint = `/budgets`;
const {data} = await api.post(endpoint, formData);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const deleteBudget = async(id) => {
try {
const endpoint = `/budgets/${id}`;
const {data} = await api.delete(endpoint);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const getLocations = async(filter) => {
try {
const endpoint = `/branches/${filter}&$limit=3&$sort%5BcreatedAt%5D=-1`;
console.log(endpoint);
const {data} = await api.get(endpoint);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const createLocation = async(formData) => {
try {
const endpoint = `/branches`;
const {data} = await api.post(endpoint, formData);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const updateLocation = async(id, formData) => {
try {
const endpoint = `/branches/${id}`;
const {data} = await api.patch(endpoint, formData);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const deleteLocation = async(id) => {
try {
const endpoint = `/branches/${id}`;
const {data} = await api.delete(endpoint);
return data;
} catch (error) {
console.log(error);
return null;
}
}
// export const editCompany = async(companyId, formData) => {
// try {
// const endpoint = `/companies/${companyId}`;
// const {data} = await api.patch(endpoint, formData);
// return data;
// } catch (error) {
// console.log(error);
// return null;
// }
// }

View File

@@ -54,10 +54,10 @@ export const getCompanies = async(filter) => {
console.log(endpoint); console.log(endpoint);
const {data} = await api.get(endpoint); const {data} = await api.get(endpoint);
console.log(data); console.log(data);
return data.data; return data;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return []; return null;
} }
} }
@@ -66,6 +66,19 @@ export const getUsersCompany = async(filter) => {
const endpoint = `/v1/users?${filter}`; const endpoint = `/v1/users?${filter}`;
// console.log({endpoint}); // console.log({endpoint});
const {data} = await api.get(endpoint); const {data} = await api.get(endpoint);
return data;
} catch (error) {
console.log(error);
return [];
}
}
export const getPublicUsersCompany = async(filter) => {
try {
const endpoint = `/v1/public-companies/users/${filter}`;
// console.log({endpoint});
const {data} = await api.get(endpoint);
console.log(data.data)
// console.log(data); // console.log(data);
return data.data; return data.data;
} catch (error) { } catch (error) {
@@ -76,7 +89,7 @@ export const getUsersCompany = async(filter) => {
export const getSettingsQuery = async(filter) => { export const getSettingsQuery = async(filter) => {
try { try {
const endpoint = "/meta-data/find?regex=" + filter.query; const endpoint = "/v1/meta-data/find?regex=" + filter.query;
const {data} = await api.get(endpoint); const {data} = await api.get(endpoint);
return data.data; return data.data;
} catch (error) { } catch (error) {
@@ -96,6 +109,17 @@ export const searchcategories = async(query) => {
} }
} }
export const searchProducts = async(query) => {
try {
const endpoint = "/products/?name[$regex]=" + query + "&name[$options]=i";
const {data} = await api.get(endpoint);
return data.data;
} catch (error) {
console.log(error);
return [];
}
}
export const searchstates = async(query) => { export const searchstates = async(query) => {
try { try {
const endpoint = "/v1/states/find?regex=" + query; const endpoint = "/v1/states/find?regex=" + query;
@@ -110,7 +134,7 @@ export const searchstates = async(query) => {
export const searchcities = async(query) => { export const searchcities = async(query) => {
try { try {
// const endpoint = "/cities/?city_name[$regex]=" + query + "&city_name[$options]=i"; // const endpoint = "/cities/?city_name[$regex]=" + query + "&city_name[$options]=i";
const endpoint = "/cities/find?regex=" + query; const endpoint = "/v1/cities/find?regex=" + query;
const {data} = await api.get(endpoint); const {data} = await api.get(endpoint);
return data.data; return data.data;
} catch (error) { } catch (error) {

68
src/services/vehicles.js Normal file
View File

@@ -0,0 +1,68 @@
import api from "../lib/axios";
export const getVehicles = async(filter) => {
try {
const endpoint = `/vehicles/${filter}`;
const {data} = await api.get(endpoint);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const updateVehicle = async(id, formData) => {
try {
const endpoint = `/vehicles/${id}`;
const {data} = await api.patch(endpoint, formData);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const deleteVehicle = async(id) => {
try {
const endpoint = `/vehicles/${id}`;
const {data} = await api.delete(endpoint);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const createVehicle = async(formData) => {
try {
const endpoint = `/vehicles/`;
const {data} = await api.post(endpoint, formData);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const saveProposal = async(formData) => {
try {
const endpoint = `/proposals`;
const {data} = await api.post(endpoint, formData);
return data;
} catch (error) {
console.log(error);
return null;
}
}
export const updateProposal = async(id, formData) => {
try {
const endpoint = `/proposals/${id}`;
const {data} = await api.patch(endpoint, formData);
return data;
} catch (error) {
console.log(error);
return null;
}
}

View File

@@ -4,12 +4,14 @@ import { useRouter } from 'vue-router';
import { renewToken } from '../services/auth'; import { renewToken } from '../services/auth';
import {useNotificationsStore} from './notifications'; import {useNotificationsStore} from './notifications';
import {useCompanyStore} from './company'; import {useCompanyStore} from './company';
import { useLoadsStore } from "./loads";
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const router = useRouter(); const router = useRouter();
const noty = useNotificationsStore(); const noty = useNotificationsStore();
const company = useCompanyStore(); const company = useCompanyStore();
const loadStore = useLoadsStore();
const sesion = ref('') const sesion = ref('')
const checking = ref(false); const checking = ref(false);
const authStatus = ref('checking'); const authStatus = ref('checking');
@@ -17,7 +19,6 @@ export const useAuthStore = defineStore('auth', () => {
const user = ref(null) const user = ref(null)
onMounted( async() => { onMounted( async() => {
console.log('Se ejecuta onMounted auth');
checkSession(); checkSession();
}); });
@@ -32,6 +33,8 @@ export const useAuthStore = defineStore('auth', () => {
sesion.value = resp.data.session_token; sesion.value = resp.data.session_token;
token.value = resp.data.accessToken; token.value = resp.data.accessToken;
localStorage.setItem('session', resp.data.session_token); localStorage.setItem('session', resp.data.session_token);
localStorage.setItem('access', resp.data.accessToken);
localStorage.setItem('id', resp.data.user.company);
checking.value = false; checking.value = false;
} else { } else {
noty.show = true; noty.show = true;
@@ -51,13 +54,17 @@ export const useAuthStore = defineStore('auth', () => {
}); });
const logout = () => { const logout = () => {
console.log('logoo....');
localStorage.removeItem('session');
company.clear();
router.push({name: 'login'});
sesion.value = ''; sesion.value = '';
token.value = ''; token.value = '';
company.clear();
localStorage.clear();
user.value = null; user.value = null;
console.log(company.company);
localStorage.removeItem('access');
localStorage.removeItem('id');
localStorage.removeItem('session');
router.push({name: 'login'});
} }
return { return {

View File

@@ -1,38 +1,411 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref, watch, onMounted } from "vue"; import { ref } from "vue";
import { useAuthStore } from "./auth"; import { getBudgets, getCompany, updateBudget, updateCompany, deleteBudget, createBudget, getUsers, updateUser, createUser, deleteUser, getLocations, createLocation, updateLocation, deleteLocation } from "../services/company";
import { getCompany } from "../services/company"; import api from "../lib/axios";
import { saveProposal, updateProposal } from "../services/vehicles";
export const useCompanyStore = defineStore('company', () => { export const useCompanyStore = defineStore('company', () => {
const auth = useAuthStore();
const company = ref(null) const company = ref(null)
const users = ref([]);
const drivers = ref([]);
const usersTotal = ref(0);
const usersCurrentPage = ref(1);
const budgets = ref([]);
const budgetsTotal = ref(0);
const budgetsCurrentPage = ref(1);
const locations = ref([]);
const locationsLoads = ref([]);
const locationsTotal = ref(0);
const locationsCurrentPage = ref(1);
const proposals = ref([]);
const proposalsTotal = ref(0);
const proposalsCurrentPage = ref(1)
const loading = ref(false); const loading = ref(false);
const getCompanyData = async() => { const getCompanyData = async() => {
const companyId = localStorage.getItem('id');
loading.value = true; loading.value = true;
if(!company.value) { if(!company.value) {
console.log('Se ejecuta');
loading.value = true; loading.value = true;
const companyId = auth.user?.company;
console.log({companyId});
const resp = await getCompany(companyId); const resp = await getCompany(companyId);
console.log(resp);
company.value = resp; company.value = resp;
} }
loading.value = false; loading.value = false;
} }
const clear = () => { const getUsersCompany = async(limit = 10, skip = 0, reload = false) => {
const companyId = localStorage.getItem('id');
if(users.value.length <= 0 || reload === true) {
const filter = `company=${companyId}&$sort%5BcreatedAt%5D=-1&$limit=${limit}&$skip=${skip}`
const resp = await getUsers(filter);
if(resp !== null && resp.total > 0) {
usersTotal.value = resp.total;
users.value = resp.data;
} else {
users.value = [];
}
}
}
const getDrivers = async() => {
const companyId = localStorage.getItem('id');
if(drivers.value.length <= 0) {
const filter = `company=${companyId}&$sort%5BcreatedAt%5D=-1&$limit=100&$skip=0&job_role=driver`
const resp = await getUsers(filter);
if(resp !== null && resp.total > 0) {
drivers.value = resp.data;
} else {
drivers.value = [];
}
}
}
const createUserCompany = async(formData, localData) => {
const data = await createUser(formData);
if(data) {
users.value.unshift({
...data,
...localData
});
usersTotal.value++;
if(data.job_role === 'driver' && drivers.value.length > 0) {
drivers.value.unshift({
...data,
...localData
})
}
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
const updateUserCompany = async(id, formData, localData) => {
const data = await updateUser(id, formData);
if(data) {
const index = users.value.findIndex((user) => user._id === id);
if(index !== -1) {
users.value[index] = {
...users.value[index],
...data,
...localData
};
if(data.job_role === 'driver' && drivers.value.length > 0) { // Actualizamos en la lista drivers
const indexd = drivers.value.findIndex((user) => user._id === id);
if(indexd !== -1) {
drivers.value[indexd] = {
...drivers.value[index],
...data,
...localData
};
}
}
}
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
const deleteUserCompany = async(id) => {
const data = await deleteUser(id);
if(data) {
users.value = users.value.filter(user => user._id !== id);
if(data.job_role === 'driver' && drivers.value.length > 0) {
drivers.value = drivers.value.filter(user => user._id !== id);
}
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
const editCompany = async(formData) => {
const data = await updateCompany(company.value._id, formData);
if(data === null) {
return 'Algo salio mal, intente más tarde';
} else {
company.value = {
...company.value,
...formData,
};
return 'success';
}
}
const clear = () => { //Cuando se cierra la sesion
company.value = null; company.value = null;
users.value = [];
usersTotal.value = 0;
usersCurrentPage.value = 1;
drivers.value = [];
budgets.value = [];
proposals.value = [];
locations.value = [];
locationsLoads.value = [];
locationsTotal.value = 0;
locationsCurrentPage.value = 1;
// companyid = null;
loading.value = false;
}
const $reset = () => {
company.value = null;
// companyid = null;
loading.value = false; loading.value = false;
} }
///loads?company=64fa70c130d2650011ac4f3a&status[$ne]=Closed,posted_by_name[$regex]=ju&posted_by_name[$options]=i
const getProposalsCompany = async(filter, reload = false) => {
const companyId = localStorage.getItem('id');
try {
if(proposals.value.length <= 0 || reload) {
const endpoint = `/proposals?carrier=${companyId}&$sort%5BcreatedAt%5D=-1&${filter}`;
console.log(endpoint)
const {data} = await api.get(endpoint);
proposals.value = data.data;
proposalsTotal.value = data.total;
}
} catch (error) {
proposals.value = [];
proposalsTotal.value = 0;
}
}
const createPropsal = async(formData) => {
const data = await saveProposal(formData);
if(data) {
proposalsTotal.value++;
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
const updatePropsalLoad = async(id, formData, localData) => {
const data = await updateProposal(id, formData);
if(data) {
const index = proposals.value.findIndex((prop) => prop._id === id);
proposals.value[index] = {
...proposals.value[index],
...data,
...localData
};
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
const getBudgetsCompany = async(filterQuery, reload = false) => {
let filterArr = Object.values(filterQuery);
let cleanfilterArr = filterArr.filter(n=>n);
var filterStr = "";
if(cleanfilterArr.length >0){
filterStr ="?"+cleanfilterArr.join("&");
}
if(budgets.value.length <= 0 || reload === true) {
try {
const data = await getBudgets(filterStr + '&$sort%5BcreatedAt%5D=-1');
console.log(data.total);
if(data.total > 0) {
budgets.value = data.data;
budgetsTotal.value = data.total;
} else {
budgetsTotal.value = 0;
budgets.value = [];
}
} catch (error) {
budgets.value = [];
}
}
}
const updateBudgetCompany = async(id, formData, localData) => {
try {
const data = await updateBudget(id, formData);
if(data) {
const index = budgets.value.findIndex((budget) => budget._id === id);
budgets.value[index] = {
...budgets.value[index],
...data,
...localData
};
return 'success';
} else {
return 'No se pudo actualizar presupuesto, intente mas tarde';
}
} catch (error) {
return 'Algo salio mal, intente más tarde';
}
}
const createBudgetCompany = async(formData, localData) => {
try {
const data = await createBudget(formData);
if(data) {
budgetsTotal.value++;
budgets.value.push({
...data,
...localData
});
return 'success';
} else {
return 'No se pudo agregar presupuesto, intente mas tarde';
}
} catch (error) {
return 'Algo salio mal, intente más tarde';
}
}
const deleteBudgetCompany = async(id) => {
try {
const data = await deleteBudget(id);
if(data) {
budgetsTotal.value--;
budgets.value = budgets.value.filter(budget => budget._id !== id);
return data;
} else {
return null;
}
} catch (error) {
return null;
}
}
const getLocationsCompany = async(filterQuery, reload = false) => {
let filterArr = Object.values(filterQuery);
let cleanfilterArr = filterArr.filter(n=>n);
var filterStr = "";
if(cleanfilterArr.length > 0){
filterStr ="?"+cleanfilterArr.join("&");
}
if(locations.value.length <= 0 || reload === true) {
const resp = await getLocations(filterStr);
if(resp !== null && resp.total > 0) {
locations.value = resp.data;
locationsTotal.value = resp.total;
} else {
locations.value = [];
}
}
}
const getLocationsLoads = async() => {
if(locationsLoads.value.length <= 0) {
const filterStr = "?company="+ localStorage.getItem('id') + '&$limit=100'
const resp = await getLocations(filterStr);
if(resp !== null && resp.total > 0) {
locationsLoads.value = resp.data;
} else {
locationsLoads.value = [];
}
}
}
const createLocationCompany = async(formData, localData) => {
const data = await createLocation(formData);
if(data) {
locations.value.unshift({
...data,
...localData
});
locationsTotal.value++;
locationsLoads.value.unshift({
...data,
...localData
})
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
const updateLocationCompany = async(id, formData, localData) => {
const data = await updateLocation(id, formData);
if(data) {
const index = locations.value.findIndex((loc) => loc._id === id);
if(index !== -1) {
locations.value[index] = {
...locations.value[index],
...data,
...localData
};
if(locationsLoads.value.length > 0) {
const indexl = locationsLoads.value.findIndex((loc) => loc._id === id);
locationsLoads.value[indexl] = {
...locationsLoads.value[index],
...data,
...localData
};
}
}
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
const deleteLocationCompany = async(id) => {
const data = await deleteLocation(id);
if(data) {
locations.value = locations.value.filter(loc => loc._id !== id);
if(locationsLoads.value.length > 0) {
locationsLoads.value = locationsLoads.value.filter(loc => loc._id !== id);
}
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
return { return {
company, getCompanyData,
loading, getProposalsCompany,
getCompanyData, createPropsal,
clear updatePropsalLoad,
getBudgetsCompany,
getUsersCompany,
getDrivers,
createUserCompany,
updateUserCompany,
deleteUserCompany,
editCompany,
updateBudgetCompany,
createBudgetCompany,
deleteBudgetCompany,
getLocationsCompany,
getLocationsLoads,
createLocationCompany,
updateLocationCompany,
deleteLocationCompany,
budgets,
budgetsCurrentPage,
budgetsTotal,
users,
drivers,
usersTotal,
usersCurrentPage,
locations,
locationsLoads,
locationsTotal,
locationsCurrentPage,
clear,
$reset,
loading,
proposals,
proposalsCurrentPage,
proposalsTotal,
company,
} }
}); });

152
src/stores/loads.js Normal file
View File

@@ -0,0 +1,152 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import api from "../lib/axios";
export const useLoadsStore = defineStore('load', () => {
const currentLoad = ref(null);
const loads = ref([])
const loadsTotal = ref(0)
const loadsCurrentPage = ref(1)
const proposalsOfLoads = ref([]);
const openModalEdit = ref(false);
const openAttachmentsModal = ref(false);
const openProposalsModal = ref(false);
const getCompanyLoads = async(filterQuery, reload = false) => {
const companyid = localStorage.getItem('id');
let filterArr = Object.values(filterQuery);
let cleanfilterArr = filterArr.filter(n=>n);
var filterStr = "";
if(cleanfilterArr.length >0){
filterStr = cleanfilterArr.join("&");
}
if(loads.value.length <= 0 || reload) {
try {
const endpoint = `/loads?company=${companyid}&${filterStr}&$sort%5BcreatedAt%5D=-1`;
const {data} = await api.get(endpoint);
loads.value = data.data;
loadsTotal.value = data.total;
} catch (error) {
loads.value = [];
loadsTotal.value = 0;
console.log(error);
}
}
}
const getProposalsOfLoads = async(filterQuery) => {
try {
const endpoint = `/proposals/?load=${filterQuery}`;
const {data} = await api.get(endpoint);
// console.log(data);
proposalsOfLoads.value = data.data;
} catch (error) {
proposalsOfLoads.value = [];
console.log(error);
}
}
const saveLoad = async(load) => {
try {
const endpoint = `/loads/`;
const {data} = await api.post(endpoint, load);
loadsTotal.value++;
return data;
} catch (error) {
console.log(error);
return null;
}
}
const updateProposal = async(id, proposal) => {
try {
const endpoint = `/proposals/${id}`;
const {data} = await api.patch(endpoint, proposal);
// console.log(data);
return data;
} catch (error) {
console.log(error);
return null;
}
}
const deleteProposal = async(id) => {
try {
const endpoint = `/proposals/${id}`;
const {data} = await api.delete(endpoint);
// console.log(data);
return data;
} catch (error) {
console.log(error);
return null;
}
}
const updateLoad = async(loadId, load) => {
try {
const endpoint = `/loads/${loadId}`;
const {data} = await api.patch(endpoint, load);
// console.log(data);
return data;
} catch (error) {
console.log(error);
return null;
}
}
const deleteLoad = async(loadId) => {
try {
const endpoint = `/loads/${loadId}`;
console.log(endpoint);
const {data} = await api.delete(endpoint);
loadsTotal.value--;
return data;
} catch (error) {
console.log(error);
return null;
}
}
const getLoad = async(filterQuery) => {
try {
const endpoint = `/loads/${filterQuery}`;
const {data} = await api.get(endpoint);
return data;
} catch (error) {
console.log(error);
return [];
}
}
const clear = () => {
currentLoad.value = null;
loads.value = [];
proposalsOfLoads.value = [];
openModalEdit.value = false;
openAttachmentsModal.value = false;
openProposalsModal.value = false;
}
return {
clear,
openModalEdit,
openProposalsModal,
openAttachmentsModal,
getProposalsOfLoads,
getCompanyLoads,
deleteLoad,
getLoad,
saveLoad,
updateLoad,
updateProposal,
deleteProposal,
loads,
loadsCurrentPage,
loadsTotal,
currentLoad,
proposalsOfLoads,
}
});

82
src/stores/vehicles.js Normal file
View File

@@ -0,0 +1,82 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { createVehicle, deleteVehicle, getVehicles, updateVehicle } from "../services/vehicles";
export const useVehiclesStore = defineStore('vehicles', () => {
const vehicles = ref([]);
const vehiclesTotal = ref(0);
const vehiclesCurrentPage = ref(1)
const fetchVehicles = async(filterQuery, reload = false) => {
let filterArr = Object.values(filterQuery);
let cleanfilterArr = filterArr.filter(n=>n);
var filterStr = "";
if(cleanfilterArr.length > 0){
filterStr ="?"+cleanfilterArr.join("&");
}
if(vehicles.value.length <= 0 || reload === true) {
const resp = await getVehicles(filterStr + '&$sort%5BcreatedAt%5D=-1');
console.log(resp.data);
if(resp !== null) {
vehiclesTotal.value = resp.total;
vehicles.value = resp.data;
}
}
}
const createVehicleCompany = async(formData, localData = {}) => {
const data = await createVehicle(formData);
if(data) {
vehicles.value.push({
...data,
...localData
});
vehiclesTotal.value++;
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
const updateVehicleCompany = async(id, formData, localData = {}) => {
const data = await updateVehicle(id, formData);
if(data) {
console.log({data});
const index = vehicles.value.findIndex((vehicle) => vehicle._id === id);
vehicles.value[index] = {
...vehicles.value[index],
...data,
...localData
};
console.log(vehicles.value[index]);
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
const deleteVehicleCompany = async(id) => {
const data = await deleteVehicle(id);
if(data) {
vehicles.value = vehicles.value.filter(vehicle => vehicle._id !== id);
vehiclesTotal.value--;
return 'success';
} else {
return 'Algo salio mal, intente más tarde';
}
}
return {
vehicles,
vehiclesTotal,
vehiclesCurrentPage,
fetchVehicles,
createVehicleCompany,
updateVehicleCompany,
deleteVehicleCompany
}
});

View File

@@ -1,13 +1,182 @@
<script setup> <script setup>
import { onMounted, ref, watch } from 'vue';
import { useCompanyStore } from '../stores/company';
import { useAuthStore } from '../stores/auth';
import Spiner from '../components/ui/Spiner.vue';
import CardBudget from '../components/CardBudget.vue';
import CardEmpty from '../components/CardEmpty.vue';
import CreateBudgetModal from '../components/CreateBudgetModal.vue';
import Pagination from '../components/pagination.vue';
const companyStore = useCompanyStore();
const authStore = useAuthStore();
const loading = ref(false);
const filterQuery = ref([]);
const query = ref('');
const currentBudget = ref(null);
const openModal = ref(false);
const printOpen = ref(false);
const limit = 2;
onMounted(() => {
getInitData();
})
const getInitData = async() => {
loading.value = true;
filterQuery.value.limit = '$limit=' + limit;
filterQuery.value.skip = "$skip=0"
filterQuery.value.company = "company="+ localStorage.getItem('id');
await companyStore.getBudgetsCompany(filterQuery.value, false)
loading.value = false;
}
const getBudgetsWithFilters = async(filter) => {
loading.value = true;
await companyStore.getBudgetsCompany(filter, true);
loading.value = false;
}
watch(query, () => {
filterQuery.value.skip = "$skip="+ 0;
filterQuery.value.limit = "$limit="+ 100;
if(query.value.length === 0){
clearRequest();
filterQuery.value.search = "";
getBudgetsWithFilters(filterQuery.value);
}
});
const search = () => {
if(query.value.length >= 2){
// filterQuery.value = "company_name[$regex]=" + query.value + "&company_name[$options]=i";
filterQuery.value.search = "client[$regex]="+query.value+"&client[$options]=i";
getBudgetsWithFilters(filterQuery.value);
}
}
const getBudgetsByPage = async(data) => {
loading.value = true;
filterQuery.value.skip = "$skip="+ data.skip;
companyStore.budgetsCurrentPage = data.page
await getBudgetsWithFilters(filterQuery.value)
}
const clearFilter = () => {
clearRequest();
filterQuery.value.search = "";
filterQuery.value.company = "company="+ localStorage.getItem('id');
if(query.value == ''){
getInitData();
} else {
query.value = '';
}
}
const clearRequest = () => {
filterQuery.value.skip = "$skip="+ 0;
filterQuery.value.limit = "$limit="+ limit;
companyStore.budgetsCurrentPage = 1;
}
const handleSetCurrentBudget = (data) => {
openModal.value = true;
currentBudget.value = data.budget;
if(data.print) {
printOpen.value = true
}
}
const handleResetCurrentBudget = () => {
openModal.value = false;
currentBudget.value = null;
printOpen.value = false;
}
</script> </script>
<template> <template>
<CreateBudgetModal
v-if="openModal === true"
:budget="currentBudget"
:is-print="printOpen"
@reset-budget="handleResetCurrentBudget"
/>
<div> <div>
<h2 class="title">Calculadora</h2> <h2 class="title my-2">Calculadora</h2>
</div>
<div class="box-filters">
<div class="box-search">
<input class="form-control custom-search" type="search" name="" placeholder="Buscar por cliente" id="" @:input="search()" v-model="query" aria-label="Search">
</div>
<button
class="btn btn-danger bg-dark" type="button" @click="clearFilter">
<i class="fa-solid fa-arrow-rotate-right"></i>
<span class="clear-sm"> Reset</span><span class="clear-md"> filtros</span>
</button>
<button
class="btn-primary-sm radius-sm"
data-toggle="modal" data-target="#budgetModal"
@click="handleSetCurrentBudget({budget: null, print: false})"
><i class="fa-solid fa-plus"></i> <span class="clear-sm"> Crear</span><span class="clear-md"> presupuesto</span></button>
</div>
<Spiner v-if="loading"/>
<div v-else>
<CardBudget
v-if="companyStore.budgets.length > 0"
v-for="budget in companyStore.budgets"
:budget="budget"
@set-budget="handleSetCurrentBudget"
/>
<CardEmpty
v-else
text="No hay presupuestos agregados"
/>
<Pagination
:limit="limit"
:current-page="companyStore.budgetsCurrentPage"
:total="companyStore.budgetsTotal"
@get-elements="getBudgetsByPage"
/>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.box-filters {
display: flex;
flex-direction: row;
justify-content: end;
gap: 1rem;
margin: 1.5rem 0px;
}
.box-search {
width: 60%;
}
.custom-search {
width: 100%;
padding: 12px 20px;
}
@media (max-width: 1024px) {
.box-search {
width: 60%;
}
.box-filters {
gap: .4rem;
}
}
@media (max-width: 768px) {
.box-search {
width: 100%;
}
.box-filters {
gap: .3rem;
}
}
</style> </style>

View File

@@ -1,13 +1,194 @@
<script setup> <script setup>
import { Qalendar } from 'qalendar';
import data from '../data/events.json';
import {eventStatusLoad} from '../helpers/status';
import {getDateTime} from '../helpers/date_formats';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
const events = ref([]);
const router = useRouter();
const config = {
week: {
startsOn: 'monday',
// Takes the values 5 or 7.
nDays: 7,
// Scroll to a certain hour on mounting a week. Takes any value from 0 to 23.
// This option is not compatible with the 'dayBoundaries'-option, and will simply be ignored if custom day boundaries are set.
scrollToHour: 0,
},
month: {
showTrailingAndLeadingDates: false
},
style: {
fontFamily: 'Nunito',
colorSchemes: {
meetings: {
color: "#fff",
backgroundColor: "#131313",
},
sports: {
color: "#fff",
backgroundColor: "#ff4081",
},
},
},
eventDialog:{
isCustom: true
},
defaultMode: 'month',
isSilent: true,
// showCurrentTime: true, // Display a line indicating the current time
}
onMounted(() => {
data.forEach((e, i) => {
const indicator = eventStatusLoad(e.status);
const dateStart = getDateTime(e.date, 0);
const dateEnd = getDateTime(e.date, 1);
events.value.push({
id: i,
title: e.shipment_code,
// isEditable: true,
// isCustom: true,
with: indicator.status,
description: indicator.status,
color: indicator.color,
time: {
start: dateStart,
end: dateEnd
}
})
});
});
const redirectToTracking = (code) => {
router.push({
name: 'tracking-load',
params: {code}
});
}
</script> </script>
<template> <template>
<div> <div>
<h2 class="title">Calendario</h2> <h2 class="title mb-4">Calendario</h2>
<div class="box-indicators">
<h2>Indicadores de estado de la carga</h2>
<div class="indicators">
<i class="fa-solid fa-circle" style="color: yellow"></i> Publicado
<i class="fa-solid fa-circle" style="color: green"></i> Cargando
<i class="fa-solid fa-circle" style="color: red"></i> En transito
<i class="fa-solid fa-circle" style="color: blue"></i> Descargando
<i class="fa-solid fa-circle" style="color: blue"></i> Entregado
</div>
</div>
<div class="calendar">
<!-- <div class="calendar is-light-mode"> -->
<Qalendar
:events="events"
:config="config"
>
<template #eventDialog="props">
<div v-if="props.eventDialogData && props.eventDialogData.title" class="event-modal">
<h2>Información del status de la carga</h2>
<!-- <p>Código de carga: <span :style="{color: props.eventDialogData.color}">{{props.eventDialogData.title}}</span></p> -->
<p>
Código de carga:
<span> {{props.eventDialogData.title}}</span>
<span
class="tracking-icon"
:style="{color: props.eventDialogData.color}"
@click="redirectToTracking(props.eventDialogData.title)">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-geo-alt-fill" viewBox="0 0 16 16">
<path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"></path>
</svg>
</span>
</p>
<p>Estatus de la carga: <i :style="{color: props.eventDialogData.color}" class="fa-solid fa-circle"></i> <span>{{props.eventDialogData.with}}</span></p>
<button class="btn btn-dark" @click="props.closeEventDialog">
Cerrar
</button>
</div>
</template>
</Qalendar>
</div>
</div> </div>
</template> </template>
<style scoped> <style src="qalendar/dist/style.css"></style>
<style lang="scss" scoped>
.calendar {
height: calc(100vh - 220px);
}
.tracking-icon {
cursor: pointer;
color: #f2a23f;
}
.tracking-icon svg{
height: 30px;
}
.tracking-icon:hover {
color: #ddb380;;
height: 150px;
}
.tracking-icon svg:hover{
height: 33px;
}
.event-modal {
padding: 12px 20px;
display: flex;
flex-direction: column;
}
.event-modal h2 {
color: rgb(208, 182, 182);
font-family: sans-serif;
font-size: 1.4rem;
font-weight: 900;
margin-bottom: 2rem;
}
.event-modal p {
color: rgb(208, 182, 182);
font-family: sans-serif;
font-size: 1rem;
font-weight: 700;
}
.box-indicators {
display: flex;
justify-content: end;
flex-direction: column;
padding: 12px 10px;
}
.box-indicators h2 {
display: flex;
justify-content: end;
font-weight: 600;
font-size: 1.2rem;
}
.indicators {
display: flex;
justify-content: end;
flex-direction: row;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.calendar {
height: calc(100vh - 280px);
}
}
</style> </style>

View File

@@ -1,14 +1,14 @@
<script setup> <script setup>
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import Spiner from '../components/ui/Spiner.vue'; import Spiner from '../components/ui/Spiner.vue';
import useDirecty from '../composables/useDirectory'; import useDirectory from '../composables/useDirectory';
import CardCompany from '../components/cardcompany.vue'; import CardCompany from '../components/cardcompany.vue';
import TruckTypes from '../components/ui/TruckTypes.vue'; import TruckTypes from '../components/ui/TruckTypes.vue';
import Segments from '../components/ui/Segments.vue'; import Segments from '../components/ui/Segments.vue';
import States from '../components/ui/States.vue'; import States from '../components/ui/States.vue';
import Cities from '../components/ui/Cities.vue'; import Cities from '../components/ui/Cities.vue';
const {loading, companies, getCompaniesData} = useDirecty(); const {loading, companies, getCompaniesData} = useDirectory();
const query = ref(''); const query = ref('');
const selectedTruckType = ref([]); const selectedTruckType = ref([]);
const selectedCategory = ref([]); const selectedCategory = ref([]);

25
src/views/FaqsView.vue Normal file
View File

@@ -0,0 +1,25 @@
<script setup>
import CardFaq from '../components/CardFaq.vue';
import faqs from '../data/faqs.json';
</script>
<template>
<div class="container">
<h1 class="title mt-5">Preguntas frecuentes</h1>
<div v-for="section in faqs">
<h2 class="title-section mt-4">{{ section.section }}</h2>
<CardFaq
v-for="faq in section.items"
:faq="faq"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.title-section {
font-size: 1.4rem;
font-weight: 600;
}
</style>

View File

@@ -1,6 +1,142 @@
<script setup> <script setup>
import segments from '../data/segments.json';
import loadsType from '../data/loadsType.json';
import BarChartStatistics from '../components/BarChartStatistics.vue';
import DoughnutChartStatistics from '../components/DoughnutChartStatistics.vue';
</script> </script>
<template> <template>
<h1>HomeView</h1> <h1 class="title my-4">Dashboard Administrativo</h1>
<div class="container-dashboard">
<div class="card-fixed card-dashboard">
<h3>Total de cargas este mes</h3>
<div class="main-info">
35
<div class="indicator-text" style="color: green;">
<i class="fa-solid fa-arrow-up"></i>
<!-- <i class="fa-solid fa-arrow-down"></i> -->
23%
</div>
</div>
</div>
<!-- <ChartLoad/> -->
<div class="card-fixed card-dashboard">
<h3>Cargas activas</h3>
<div class="card-chart">
<DoughnutChartStatistics
:data="['Published', 'Transit', 'Delivered', 'Published', 'Downloading', 'Loading']"
:data-model="loadsType"
target-find="name"
target-label="status"
/>
</div>
</div>
<div class="card-fixed card-dashboard">
<h3>Segmentos más usados</h3>
<div class="card-chart">
<DoughnutChartStatistics
:data="['Agricola', 'Agricola', 'Cemento', 'Agricola', 'Intermoadal', 'Agricola']"
:data-model="segments"
target-find="name"
/>
</div>
</div>
<!-- </div>
<div class="container-dashboard"> -->
<div class="card-fixed card-dashboard">
<h3>Estados más usados</h3>
<div class="card-chart">
<BarChartStatistics
label="Ciuades"
:data="['Yucatán', 'Guadalajara', 'Colima', 'Yucatán', 'Nuevo león', 'Yucatán', 'Guadalajara']"
/>
</div>
</div>
<div class="card-fixed card-dashboard">
<h3>Ciudades más usadas</h3>
<div class="card-chart">
<BarChartStatistics
label="Estados"
:data="['Mérida', 'Guadalajara', 'Colima', 'Guadalajara', 'Monterrey', 'Izamal', 'Mérida']"
/>
</div>
</div>
<div class="card-fixed card-dashboard">
<h3>Tipo de transporte más usados</h3>
<div class="card-chart">
<BarChartStatistics
label="Vehiculos"
:data="['FULL / DOBLE REMOLQUE', 'FULL', 'FULL / DOBLE REMOLQUE', 'FULL', 'FULL / DOBLE REMOLQUE', 'FULL', 'FULL / DOBLE REMOLQUE', 'Auto']"
/>
</div>
</div>
</div>
</template> </template>
<style scoped>
.container-dashboard {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
/* gap: 1rem; */
}
.card-dashboard {
width: 32%;
/* margin: 8px 8px; */
min-height: 300px;
display: flex;
flex-direction: column;
}
.card-chart {
width: 100%;
/* max-width: 333px; */
display: flex;
flex-direction: column;
}
.main-info{
align-items: center;
align-content: center;
display: flex;
flex-direction: column;
justify-content: center;
margin: auto 0;
padding: 0px;
font-size: 7rem;
font-weight: 900;
}
.indicator-text {
font-size: 3rem;
}
.container-dashboard h3 {
font-size: 1.2rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: #323232;
}
@media (max-width: 1300px) {
.card-dashboard {
width: 48%;
}
}
@media (max-width: 768px) {
.container-dashboard {
width: 100%;
flex-direction: column;
justify-content: center;
flex-wrap: nowrap;
}
.card-dashboard {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,184 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import Spiner from '../components/ui/Spiner.vue';
import CardLoad from '../components/CardLoad.vue';
import AttachmentsModal from '../components/AttachmentsModal.vue';
import { useLoadsStore } from '../stores/loads';
import FormLoadModal from '../components/FormLoadModal.vue';
import ProposalsModal from '../components/ProposalsModal.vue';
import CardEmpty from '../components/CardEmpty.vue';
import Pagination from '../components/Pagination.vue';
const loadStore = useLoadsStore();
const loading = ref(false);
const query = ref('');
const filterQuery = ref([]);
const limit = 3;
onMounted(() =>{
console.log('init')
getDataLoadsInit(false);
})
///loads?company=64fa70c130d2650011ac4f3a&$limit=3&$skip=0&status[$ne]=Closed&$sort%5BcreatedAt%5D=-1
watch(query, () => {
filterQuery.value.skip = "$skip="+ 0;
filterQuery.value.limit = "$limit="+ 100;
if(query.value.length === 0){
console.log('Clear manueal')
// console.log(loadStore.loadsTotal)
clearRequest();
filterQuery.value.search = "";
getLoadWithFilters(filterQuery.value);
}
});
const getDataLoadsInit = async(reload) => {
filterQuery.value.limit = '$limit=' + limit;
filterQuery.value.skip = "$skip=0"
filterQuery.value.status = "status[$ne]="+"Closed";
loading.value = true;
await loadStore.getCompanyLoads(filterQuery.value, reload);
loading.value = false;
}
const getLoadsByPage = async(data) => {
console.log(data);
loading.value = true;
filterQuery.value.skip = "$skip="+ data.skip;
loadStore.loadsCurrentPage = data.page
await loadStore.getCompanyLoads(filterQuery.value, true)
loading.value = false;
}
const getLoadWithFilters = async(filter) => {
loading.value = true;
await loadStore.getCompanyLoads(filter, true);
loading.value = false;
}
const search = () => {
setTimeout(() => {
if(query.value.length >= 2){
// filterQuery.value = "company_name[$regex]=" + query.value + "&company_name[$options]=i";
filterQuery.value.search = "posted_by_name[$regex]="+query.value+"&posted_by_name[$options]=i";
getLoadWithFilters(filterQuery.value);
}
}, 100)
}
const clearFilter = () => {
clearRequest();
filterQuery.value.search = ""
filterQuery.value.status = "status[$ne]="+"Closed";
console.log('click here');
if(query.value == ''){
getDataLoadsInit(true);
} else {
query.value = '';
}
}
const clearRequest = () => {
filterQuery.value.skip = "$skip="+ 0;
filterQuery.value.limit = "$limit="+ limit;
loadStore.loadsCurrentPage = 1;
}
const loadHistory = () => {
clearRequest();
filterQuery.value.status = ""
filterQuery.value.status = "status="+ "Closed";
getLoadWithFilters(filterQuery.value);
}
</script>
<template>
<AttachmentsModal v-if="loadStore.openAttachmentsModal"/>
<FormLoadModal v-if="loadStore.openModalEdit"/>
<ProposalsModal v-if="loadStore.openProposalsModal"/>
<div>
<h2 class="title mb-5">Mis cargas publicadas</h2>
<div>
<div class="box-filters">
<div class="box-search">
<input class="form-control custom-search" type="search" name="" placeholder="Nombre de la persona que publica" id="" @:input="search()" v-model="query" aria-label="Search">
</div>
<button
class="btn btn-danger bg-dark" type="button" @click="clearFilter">
<i class="fa-solid fa-arrow-rotate-right"></i>
<span class="clear-sm"> Reset</span><span class="clear-md"> filtros</span>
</button>
<button
@click="loadHistory"
class="btn-primary-sm radius-sm"
><i class="fa-solid fa-clock-rotate-left"></i><span class="clear-sm"> Historial</span><span class="clear-md"> de cargas</span></button>
<button
class="btn-primary-sm radius-sm"
data-toggle="modal" data-target="#formLoadModal"
@click="loadStore.openModalEdit = true"
><i class="fa-solid fa-plus"></i> <span class="clear-sm"> Crear</span><span class="clear-md"> carga</span></button>
</div>
<Spiner v-if="loading"/>
<div v-else>
<CardLoad
v-if="loadStore.loads.length > 0"
v-for="load in loadStore.loads"
:key="load._id"
:load="load"
/>
<CardEmpty v-else text="No hay cargas agregadas"/>
<Pagination
:limit="limit"
:total="loadStore.loadsTotal"
:current-page="loadStore.loadsCurrentPage"
@get-elements="getLoadsByPage"
/>
</div>
</div>
</div>
</template>
<style scoped>
.box-filters {
display: flex;
flex-direction: row;
justify-content: end;
gap: 1rem;
}
.box-search {
width: 40%;
}
.radius-sm{
border-radius: 5px;
}
.custom-search {
width: 100%;
padding: 12px 20px;
}
@media (max-width: 1024px) {
.box-search {
width: 50%;
}
.box-filters {
gap: .4rem;
}
}
@media (max-width: 768px) {
.box-search {
width: 100%;
}
.box-filters {
gap: .3rem;
}
}
</style>

View File

@@ -1,13 +0,0 @@
<script setup>
</script>
<template>
<div>
<h2 class="title">Cargas</h2>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,13 +1,183 @@
<script setup> <script setup>
import { onMounted, ref, watch } from 'vue';
import { useCompanyStore } from '../stores/company';
import Spiner from '../components/ui/Spiner.vue';
import CardEmpty from '../components/CardEmpty.vue';
import CreateLocationModal from '../components/CreateLocationModal.vue';
import CardLocation from '../components/CardLocation.vue';
import Pagination from '../components/Pagination.vue';
const companyStore = useCompanyStore();
const loading = ref(false);
const filterQuery = ref([]);
const query = ref('');
const locationCurrent = ref(null);
const openModal = ref(false);
onMounted(() => {
getInitData();
})
const limit = 3;
const getInitData = async() => {
loading.value = true;
// companyStore.locationsCurrentPage = companyStore.locationsCurrentPage;
filterQuery.value.company = "company="+ localStorage.getItem('id');
await companyStore.getLocationsCompany(filterQuery.value, false)
loading.value = false;
}
const getLocationsByPage = async(data) => {
loading.value = true;
filterQuery.value.company = "company="+ localStorage.getItem('id');
filterQuery.value.skip = "$skip="+ data.skip;
companyStore.locationsCurrentPage = data.page
await companyStore.getLocationsCompany(filterQuery.value, true)
loading.value = false;
}
const getLocationsWithFilters = async(filter) => {
loading.value = true;
await companyStore.getLocationsCompany(filter, true);
loading.value = false;
}
watch(query, () => {
filterQuery.value.skip = "$skip="+ 0;
filterQuery.value.limit = "$limit="+ 100;
if(query.value.length === 0){
clearRequest();
filterQuery.value.search = "";
// filterQuery.value.page = 1
getLocationsWithFilters(filterQuery.value);
}
});
const search = () => {
if(query.value.length >= 2){
// filterQuery.value = "company_name[$regex]=" + query.value + "&company_name[$options]=i";
filterQuery.value.search = "branch_name[$regex]="+ query.value +"&branch_name[$options]=i";
getLocationsWithFilters(filterQuery.value);
}
}
const clearFilter = () => {
clearRequest();
filterQuery.value.search = "";
filterQuery.value.company = "company="+ localStorage.getItem('id');
if(query.value == ''){
getInitData();
} else {
query.value = '';
}
}
const clearRequest = () => {
filterQuery.value.skip = "$skip="+ 0;
filterQuery.value.limit = "$limit="+ limit;
companyStore.locationsCurrentPage = 1;
}
const handleSetCurrentLocation = (location) => {
openModal.value = true;
locationCurrent.value = location;
}
const handleResetCurrentBudget = () => {
openModal.value = false;
locationCurrent.value = null;
}
</script> </script>
<template> <template>
<div> <div>
<CreateLocationModal
v-if="openModal === true"
:location="locationCurrent"
@reset-location="handleResetCurrentBudget"
/>
<h2 class="title">Ubicaciones</h2> <h2 class="title">Ubicaciones</h2>
<div class="box-filters">
<div class="box-search">
<input class="form-control custom-search" type="search" name="" placeholder="Buscar por nombre de locación" id="" @:input="search()" v-model="query" aria-label="Search">
</div>
<button
class="btn btn-danger bg-dark" type="button" @click="clearFilter">
<i class="fa-solid fa-arrow-rotate-right"></i>
<span class="clear-sm"> Reset</span><span class="clear-md"> filtros</span>
</button>
<button
class="btn-primary-sm radius-sm"
data-toggle="modal" data-target="#locationFormModal"
@click="handleSetCurrentLocation(null)"
><i class="fa-solid fa-plus"></i> <span class="clear-sm"> Agregar</span><span class="clear-md"> locación</span></button>
</div>
<div v-if="loading" class="spiner-box">
<Spiner/>
</div>
<div v-else>
<CardLocation
v-if="companyStore.locations.length > 0"
v-for="location in companyStore.locations"
:key="location._id"
:location="location"
@set-location="handleSetCurrentLocation(location)"
/>
<CardEmpty v-else text="No hay ubicaciones agregadas"/>
<Pagination
:limit="limit"
:total="companyStore.locationsTotal"
:current-page="companyStore.locationsCurrentPage"
@get-elements="getLocationsByPage"
/>
</div>
</div> </div>
</template> </template>
<style scoped> <style lang="scss" scoped>
.box-filters {
display: flex;
flex-direction: row;
justify-content: end;
gap: 1rem;
margin: 1.5rem 0px;
}
// .spiner-box {
// display: flex;
// justify-content: center;
// height: calc(100vh - 400px)
// }
.box-search {
width: 60%;
}
.custom-search {
width: 100%;
padding: 12px 20px;
}
@media (max-width: 1024px) {
.box-search {
width: 60%;
}
.box-filters {
gap: .4rem;
}
}
@media (max-width: 768px) {
.box-search {
width: 100%;
}
.box-filters {
gap: .3rem;
}
}
</style> </style>

View File

@@ -8,7 +8,6 @@
import { RouterLink, useRouter } from 'vue-router'; import { RouterLink, useRouter } from 'vue-router';
import { useAuthStore } from '../stores/auth'; import { useAuthStore } from '../stores/auth';
const form = reactive({ const form = reactive({
email: 'alexandrous.dev@gmail.com', email: 'alexandrous.dev@gmail.com',
password: 'Password0', password: 'Password0',
@@ -41,6 +40,9 @@
console.log(resp.data.user); console.log(resp.data.user);
if(resp.data.user.first_name && resp.data.user.last_name) { if(resp.data.user.first_name && resp.data.user.last_name) {
localStorage.setItem('session', resp.data.session_token); localStorage.setItem('session', resp.data.session_token);
localStorage.setItem('access', resp.data.accessToken);
localStorage.setItem('id', resp.data.user.company);
console.log('id', resp.data.user.company)
router.push({name: 'home'}); router.push({name: 'home'});
auth.$patch({ auth.$patch({
sesion: resp.data.session_token, sesion: resp.data.session_token,

View File

@@ -63,19 +63,19 @@
<div class="col-sm-12 col-md-6 col-lg-6"> <div class="col-sm-12 col-md-6 col-lg-6">
<div class="item-company"> <div class="item-company">
<span class="font-weight-bold">Segmento de empresa: </span> <span class="font-weight-bold">Segmento de empresa: </span>
{{company.company?._categories}} {{company.company?.categories.map((e) => e.name).join(', ')}}
</div> </div>
<div class="item-company"> <div class="item-company">
<span class="font-weight-bold">Ubicación de carga por estado: </span> <span class="font-weight-bold">Ubicación de carga por estado: </span>
{{company.company?._company_state}} {{company.company?.company_state.map((e) => e).join(', ')}}
</div> </div>
<div class="item-company"> <div class="item-company">
<span class="font-weight-bold">Ubicación de carga por municipio: </span> <span class="font-weight-bold">Ubicación de carga por municipio: </span>
{{company.company?._company_city}} {{company.company?.company_city.map((e) => e).join(', ')}}
</div> </div>
<div class="item-company"> <div class="item-company">
<span class="font-weight-bold">ETransportes utilizados: </span> <span class="font-weight-bold">Transportes utilizados: </span>
{{company.company?._truck_types}} {{company.company?.truck_type.map((e) => e).join(', ')}}
</div> </div>
<div class="item-company"> <div class="item-company">
<span class="font-weight-bold">Información general de la empresa: </span> <span class="font-weight-bold">Información general de la empresa: </span>
@@ -106,7 +106,7 @@
} }
.item-company { .item-company {
margin-bottom: 8px; margin-bottom: 1rem;
font-size: 1.2rem; font-size: 1.2rem;
color: #323030; color: #323030;
/* font-weight: bold; */ /* font-weight: bold; */

View File

@@ -0,0 +1,184 @@
<template>
<div class="container my-5">
<h1 class="text-center">AVISO DE PRIVACIDAD</h1>
<p class="mt-5">Gracias por elegir ser parte de nuestra comunidad en Vía Porte ("Compañía", "nosotros", "nos", "nuestro"). Estamos comprometidos a proteger su información personal y su derecho a la privacidad. Si tiene alguna pregunta o inquietud sobre este aviso de privacidad o nuestras prácticas con respecto a su información personal, comuníquese con nosotros.</p>
<p>Este aviso de privacidad se aplica a toda la información recopilada a través de nuestros Servicios (que, como se describe anteriormente, incluye nuestro sitio web y nuestra aplicación), así como cualquier servicio, ventas, marketing. Lea atentamente este aviso de privacidad, ya que le ayudará a comprender qué hacemos con la información que recopilamos.</p>
<h4>1. ¿QUÉ INFORMACIÓN RECOPILAMOS?</h4>
<p>Información personal que nos divulga</p>
<p>En resumen: recopilamos la información personal que nos proporciona.</p>
<p>Recopilamos información personal que nos proporciona voluntariamente cuando se registra en los Servicios, expresa su interés en obtener información sobre nosotros o nuestros productos y Servicios, cuando participa en actividades en los Servicios (como publicar mensajes en nuestros foros en línea o participar en concursos, concursos o sorteos) o de otro modo cuando se comunique con nosotros.</p>
<p>La información personal que recopilamos depende del contexto de sus interacciones con nosotros y los Servicios, las elecciones que haga y los productos y funciones que utilice. La información personal que recopilamos puede incluir lo siguiente:</p>
<p>Información personal proporcionada por usted. Recopilamos números de teléfono; nombres; correos electrónicos; nombres de usuario; contraseñas; tipos de remolques; y otra información similar.</p>
<p>Podemos recopilar lo necesarios para procesar su pago si realiza compras, como el número de su instrumento de pago (como un número de tarjeta de crédito) y el código de seguridad asociado con su instrumento de pago.</p>
<p>Da de inicio de sesión en redes sociales. Podemos brindarle la opción de registrarse con nosotros utilizando los detalles de su cuenta de redes sociales existente, como su cuenta de Facebook, Twitter u otra cuenta de redes sociales. Si opta por registrarse de esta manera, recopilaremos la información descrita en la sección denominada "¿CÓMO MANEJAMOS SUS INICIOS SOCIALES?" debajo.</p>
<p>Toda la información personal que nos proporcione debe ser verdadera, completa y precisa, y debe notificarnos cualquier cambio en dicha información personal.</p>
<p class="small">Información recopilada automáticamente</p>
<p>La de registro y uso. Los da de registro y uso son información relacionada con el servicio, diagnóstico, uso y rendimiento que nuestros servidores recopilan automáticamente cuando usted accede o utiliza nuestros Servicios y que registramos en archivos de registro. Dependiendo de cómo interactúe con nosotros, es da de registro pueden incluir su dirección IP, información del dispositivo, tipo de navegador y configuración e información sobre su actividad en los Servicios (como las marcas de fecha / hora asociadas con su uso, páginas y archivos visualizados, búsquedas y otras acciones que realiza, como las funciones que usa), información del dispositivo (como la actividad del sistema, informes de errores (a veces llamados 'volcados por caída') y configuraciones de hardware).</p>
<p>Del dispositivo. Recopilamos da del dispositivo, como información sobre su computadora, teléfono, tableta u otro dispositivo que utilice para acceder a los Servicios. Dependiendo del dispositivo utilizado, es da del dispositivo pueden incluir información como su dirección IP (o servidor proxy), números de identificación de dispositivo y aplicación, ubicación, tipo de navegador, modelo de hardware Proveedor de servicios de Internet y/o operador de telefonía móvil, sistema operativo y configuración del sistema. información.</p>
<p>De localización. Recopilamos da de ubicación, como información sobre la ubicación de su dispositivo, que puede ser precisa o imprecisa. La cantidad de información que recopilamos depende del tipo y la configuración del dispositivo que utiliza para acceder a los Servicios. Por ejemplo, podemos utilizar GPS y otras tecnologías para recopilar da de geolocalización que nos indiquen su ubicación actual (según su dirección IP). Puede optar por no permitirnos recopilar esta información, ya sea rechazando el acceso a la información o desactivando la configuración de Ubicación en su dispositivo. Sin embargo, tenga en cuenta que, si opta por no participar, es posible que no pueda utilizar ciertos aspectos de los Servicios.</p>
<h5>Información recopilada a través de nuestra aplicación</h5>
<p>En resumen: recopilamos información sobre su geolocalización, dispositivo móvil y notificaciones cuando utiliza nuestra aplicación.</p>
<p>Si usa nuestra aplicación, también recopilamos la siguiente información:</p>
<p>Información de geolocalización. Podemos solicitar acceso o permiso y rastrear información basada en la ubicación desde su dispositivo móvil, ya sea de forma continua o mientras usa nuestra Aplicación, para proporcionar ciertos servicios basados en la ubicación. Si desea cambiar nuestro acceso o permisos, puede hacerlo en la configuración de su dispositivo.</p>
<p>Acceso a dispositivos móviles. Podemos solicitar acceso o permiso a ciertas funciones desde su dispositivo móvil, incluida la cámara, el micrófono, los sensores y otras funciones de su dispositivo móvil. Si desea cambiar nuestro acceso o permisos, puede hacerlo en la configuración de su dispositivo.</p>
<p>La del dispositivo móvil. Recopilamos automáticamente información del dispositivo (como su ID de dispositivo móvil, modelo y fabricante), sistema operativo, información de versión e información de configuración del sistema, números de identificación de dispositivo y aplicación, tipo y versión de navegador, modelo de hardware, proveedor de servicios de Internet y/o operador de telefonía móvil. y dirección de Protocolo de Internet (IP) (o servidor proxy). Si está utilizando nuestra aplicación, también podemos recopilar información sobre la red telefónica asociada con su dispositivo móvil, el sistema operativo o la plataforma de su dispositivo móvil, el tipo de dispositivo móvil que utiliza, la identificación única del dispositivo de su dispositivo móvil e información sobre las funciones de nuestra aplicación a la que accedió.</p>
<p>Podemos solicitar que le enviemos notificaciones automáticas sobre su cuenta o ciertas funciones de la aplicación. Si desea optar por no recibir este tipo de comunicaciones, puede desactivarlas en la configuración de su dispositivo.</p>
<p>Esta información es principalmente necesaria para mantener la seguridad y el funcionamiento de nuestra aplicación, para la resolución de problemas y para nuestros análisis internos y propósitos de informes.</p>
<h4>2. ¿CÓMO UTILIZAMOS SU INFORMACIÓN?</h4>
<p>En resumen: procesamos su información con fines basados en intereses comerciales legítimos, el cumplimiento de nuestro contrato con usted, el cumplimiento de nuestras obligaciones legales y / o su consentimiento.</p>
<p>Usamos la información personal recopilada a través de nuestros Servicios para una variedad de propósitos comerciales que se describen a continuación. Procesamos su información personal para es fines basándonos en nuestros intereses comerciales legítimos, para celebrar o ejecutar un contrato con usted, con su consentimiento, y / o para cumplir con nuestras obligaciones legales. Indicamos los motivos de procesamiento específicos en los que confiamos junto a cada propósito que se enumera a continuación.</p>
<h5>Usamos la información que recopilamos o recibimos:</h5>
<p>Para facilitar la creación de cuentas y el proceso de inicio de sesión. Si elige vincular su cuenta con nosotros a una cuenta de un tercero (como su cuenta de Google o Facebook), usamos la información que nos permitió recopilar de esos terceros para facilitar la creación de la cuenta y el proceso de inicio de sesión para el desempeño del contrato. Consulte la sección a continuación titulada "¿CÓMO MANEJAMOS SUS INGRESOS SOCIALES?" para más información.</p>
<p>Publicar testimonios. Publicamos testimonios en nuestros Servicios que pueden contener información personal. Antes de publicar un testimonio, obtendremos su consentimiento para usar su nombre y el contenido del testimonio. Si desea actualizar o eliminar su testimonio, contáctenos y asegúrese de incluir su nombre, ubicación del testimonio e información de contacto.</p>
<p>Solicite comentarios. Podemos utilizar su información para solicitar comentarios y comunicarnos con usted sobre su uso de nuestros Servicios.</p>
<p>Para habilitar las comunicaciones de usuario a usuario. Podemos utilizar su información para permitir las comunicaciones de usuario a usuario con el consentimiento de cada usuario.</p>
<p>Para administrar cuentas de usuario. Podemos utilizar su información con el fin de administrar nuestra cuenta y mantenerla en funcionamiento.</p>
<p>Para enviarle información administrativa. Podemos utilizar su información personal para enviarle información sobre productos, servicios y nuevas funciones y/o información sobre cambios en nuestros términos, condiciones y políticas.</p>
<p>Para proteger nuestros Servicios. Podemos utilizar su información como parte de nuestros esfuerzos para mantener nuestros Servicios seguros y protegidos (por ejemplo, para el monitoreo y la prevención de fraudes).</p>
<p>Para hacer cumplir nuestros términos, condiciones y políticas con fines comerciales, para cumplir con los requisitos legales y reglamentarios o en relación con nuestro contrato.</p>
<p>Para responder a solicitudes legales y prevenir daños. Si recibimos una citación u otra solicitud legal, es posible que debamos inspeccionar los da que tenemos para determinar cómo responder.</p>
<p>Cumplir y gestionar sus pedidos. Podemos utilizar su información para cumplir y administrar sus pedidos, pagos, devoluciones e intercambios realizados a través de los Servicios.</p>
<p>Administrar sorteos y concursos. Podemos utilizar su información para administrar sorteos de premios y concursos cuando elija participar en nuestros concursos.</p>
<p>Para entregar y facilitar la entrega de servicios al usuario. Podemos utilizar su información para brindarle el servicio solicitado.</p>
<p>Responder a las consultas de los usuarios / ofrecer soporte a los usuarios. Podemos utilizar su información para responder a sus consultas y resolver cualquier problema potencial que pueda tener con el uso de nuestros Servicios.</p>
<p>Para enviarle comunicaciones de marketing y promocionales. Nosotros y / o nuestros socios de marketing externos podemos utilizar la información personal que nos envíe para nuestros fines de marketing, si esto está de acuerdo con sus preferencias de marketing. Por ejemplo, cuando exprese su interés en obtener información sobre nosotros o nuestros Servicios, se suscriba al marketing o se ponga en contacto con nosotros, recopilaremos su información personal. Puede optar por no recibir nuestros correos electrónicos de marketing en cualquier momento (consulte "¿CUÁLES SON SUS DERECHOS DE PRIVACIDAD?" A continuación).</p>
<p>Entregarle publicidad dirigida. Podemos utilizar su información para desarrollar y mostrar contenido y publicidad personalizados (y trabajar con terceros que lo hagan) adaptados a sus intereses y / o ubicación y para medir su efectividad.</p>
<p>Para otros fines comerciales. Podemos utilizar su información para otros fines comerciales, como análisis de datos, identificación de tendencias de uso, determinación de la eficacia de nuestras campañas promocionales y para evaluar y mejorar nuestros Servicios, productos, marketing y su experiencia. Podemos usar y almacenar esta información en forma agregada y anónima para que no esté asociada con usuarios finales individuales y no incluya información personal. No usaremos información personal identificable sin su consentimiento.</p>
<h4>3. ¿SE COMPARTIRÁ SU INFORMACIÓN CON ALGUIEN?</h4>
<p>En resumen: solo compartimos información con su consentimiento, para cumplir con las leyes, para brindarle servicios, para proteger sus derechos o para cumplir con las obligaciones comerciales.</p>
<p>Podemos procesar o compartir sus da que tenemos en base a la siguiente base legal:</p>
<p>Consentimiento: podemos procesar sus da si nos ha dado su consentimiento específico para usar su información personal para un propósito específico.</p>
<p>Intereses legítimos: podemos procesar sus da cuando sea razonablemente necesario para lograr nuestros intereses comerciales legítimos.</p>
<p>Ejecución de un contrato: cuando hayamos celebrado un contrato con usted, podemos procesar su información personal para cumplir con los términos de nuestro contrato.</p>
<p>Obligaciones legales: podemos divulgar su información cuando estemos legalmente obligados a hacerlo para cumplir con la ley aplicable, las solicitudes gubernamentales, un procedimiento judicial, una orden judicial o un proceso legal, como en respuesta a una orden judicial o una citación (incluso en respuesta a las autoridades para cumplir con los requisitos de seguridad nacional o de aplicación de la ley).</p>
<p>Intereses vitales: podemos divulgar su información cuando creamos que es necesario para investigar, prevenir o tomar medidas con respecto a posibles violaciones de nuestras políticas, sospecha de fraude, situaciones que impliquen amenazas potenciales a la seguridad de cualquier persona y actividades ilegales, o como evidencia en litigio en el que estamos involucrados.</p>
<p>Más específicamente, es posible que necesitemos procesar sus da o compartir su información personal en las siguientes situaciones:</p>
<p>Transferencias comerciales. Podemos compartir o transferir su información en relación con, o durante las negociaciones de, cualquier fusión, venta de activos de la empresa, financiación o adquisición de la totalidad o una parte de nuestro negocio a otra empresa.</p>
<p>API de Google Maps Platform. Podemos compartir su información con ciertas API de Google Maps Platform (por ejemplo, API de Google Maps, API de lugar). Para obtener más información sobre la Política de privacidad de Google, consulte este enlace. Obtenemos y almacenamos en su dispositivo ('caché') su ubicación. Puede revocar su consentimiento en cualquier momento comunicándose con nosotros a la información de contacto proporcionada al final de este documento.</p>
<p>Proveedores, consultores y otros proveedores de servicios externos. Podemos compartir sus da con proveedores externos, proveedores de servicios, contratistas o agentes que brindan servicios para nosotros o en nuestro nombre y requieren acceso a dicha información para realizar ese trabajo. Los ejemplos incluyen: procesamiento de pagos, análisis de da, entrega de correo electrónico, servicios de alojamiento, servicio al cliente y esfuerzos de marketing. Podemos permitir que terceros seleccionados utilicen tecnología de seguimiento en los Servicios, lo que les permitirá recopilar da en nuestro nombre sobre cómo interactúa con nuestros Servicios a lo largo del tiempo. Esta información puede usarse para, entre otras cosas, analizar y rastrear da, determinar la popularidad de cierto contenido, páginas o características, y comprender mejor la actividad en línea. A menos que se describa en este aviso, no compartimos, vendemos, alquilamos ni intercambiamos su información con terceros con fines promocionales.</p>
<p>Anunciantes de terceros. Podemos utilizar empresas de publicidad de terceros para publicar anuncios cuando visita o utiliza los Servicios. Estas empresas pueden utilizar información sobre sus visitas a nuestro (s) sitio (s) web (s) y otros sitios web que están contenidos en cookies web y otras tecnologías de seguimiento con el fin de proporcionar anuncios sobre bienes y servicios de su interés.</p>
<p>Otros usuarios. Cuando comparte información personal (por ejemplo, al publicar comentarios, contribuciones u otro contenido en los Servicios) o interactúa de otra manera con áreas públicas de los Servicios, todos los usuarios pueden ver dicha información personal y puede estar disponible públicamente fuera de los Servicios en perpetuidad. Si interactúa con otros usuarios de nuestros Servicios y se registra en nuestros Servicios a través de una red social (como Facebook), sus contactos en la red social verán su nombre, foto de perfil y descripciones de su actividad. Del mismo modo, otros usuarios podrán ver descripciones de su actividad, comunicarse con usted dentro de nuestros Servicios y ver su perfil.</p>
<h4>4. ¿CON QUIÉN SE COMPARTIRÁ SU INFORMACIÓN?</h4>
<p>En resumen: solo compartimos información con las siguientes categorías de terceros.</p>
<p>Solo compartimos y divulgamos su información con las siguientes categorías de terceros. Si hemos procesado sus datos en función de su consentimiento y desea revocar su consentimiento, comuníquese con nosotros utilizando los da de contacto proporcionados en la sección a continuación titulada <b>"¿CÓMO PUEDE CONTACTARNOS SOBRE ESTE AVISO?".</b></p>
<h5>Servicios de análisis de datos</h5>
<h4>5. ¿UTILIZAMOS COOKIES Y OTRAS TECNOLOGÍAS DE SEGUIMIENTO?</h4>
<p>En resumen: podemos utilizar cookies y otras tecnologías de seguimiento para recopilar y almacenar su información.</p>
<p>Podemos utilizar cookies y tecnologías de seguimiento similares (como balizas web y píxeles) para acceder o almacenar información. La información específica sobre cómo usamos dichas tecnologías y cómo puede rechazar ciertas cookies se establece en nuestro Aviso de cookies.</p>
<h4>6. ¿CÓMO MANEJAMOS SUS INGRESOS SOCIALES?</h4>
<p>En resumen: si elige registrarse o iniciar sesión en nuestros servicios utilizando una cuenta de redes sociales, es posible que tengamos acceso a cierta información sobre usted.</p>
<p>Nuestros servicios le ofrecen la posibilidad de registrarse e iniciar sesión utilizando los detalles de su cuenta de redes sociales de terceros (como sus inicios de sesión en Facebook o Twitter). Cuando elija hacer esto, recibiremos cierta información de perfil sobre usted de su proveedor de redes sociales. La información de perfil que recibimos puede variar según el proveedor de redes sociales en cuestión, pero a menudo incluirá su nombre, dirección de correo electrónico, lista de amigos, foto de perfil y otra información que elija hacer pública en dicha plataforma de redes sociales.</p>
<p>Utilizaremos la información que recibimos solo para los fines que se describen en este aviso de privacidad o que se le aclaran en los Servicios correspondientes. Tenga en cuenta que no controlamos ni somos responsables de otros usos de su información personal por parte de su proveedor externo de redes sociales. Le recomendamos que revise su aviso de privacidad para comprender cómo recopilan, usan y comparten su información personal, y cómo puede establecer sus preferencias de privacidad en sus sitios y aplicaciones.</p>
<h4>7. ¿CUÁL ES NUESTRA POSTURA EN LOS SITIOS WEB DE TERCEROS?</h4>
<p>En resumen: no somos responsables de la seguridad de la información que usted comparte con proveedores externos que publicitan, pero que no están afiliados a nuestro sitio web.</p>
<p>Los Servicios pueden contener anuncios de terceros que no están afiliados a nosotros y que pueden vincular a otros sitios web, servicios en línea o aplicaciones móviles. No podemos garantizar la seguridad y privacidad de los da que proporciona a terceros. Los recopilados por terceros no están cubiertos por este aviso de privacidad. No somos responsables del contenido o las prácticas y políticas de privacidad y seguridad de terceros, incluidos otros sitios web, servicios o aplicaciones que puedan estar vinculados a los Servicios o desde ellos. Debe revisar las políticas de dichos terceros y contactarlos directamente para responder a sus preguntas.</p>
<h4>8. ¿POR CUÁNTO TIEMPO CONSERVAMOS SU INFORMACIÓN?</h4>
<p>En resumen: Conservamos su información durante el tiempo que sea necesario para cumplir con los propósitos que se describe en este aviso de privacidad, a menos que la ley exija lo contrario.</p>
<p>Solo conservaremos su información personal durante el tiempo que sea necesario para los fines establecidos en este aviso de privacidad, a menos que la ley exija o permita un período de retención más prolongado (como impuestos, contabilidad u otros requisitos legales). Ningún propósito en este aviso requerirá que mantengamos su información personal por más de doce (12) meses después del inicio del período inactivo de la cuenta del usuario.</p>
<p>Cuando no tengamos una necesidad comercial legítima en curso para procesar su información personal, eliminaremos o anonimizaremos dicha información o, si esto no es posible (por ejemplo, porque su información personal se ha almacenado en archivos de respaldo), lo haremos de manera segura almacenar su información personal y aislarla de cualquier procesamiento posterior hasta que sea posible la eliminación.</p>
<h4>9. ¿CÓMO MANTENEMOS SEGURA SU INFORMACIÓN?</h4>
<p>En resumen: nuestro objetivo es proteger su información personal a través de un sistema de medidas de seguridad organizativas y técnicas.</p>
<p>Hemos implementado medidas de seguridad técnicas y organizativas apropiadas diseñadas para proteger la seguridad de cualquier información personal que procesamos. Sin embargo, a pesar de nuestras salvaguardas y esfuerzos para asegurar su información, no se puede garantizar que ninguna transmisión electrónica a través de Internet o tecnología de almacenamiento de información sea 100% segura, por lo que no podemos prometer ni garantizar que los piratas informáticos, los ciberdelincuentes u otros terceros no autorizados no serán capaz de vencer nuestra seguridad y recopilar, acceder, robar o modificar su información de manera inapropiada. Aunque haremos todo lo posible para proteger su información personal, la transmisión de información personal hacia y desde nuestros Servicios es bajo su propio riesgo. Solo debe acceder a los Servicios dentro de un entorno seguro.</p>
<h4>10. ¿RECOPILAMOS INFORMACIÓN DE MENORES?</h4>
<p>En resumen: no recopilamos ni comercializamos a sabiendas de niños menores de 18 años.</p>
<p>No solicitamos a sabiendas ni comercializamos a niños menores de 18 años. Al utilizar los Servicios, usted declara que tiene al menos 18 años o que es el padre o tutor de dicho menor y da su consentimiento para el uso de los Servicios por parte de dicho menor dependiente. Si nos enteramos de que se ha recopilado información personal de usuarios menores de 18 años, desactivaremos la cuenta y tomaremos medidas razonables para eliminar de inmediato dichos da de nuestros registros. Si tiene conocimiento de algún dato que podamos haber recopilado de niños menores de 18 años</p>
<p class="small">¿CUÁLES SON SUS DERECHOS DE PRIVACIDAD?</p>
<p>En resumen: puede revisar, cambiar o cancelar su cuenta en cualquier momento.</p>
<p class="small">Información de la cuenta</p>
<p>Si en algún momento desea revisar o cambiar la información de su cuenta o cancelar su cuenta, puede:</p>
<ul>
<li>Inicie sesión en la configuración de su cuenta y actualice su cuenta de usuario.</li>
<li>Contáctenos usando la información de contacto proporcionada.</li>
</ul>
<p>Si solicita cancelar su cuenta, desactivaremos o eliminaremos su cuenta e información de nuestras bases de da activas. Sin embargo, podemos retener cierta información en nuestros archivos para prevenir fraudes, solucionar problemas, ayudar con cualquier investigación, hacer cumplir nuestros Términos de uso y / o cumplir con los requisitos legales aplicables.</p>
<p>Cookies y tecnologías similares: la mayoría de los navegadores web están configurados para aceptar cookies de forma predeterminada. Si lo prefiere, normalmente puede optar por configurar su navegador para eliminar las cookies y rechazar las cookies. Si elige eliminar las cookies o rechazarlas, esto podría afectar ciertas características o servicios de nuestros Servicios.</p>
<p>Opción de no recibir marketing por correo electrónico: puede darse de baja de nuestra lista de correo electrónico de marketing en cualquier momento haciendo clic en el enlace para darse de baja en los correos electrónicos que le enviamos o poniéndose en contacto con nosotros utilizando los detalles que se proporcionan a continuación. Luego será eliminado de la lista de correo electrónico de marketing; sin embargo, aún podemos comunicarnos con usted, por ejemplo, para enviarle correos electrónicos relacionados con el servicio que son necesarios para la administración y el uso de su cuenta, para responder a las solicitudes de servicio o para otros fines no comerciales. Para optar por no participar, puede:</p>
<ul>
<li>Acceda a la configuración de su cuenta y actualice sus preferencias.</li>
<li>Contáctenos usando la información de contacto proporcionada.</li>
</ul>
<h4>12. CONTROLES PARA LAS CARACTERÍSTICAS DE NO SEGUIR</h4>
<p>La mayoría de los navegadores web y algunos sistemas operativos y aplicaciones móviles incluyen una función o configuración de No rastrear ("DNT") que puede activar para indicar su preferencia de privacidad para que no se controlen y recopilen sobre sus actividades de navegación en línea. En esta etapa, no se ha finalizado ningún estándar tecnológico uniforme para reconocer e implementar señales DNT. Como tal, actualmente no respondemos a las señales del navegador DNT ni a ningún otro mecanismo que comunique automáticamente su elección de no ser rastreado en línea. Si se adopta un estándar para el seguimiento en línea que debemos seguir en el futuro, le informaremos sobre esa práctica en una versión revisada de este aviso de privacidad.</p>
<h5>¿Cómo utilizamos y compartimos su información personal?</h5>
<p>Vía Porte recopila y comparte su información personal a través de:</p>
<h5>Cookies de orientación / cookies de marketing</h5>
<p>Puede encontrar más información sobre nuestras prácticas de recopilación e intercambio de este aviso de privacidad.</p>
<p>Si está utilizando un agente autorizado para ejercer su derecho de exclusión voluntaria, podemos denegar una solicitud si el agente autorizado no presenta prueba de que ha sido autorizado válidamente para actuar en su nombre.</p>
<p class="small">¿Se compartirá su información con alguien más?</p>
<p>Podemos divulgar su información personal con nuestros proveedores de servicios de conformidad con un contrato escrito entre nosotros y cada proveedor de servicios. Cada proveedor de servicios es una entidad con fines de lucro que procesa la información en nuestro nombre.</p>
<p>Podemos utilizar su información personal para nuestros propios fines comerciales, como para realizar investigaciones internas para el desarrollo tecnológico y la demostración. Esto no se considera una "venta" de sus da personales.</p>
<p>Vía Porte ha revelado las siguientes categorías de información personal a terceros con fines comerciales en los doce (12) meses anteriores:</p>
<p>Categoría A. Identificadores, como detalles de contacto, como su nombre real, alias, dirección postal, número de teléfono o de contacto móvil, identificador personal único, identificador en línea, dirección de Protocolo de Internet, dirección de correo electrónico y nombre de cuenta.</p>
<p>Categoría E. Información biométrica, como huellas dactilares y de voz.</p>
<p>Categoría F. Información de actividad de Internet u otra red electrónica, como historial de navegación, historial de búsqueda, comportamiento en línea, da de interés e interacciones con nuestro y otros sitios web, aplicaciones, sistemas y anuncios.</p>
<p>Categoría I. Información profesional o relacionada con el empleo, como los da de contacto de la empresa para poder brindarle nuestros servicios a nivel empresarial, título del trabajo, así como historial laboral y calificaciones profesionales si solicita un trabajo con nosotros.</p>
<p>Las categorías de terceros a quiénes revelamos información personal con fines comerciales o comerciales se pueden encontrar en "¿CON QUIÉN SE COMPARTIRÁ SU INFORMACIÓN?".</p>
<p>Vía Porte no vende ninguna información personal a terceros con fines comerciales o comerciales en los doce (12) meses anteriores. Vía Porte no venderá información personal en el futuro perteneciente a visitantes del sitio web, usuarios y otros consumidores.</p>
<h5>Sus derechos con respecto a sus datos personales</h5>
<p>Puede solicitar la eliminación de su información personal. Si nos solicita que eliminemos su información personal, respetaremos su solicitud y eliminaremos su información personal, sujeto a ciertas excepciones previstas por la ley, tales como (pero no limitado a) el ejercicio por otro consumidor de su derecho a la libertad de expresión. Nuestros requisitos de cumplimiento resultantes de una obligación legal o cualquier procesamiento que pueda ser necesario para proteger contra actividades ilegales.</p>
<h5>Derecho a ser informado - Solicitud de información</h5>
<p>Dependiendo de las circunstancias, tiene derecho a saber:</p>
<ul>
<li>
Si recopilamos y usamos su información personal;<br><br>
las categorías de información personal que recopilamos;<br><br>
los fines para los que se utiliza la información personal recopilada;<br><br>
</li>
<li>
Si vendemos su información personal a terceros;<br><br>
las categorías de información personal que vendimos o divulgamos con fines comerciales;<br><br>
las categorías de terceros a quiénes se vendió o divulgó la información personal con fines comerciales; yel propósito comercial o empresarial para recopilar o vender información personal.<br><br>
</li>
<li>De acuerdo con la ley aplicable, no estamos obligados a proporcionar o eliminar información del consumidor que no esté identificada en respuesta a una solicitud de un consumidor ni a volver a identificar da individuales para verificar una solicitud de un consumidor.</li>
<li>Derecho a la no discriminación para el ejercicio de los derechos de privacidad del consumidor</li>
<li>No lo discriminaremos si ejerce sus derechos de privacidad.</li>
</ul>
<h5>Proceso de verificación</h5>
<p>Al recibir su solicitud, necesitaremos verificar su identidad para determinar que es la misma persona de la que tenemos la información en nuestro sistema. Es esfuerzos de verificación requieren que le pidamos que proporcione información para que podamos compararla con la información que nos ha proporcionado anteriormente. Por ejemplo, según el tipo de solicitud que envíe, es posible que le pidamos que proporcione cierta información para que podamos relacionar la información que proporcione con la información que ya tenemos registrada, o podemos comunicarnos con usted a través de un método de comunicación (por ejemplo, teléfono o correo electrónico) que nos haya proporcionado previamente. También podemos utilizar otros métodos de verificación según lo requieran las circunstancias.</p>
<p>Solo usaremos la información personal proporcionada en su solicitud para verificar su identidad o autoridad para realizar la solicitud. En la medida de lo posible, evitaremos solicitarle información adicional para fines de verificación. Sin embargo, si no podemos verificar su identidad a partir de la información que ya tenemos, podemos solicitarle que proporcione información adicional con el fin de verificar su identidad y con fines de seguridad o prevención de fraudes. Eliminaremos dicha información adicional tan pronto como terminemos de verificarlo.</p>
<h4>14. ¿HACEMOS ACTUALIZACIONES A ESTE AVISO?</h4>
<p>En resumen: , actualizaremos este aviso según sea necesario para cumplir con las leyes pertinentes.</p>
<p>Es posible que actualicemos este aviso de privacidad de vez en cuando. La versión actualizada se indicará con una fecha "Revisada" actualizada y la versión actualizada entrará en vigor tan pronto como sea accesible. Si realizamos cambios sustanciales a este aviso de privacidad, podemos notificarle publicando un aviso de manera prominente de dichos cambios o enviándole directamente una notificación. Le recomendamos que revise este aviso de privacidad con frecuencia para estar informado de cómo protegemos su información.</p>
<h4>15. ¿CÓMO PUEDE REVISAR, ACTUALIZAR O ELIMINAR LOS DATOS QUE RECOPILAMOS DE USTED?</h4>
<p>Según las leyes aplicables de su país, es posible que tenga derecho a solicitar acceso a la información personal que recopilamos de usted, cambiar esa información o eliminarla en algunas circunstancias. Para solicitar revisar, actualizar o eliminar su información personal, envíe un correo electrónico o llame al teléfono que aparece en nuestra página.</p>
</div>
</template>
<style scoped>
p{
font-size: 1.1rem;
}
.small {
font-size: 1rem;
}
h3 {
font-size: 1.4rem;
text-align: center;
margin: 1rem 0px;
}
h4 {
font-size: 1.2rem;
text-align: left;
margin: 1rem 0px;
font-weight: normal;
}
h5 {
font-size: 1.2rem;
text-align: left;
margin: 1rem 0px;
font-weight: normal;
text-align: center;
}
ul > li {
font-size: 1.1rem;
margin: 0.5rem 0px;
}
span {
margin-bottom: 12px;
}
</style>

View File

@@ -0,0 +1,39 @@
<script setup>
import { onMounted, ref } from 'vue';
import Spiner from '../components/ui/Spiner.vue';
import { useRoute } from 'vue-router';
import useDirectory from '../composables/useDirectory';
import CardUser from '../components/CardUser.vue';
const {loading, users, getUsersData} = useDirectory();
const {params} = useRoute();
onMounted(() => {
const id = params.id;
getUsersData(id);
});
</script>
<template>
<div class="mb-5">
<h2 class="title mt-5 mb-5">Usuarios de la <span class="title-main">Empresa</span></h2>
<Spiner v-if="loading"/>
<div
class="card-info flex-column align-items-center"
v-if="loading === false && users.length <= 0"
>
<img src="/images/logo.png" alt="logo" width="100">
<h2 class="title">No hay registros</h2>
</div>
<CardUser
v-for="user in users"
:user="user"
:readonly="true"
/>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,13 +0,0 @@
<script setup>
</script>
<template>
<div>
<h2 class="title">Mis publicaciones</h2>
</div>
</template>
<style scoped>
</style>

View File

@@ -174,7 +174,18 @@
type="submit" type="submit"
class="btn-primary-lg btn-lg-block radius-1 mt-5" value="Continuar"> class="btn-primary-lg btn-lg-block radius-1 mt-5" value="Continuar">
<p class="mt-5 fs-6 text-center">Al registrarte aceptas nuestros <RouterLink class="btn-text" :to="{name: 'login'}">términos y cóndiciones</RouterLink></p> <p class="mt-5 fs-6 text-center">
Al registrarte aceptas nuestros
<RouterLink
class="btn-text me-1"
:to="{name: 'terms-conditions'}"
target="_blank">términos y condiciones</RouterLink>
y
<RouterLink
class="btn-text ms-1"
:to="{name: 'notice-privacy'}"
target="_blank"> Aviso de privaciadad</RouterLink>
</p>
<p class="mt-5 fs-6 text-center">¿Ya tienes una cuenta? <RouterLink class="btn-text" :to="{name: 'login'}">Ingresa aqui</RouterLink></p> <p class="mt-5 fs-6 text-center">¿Ya tienes una cuenta? <RouterLink class="btn-text" :to="{name: 'login'}">Ingresa aqui</RouterLink></p>
</form> </form>

View File

@@ -0,0 +1,240 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import useSearchLoads from '../composables/userSearchLoads';
import Spiner from '../components/ui/Spiner.vue';
import CardLoad from '../components/CardLoad.vue'
import CardEmpty from '../components/CardEmpty.vue'
import TruckTypes from '../components/ui/TruckTypes.vue';
import Segments from '../components/ui/Segments.vue';
import States from '../components/ui/States.vue';
import Cities from '../components/ui/Cities.vue';
import MakeProposalModal from '../components/MakeProposalModal.vue';
import Pagination from '../components/pagination.vue';
const filterQuery = ref([]);
const query = ref('');
const selectedTruckType = ref([]);
const selectedCategory = ref([]);
const selectedState = ref([]);
const selectedCities = ref([]);
const limit = 3;
const isSearch = ref(false);
const { getLoadsPublished, loads, loading, currentPage, total } = useSearchLoads();
onMounted(() => {
getInitData();
});
watch(query, () => {
isSearch.value = true;
setFilterUnlimited();
if(query.value.length === 0){
if(selectedCategory.value?.length === 0 && selectedTruckType.value?.length === 0 && selectedCities.value?.length === 0 && selectedState.value?.length === 0 ) {
clearRequest();
isSearch.value = false;
}
filterQuery.value.search = "";
getLoadsPublished(filterQuery.value);
}
});
watch(selectedState, () => {
if(selectedState.value != null){
setFilterUnlimited()
filterQuery.value.state = "origin.state[$in][]="+ selectedState.value.state_name;
getLoadsPublished(filterQuery.value);
}
});
watch(selectedCities, () => {
if(selectedCities.value != null){
setFilterUnlimited()
filterQuery.value.city = "origin.city[$regex]="+ selectedCities.value.city_name;
getLoadsPublished(filterQuery.value);
}
});
watch(selectedCategory, () => {
if(selectedCategory.value != null){
setFilterUnlimited()
filterQuery.value.category = "categories[$in][]=" + selectedCategory.value._id;
getLoadsPublished(filterQuery.value);
}
});
watch(selectedTruckType, () => {
if(selectedTruckType.value != null){
setFilterUnlimited()
filterQuery.value.truck_type = "truck_type[$in][]="+ selectedTruckType.value.meta_value;
getLoadsPublished(filterQuery.value);
}
});
const search = () => {
if(query.value.length >= 2){
// filterQuery.value = "company_name[$regex]=" + query.value + "&company_name[$options]=i";
filterQuery.value.search = "company.company_name[$regex]=" + query.value + "&company.company_name[$options]=i";
getLoadsPublished(filterQuery.value);
}
}
const clearFilter = () => {
clearRequest();
isSearch.value = false;
selectedCities.value = null;
selectedCategory.value = null;
selectedState.value = null;
selectedTruckType.value = null;
filterQuery.value.truck_type = "";
filterQuery.value.category = "";
filterQuery.value.city = "";
filterQuery.value.state = "";
filterQuery.value.status = "status=Published";
if(query.value == ''){
getLoadsPublished(filterQuery.value);
} else {
query.value = '';
}
}
const clearState = () => {
filterQuery.value.state = "";
getLoadsPublished(filterQuery.value);
}
const clearCity = () => {
filterQuery.value.city = "";
getLoadsPublished(filterQuery.value);
}
const clearTruckType = () => {
filterQuery.value.truck_type = "";
getLoadsPublished(filterQuery.value);
}
const clearCategory = () => {
filterQuery.value.category = "";
getLoadsPublished(filterQuery.value);
}
const getInitData = async() => {
filterQuery.value.limit = '$limit=' + limit;
filterQuery.value.skip = "$skip=0";
filterQuery.value.company = "";
filterQuery.value.status = "status=Published",
await getLoadsPublished(filterQuery.value);
}
const getLoadsByPage = (data) => {
console.log(data);
filterQuery.value.skip = "$skip="+ data.skip;
currentPage.value = data.page
getLoadsPublished(filterQuery.value);
}
const clearRequest = () => {
filterQuery.value.skip = "$skip="+ 0;
filterQuery.value.limit = "$limit="+ limit;
currentPage.value = 1;
}
const setFilterUnlimited = () => {
filterQuery.value.skip = "$skip="+ 0;
filterQuery.value.limit = "$limit="+ 100;
}
const currentLoad = ref(null);
const handleSetCurrentLoad = (load) => {
console.log(load);
currentLoad.value = load
}
const handleResetCurrentLoad = () => {
console.log('se resear');
currentLoad.value = null
}
</script>
<template>
<h2 class="title mb-5">Buscar cargas</h2>
<MakeProposalModal
v-if="currentLoad"
:load="currentLoad"
@reset-load="handleResetCurrentLoad"
/>
<div class="card-filters">
<div class="d-flex mb-2">
<input class="form-control me-2" type="search" name="" placeholder="Buscar embarcador" id="" @:input="search()" v-model="query" aria-label="Search">
<button class="btn btn-outline-dark me-2" type="button" @click="search">Buscar</button>
<button
class="btn btn-danger" type="button" @click="clearFilter">
<i class="fa-solid fa-arrow-rotate-right"></i>
</button>
</div>
<div class="row">
<div class="col-xs-12 col-sm-6 px-2 py-2">
<TruckTypes v-model="selectedTruckType" @clear-option="clearTruckType"/>
</div>
<div class=" col-xs-12 col-sm-6 px-2 py-2">
<Segments v-model="selectedCategory" @clear-option="clearCategory"/>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-6 px-2 py-2">
<States v-model="selectedState" @clear-option="clearState"/>
</div>
<div class="col-xs-12 col-sm-6 px-2 py-2">
<Cities v-model="selectedCities" @clear-option="clearCity"/>
</div>
</div>
</div>
<Spiner class="mt-4" v-if="loading"/>
<div v-else>
<CardLoad
v-if="loads.length > 0"
v-for="load in loads"
:load="load"
:read-only="false"
@set-load="handleSetCurrentLoad(load)"
/>
<CardEmpty
v-else
text="No hay cargas publicadas"
/>
<Pagination
v-if="!isSearch"
:limit="limit"
:total="total"
:current-page="currentPage"
@get-elements="getLoadsByPage"
/>
</div>
</template>
<style scoped>
.card-filters {
display: flex;
margin: 0 auto;
background-color: #FFF;
border-radius: 13px;
/* background-color: red; */
flex-direction: column;
/* align-items: center; */
justify-content: center;
width: 100%;
padding: 20px 10%;
margin-bottom: 24px;
}
@media (max-width: 768px) {
.card-filters {
padding: 20px 16px;
}
}
</style>

View File

@@ -1,14 +1,15 @@
<script setup> <script setup>
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import Spiner from '../components/ui/Spiner.vue'; import Spiner from '../components/ui/Spiner.vue';
import useDirecty from '../composables/useDirectory'; import useDirectory from '../composables/useDirectory';
import CardCompany from '../components/cardcompany.vue'; import CardCompany from '../components/cardcompany.vue';
import TruckTypes from '../components/ui/TruckTypes.vue'; import TruckTypes from '../components/ui/TruckTypes.vue';
import Segments from '../components/ui/Segments.vue'; import Segments from '../components/ui/Segments.vue';
import States from '../components/ui/States.vue'; import States from '../components/ui/States.vue';
import Cities from '../components/ui/Cities.vue'; import Cities from '../components/ui/Cities.vue';
import Pagination from '../components/pagination.vue';
const {loading, companies, getCompaniesData} = useDirecty(); const {loading, companies, getCompaniesData, companiesTotal, currentCompaniesPage} = useDirectory();
const query = ref(''); const query = ref('');
const selectedTruckType = ref([]); const selectedTruckType = ref([]);
const selectedCategory = ref([]); const selectedCategory = ref([]);
@@ -16,11 +17,25 @@
const selectedCities = ref([]); const selectedCities = ref([]);
const filterQuery = ref([]); const filterQuery = ref([]);
const limit = 3;
onMounted(() => { onMounted(() => {
filterQuery.value.company_type = 'shipper'; filterQuery.value.company_type = 'shipper';
filterQuery.value.limit = 'elements=' + limit;
filterQuery.value.page = "page=1";
getCompaniesData(filterQuery.value); getCompaniesData(filterQuery.value);
}); });
///v1/public-companies/shipper?elements=3&skip=6&$sort%5BcreatedAt%5D=-1
const getCompaniesByPage = (data) => {
console.log(data);
// filterQuery.value.company_type = 'shipper';
filterQuery.value.page = "page="+ data.page;
currentCompaniesPage.value = data.page
getCompaniesData(filterQuery.value);
}
watch(query, () => { watch(query, () => {
if(query.value.length === 0){ if(query.value.length === 0){
filterQuery.value.search = ""; filterQuery.value.search = "";
@@ -66,7 +81,7 @@
} }
const clearFilter = () => { const clearFilter = () => {
clearRequest();
selectedCities.value = null; selectedCities.value = null;
selectedCategory.value = null; selectedCategory.value = null;
selectedState.value = null; selectedState.value = null;
@@ -86,6 +101,12 @@
} }
} }
const clearRequest = () => {
filterQuery.value.page = "page="+ 1;
filterQuery.value.limit = "elements="+ limit;
currentPage.value = 1;
}
const clearState = () => { const clearState = () => {
filterQuery.value.state = ""; filterQuery.value.state = "";
getCompaniesData(filterQuery.value); getCompaniesData(filterQuery.value);
@@ -137,20 +158,29 @@
</div> </div>
</div> </div>
<Spiner v-if="loading"/> <Spiner v-if="loading"/>
<div <div v-else>
class="card-info flex-column align-items-center" <div
v-if="loading === false && companies.length <= 0" class="card-info flex-column align-items-center"
> v-if="companies.length <= 0"
<img src="/images/logo.png" alt="logo" width="100"> >
<h2 class="title">No hay registros</h2> <img src="/images/logo.png" alt="logo" width="100">
<h2 class="title">No hay registros</h2>
</div>
<CardCompany
v-else
:key="company._id"
v-for="company in companies"
:company="company"
/>
<Pagination
v-if="!loading"
:total="companiesTotal"
:limit="limit"
:current-page="currentCompaniesPage"
@get-elements="getCompaniesByPage"
/>
</div> </div>
<CardCompany
v-else
:key="company._id"
v-for="company in companies"
:company="company"
/>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,101 @@
<template>
<div class="container my-5">
<h1 class="text-center">TÉRMINOS Y CONDICIONES</h1>
<p class="mt-5">Eta víaporte proporciona soluciones a la industria del transporte que hacen que los negocios sean más rápidos, eficientes y rentables. Conectamos e interactuamos a través de nuestros tableros de carga impulsados por contratistas de transporte y líneas fleteras por medio de foros de discusión y mensajería instantánea.</p>
<p>Somos una comunidad de transportistas y contratistas en la industria de carga de camiones a granel, líquidos y secos. Eta víaporte es impulsado por los miembros de su comunidad para crear soluciones de conectar a contratistas de transporte con líneas fleteras creando una red de servicio de correspondencia de cargas en línea que permita a los usuarios unirlos y hacerlos coincidir con mensajes instantáneos por medio de un tablero en tiempo real que publique y busque cargas simultáneamente.</p>
<h3>CONECTAMOS CONTRATISTAS DE TRANSPORTE CON LÍNEAS FLETERAS</h3>
<p>Eta víaporte es una comunidad en línea de transportistas. Nuestros remitentes son gerentes de logística de transporte de granos, fertilizantes, agregados, líquidos, secos y todos los productos agrícolas. Eta víaporte reúne a los dos grupos, haciéndolos coincidir con mensajes instantáneos por medio de un tablero en tiempo real que publique y busque cargas simultáneamente.</p>
<h3>Términos de servicio de Eta víaporte</h3>
<p>Bienvenido al sitio web Eta víaporte ("Sitio") proporcionado por Eta víaporte. El Sitio y todos los servicios, módulos, funciones, software o plataformas relacionados (en conjunto, "Servicios") se crearon para brindarle una manera mucho más fácil de encontrar cargas y mover más carga. Es Términos de servicio de Eta víaporte ("") constituyen un conjunto de reglas por las cuales operamos dichos Servicios.</p>
<h4>ACEPTACIÓN DE LOS TÉRMINOS</h4>
<p>Eta víaporte le brinda su servicio, sujeto a los siguientes Términos de servicio (""). Su uso del sitio web, aplicaciones móviles Y otros servicios constituye su acuerdo de estar sujeto a todos los términos, condiciones y avisos aquí contenidos. Nos reservamos el derecho, a nuestro exclusivo criterio y con o sin aviso, de cambiar, modificar, agregar o eliminar cualquier parte o en su totalidad en cualquier momento. SI NO ACEPTA ESTAR TOTALMENTE OBLIGADO POR TODOS ES TÉRMINOS, NO ACCEDA AL SITIO Y NO USE LOS SERVICIOS. Debe leer detenidamente todos los Términos, ya que constituyen un acuerdo legalmente vinculante entre usted y nosotros.</p>
<h4>DESCRIPCIÓN DEL SERVICIO</h4>
<p>Eta víaporte ofrece actualmente a los usuarios de México un servicio de correspondencia de carga en línea que permite a los usuarios publicar, buscar cargas y camiones disponibles y utilizar cualquier otro servicio gratuito integrado en nuestro sitio web (el "Servicio"). También comprende y acepta que el servicio puede incluir ciertas comunicaciones de Eta víaporte, como anuncios de servicio, mensajes administrativos, y que estas comunicaciones se consideran parte de la membresía de Eta víaporte y no podrá optar por no recibirlas. A menos que se indique explícitamente lo contrario, cualquier característica nueva que aumente o mejore el Servicio actual, incluido el lanzamiento de nuevas propiedades de Eta víaporte, estará sujeta a los términos. Usted comprende y acepta que el Servicio se proporciona "TAL CUAL" y que Eta víaporte no asume ninguna responsabilidad por la puntualidad, eliminación, entrega incorrecta o falta de almacenamiento de las comunicaciones del usuario o la configuración de personalización. Usted es responsable de obtener acceso al Servicio y ese acceso puede implicar tarifas de terceros (como el proveedor de servicios de Internet o cargos por tiempo aire). Además, debe proporcionar y ser responsable de todo el equipo necesario para acceder al Servicio.</p>
<h4>APLICACIÓN MÓVIL (APLICACIÓN)</h4>
<p>El uso de nuestra aplicación móvil para iOS, Android u otros dispositivos mientras conduce un camión o vehículo motorizado puede causarle lesiones graves, la muerte o daños a la propiedad a usted u otras personas. NO use nuestra aplicación móvil a menos que su vehículo esté parado y estacionado. USTED ASUME TODOS LOS RIESGOS Y RESPONSABILIDADES.</p>
<h4>REQUISITOS DEL SUSCRIPTOR</h4>
<ol>
<li>El suscriptor es o representa un propietario-operador de buena fe, transportista, remitente, agente de cargas o consultor de la industria de camiones;</li>
<li>El acceso del suscriptor a Eta víaporte es solo para fines comerciales y se limita a encontrar cargas y / o camiones mediante el uso de nuestro servicio de igualación de carga y / o aprovechar los otros servicios ofrecidos en Eta víaporte;</li>
<li>El propósito comercial del suscriptor al acceder y utilizar Eta víaporte no es competir directa o indirectamente con Eta víaporte ni ganar ventaja competitiva en relación con él.</li>
<li>El suscriptor no permitirá que los usuarios no registrados utilicen la contraseña y / o el nombre de usuario del suscriptor para acceder a la cuenta del suscriptor sin el consentimiento expreso por escrito de Eta víaporte;</li>
<li>Al enviar contenido al sitio web de Eta víaporte, el suscriptor garantiza y declara que la información es precisa, que el suscriptor está autorizado a enviar la información y que el contenido de la información, el formato y el método de entrega son apropiados.</li>
</ol>
<p>Si el usuario viola cualquier disposición para la calificación del suscriptor, Eta víaporte puede cancelar la cuenta del suscriptor sin previo aviso o advertencia, además de estar sujeto a todos y cada uno de los otros recursos legales que puedan estar disponibles, incluidos, entre otros, civiles y / o acciones criminales bajo la ley estatal / provincial y / o federal. En caso de que Eta víaporte se vea obligado a entablar una acción legal contra el suscriptor para hacer cumplir por cualquier otra violación de la ley estatal y / o federal no enunciada específicamente en este documento, el suscriptor acepta expresamente asumir todos y cada uno de los asociados con dicha acción, incluidos, entre otros, limitado a los honorarios de abogados reales incurridos por Eta víaporte</p>
<h4>KILOMETRAJE</h4>
<p>Como una opción de beneficio adicional para los miembros, Eta víaporte muestra el kilometraje de las cargas publicadas. Su única finalidad es dar a los usuarios una estimación de la distancia entre los puntos de inicio y destino de la carga. Eta víaporte no hace ningún reclamo sobre la precisión del kilometraje y de ninguna manera el remitente / transportista / camionero está vinculado a él. Elremitente / transportista / camionero tiene la última palabra en cuanto al kilometraje que pagará.</p>
<h4>NO REVENTA O USO EXTERNO DEL SERVICIO O DATOS</h4>
<p>Usted acepta no reproducir, duplicar, raspar, copiar, vender, comercializar, revender o explotar con fines comerciales, cualquier parte del Servicio, uso del Servicio o acceso al Servicio.</p>
<h4>MODIFICACIONES AL SERVICIO</h4>
<p>Eta víaporte se reserva el derecho, en cualquier momento y de vez en cuando, de modificar o interrumpir, temporal o permanentemente, el Servicio (o cualquier parte de este) con o sin previo aviso. También podemos imponer límites o restricciones a ciertos servicios, características o contenido o restringir su acceso a partes o todo nuestro Sitio sin previo aviso. Usted acepta que Eta víaporte no será responsable ante usted ni ante ningún tercero por ninguna modificación, suspensión o interrupción del Servicio.</p>
<h4>PROCEDIMIENTO DE TRATAMIENTO DE QUEJAS</h4>
<p>Si un problema se atribuye a un servicio proporcionado por Eta víaporte, nuestro objetivo es resolver el problema de manera rápida, equitativa y amigable. Todas las quejas deben enviarse por correo electrónico. Incluya su nombre, información de suscripción y una descripción completa de su queja. Nos esforzamos por responder a todas las consultas dentro de las 24 horas.</p>
<h4>TERMINACIÓN</h4>
<p>Usted acepta que Eta víaporte puede, bajo ciertas circunstancias y sin previo aviso, cancelar inmediatamente su cuenta Eta víaporte y el acceso al Servicio. La causa de dicha terminación incluirá, pero no se limitará a:</p>
<ol>
<li>Incumplimiento o violaciones de los otros acuerdos o pautas incorporados;</li>
<li>Solicitudes de las fuerzas del orden u otras agencias gubernamentales;</li>
<li>Una solicitud suya (eliminación de cuenta por iniciativa propia);</li>
<li>Interrupción o modificación material del Servicio (o cualquier parte de este);</li>
<li>Problemas o problemas técnicos o de seguridad inesperados; y</li>
<li>Períodos prolongados de inactividad.</li>
</ol>
<p>La cancelación de su cuenta Eta víaporte incluye:</p>
<ol>
<li>Eliminación del acceso a todas las ofertas dentro del Servicio;</li>
<li>Eliminación de su contraseña y toda la información, archivos y contenido relacionados con o dentro de su cuenta (o cualquier parte de esta); y</li>
<li>Salvo un uso posterior del Servicio. Además, acepta que todas las cancelaciones por causa justificada se realizarán a la entera discreción de Eta víaporte y que Eta víaporte no será responsable ante usted ni ante ningún tercero por la cancelación de su cuenta o el acceso al Servicio.</li>
</ol>
<h4>ENVÍO DE CONTENIDO</h4>
<p>Para mantener un Servicio de Emparejamiento de Carga en Línea líder, debemos protegernos contra aquellos que puedan intentar aprovecharse de otros o que simplemente no sigan las reglas. Si envía contenido a nuestro sitio, garantiza y declara que la información es precisa, que está autorizado a enviar la información y que el contenido, el formato y el método de entrega de la información son los adecuados. Para proteger a cada usuario, nos reservamos el derecho absoluto de revisar, rechazar o modificar la información enviada a nuestro exclusivo e independiente criterio. Al transferir información, usted acepta que Eta víaporte, sus afiliados y cesionarios tienen licencia para usar, reproducir, mostrar, ejecutar, adaptar, modificar, distribuir y promover la información de una manera que consideremos razonable a nuestro exclusivo e independiente juicio.</p>
<h4>TECNOLOGÍA Y COMUNICACIONES ELECTRÓNICAS</h4>
<p>El contenido y el software utilizados en este sitio web y el diseño y diseño del sitio web son propiedad exclusiva de Eta víaporte y están protegidos por derechos de autor, marcas comerciales, marcas de servicio, patentes y otros derechos y leyes de propiedad. Los usuarios no pueden copiar ni recuperar da u otro contenido de este sitio web, ya sea manualmente o mediante el uso de dispositivos automáticos, con el fin de crear, directa o indirectamente, una colección, base de dato o directorio sin el permiso expreso por escrito de Eta víaporte. Los suscriptores no pueden usar etiquetas u otro texto oculto que utilice el nombre o las marcas comerciales de Eta víaporte, ni los suscriptores pueden usar técnicas de enmarcado para incluir cualquier parte del sitio web, sin un permiso expreso por escrito. A menos que Eta víaporte lo autorice expresamente por escrito, el suscriptor no puede reproducir, modificar, distribuir, transmitir, volver a publicar, mostrar, alquilar, vender, licenciar, editar o crear trabajos derivados de cualquier contenido u otro material de este sitio web.</p>
<p>Usted reconoce que Eta víaporte es un negocio basado en Internet y que nuestro Servicio está disponible a través de Internet. Para mantener una cuenta con nosotros, deberá poder acceder a nuestro sitio web en Internet, lo que requiere un equipo informático adecuado, acceso a Internet, un navegador del sitio web y una cuenta de correo electrónico. Usted reconoce y comprende que puede incurrir en ciertos operativos en relación con su uso de Internet, como tarifas mensuales para un proveedor de servicios, cargos por tiempo de uso, etc., de los cuales usted es el único responsable.</p>
<h4>INTERRUPCIÓN Y MODIFICACIÓN DEL SERVICIO</h4>
<p>Eta víaporte siempre se esfuerza por lograr un tiempo de actividad continuo del servicio, sin embargo, se reserva el derecho de modificar o descontinuar el servicio de la placa de carga con o sin previo aviso. Eta víaporte no será responsable ante ningún miembro o tercero si Eta víaporte ejerce su derecho de modificar o descontinuar el servicio de la placa de carga.</p>
<p>Todos los miembros reconocen y aceptan que Eta víaporte no garantiza el acceso continuo, ininterrumpido o seguro al servicio del tablero de carga.</p>
<h4>DERECHOS DE PROPIEDAD</h4>
<p>Usted reconoce y acepta que el Servicio y cualquier software necesario utilizado en relación con el Servicio ("Software") contienen información confidencial y de propiedad que está protegida por la propiedad intelectual aplicable y otras leyes. Además, reconoce y acepta que el contenido de los anuncios de los patrocinadores o la información que se le presenta a través del Servicio o de los anunciantes está protegido por derechos de autor, marcas comerciales, marcas de servicio, patentes u otros derechos y leyes de propiedad. Salvo que Eta víaporte o los anunciantes lo autoricen expresamente, usted acepta no modificar, alquilar, arrendar, prestar, vender, distribuir o crear trabajos derivados basados en el Servicio o el Software, en su totalidad o en parte.</p>
<h4>RENUNCIA DE GARANTÍAS</h4>
<p>Eta víaporte mantiene un servicio de igualación de carga en línea para que lo utilicen los transportistas y los remitentes con el fin de comunicarse y hacer negocios entre ellos. Eta víaporte no maneja el efectivo, no controla a los usuarios de ninguna manera ni organiza el movimiento de la carga. Todos los arreglos son realizados por los usuarios del sitio web, y los términos y condiciones de dicho movimiento de carga son únicamente entre el transportista y el remitente. No se pretende ni se crea ninguna relación de agencia, sociedad, empresa conjunta o empleado-empleador por su uso de este sitio web. Si hay una disputa por parte de un usuario o entre usuarios, queremos saberlo para asegurarnos de que todos estén haciendo lo que deberían hacer en nuestro sitio web. Tenga en cuenta que somos simplemente un intermediario para el negocio de camiones y, para protegernos, acepta que no nos involucrará en ninguna disputa que pueda surgir entre los usuarios. Para hacerlo, acepta liberar Eta víaporte, sus funcionarios, empleados y agentes de todos los reclamos, demandas y daños, reales y consecuentes, de todo tipo y naturaleza, conocidos o desconocidos, de cualquier manera, relacionada con dichas disputas.</p>
<h3>EL USO DE LA INFORMACIÓN Y LOS DATOS CONTENIDOS EN ESTE SITIO WEB Y LAS APLICACIONES MÓVILES (APLICACIÓN) ES BAJO SU PROPIO RIESGO</h3>
<p>Eta víaporte no ofrece ninguna garantía o garantía aplicable. Si bien Eta víaporte hace esfuerzos razonables para publicar información precisa y oportuna, no hacemos declaraciones ni garantías de la precisión del contenido en el Sitio y la aplicación móvil y no asumimos responsabilidad alguna por inexactitudes, errores u omisiones en dicho contenido. La información y los datos de este sitio se proporcionan "tal cual" y "según esté disponible" sin garantía o condición de ningún tipo o naturaleza, ya sea expresa o implícita. Eta víaporte renuncia específicamente a las garantías implícitas de título, idoneidad para un propósito particular, comerciabilidad y no infracción. En ningún caso Eta víaporte será responsable por la pérdida de beneficios o cualquier daño especial, incidental o consecuente (incluidos, entre otros, daños indirectos, especiales, punitivos o ejemplares por pérdida de negocio, lucro cesante, interrupción del negocio o pérdida de información o datos comerciales) que surjan, incluida la negligencia, que surjan de o en conexión con este acuerdo y / o el uso del suscriptor del sitio web. Eta víaporte no acepta responsabilidad u obligación por cualquier uso de los datos y / o información o la confianza depositada en dichos datos y / o información. Eta víaporte no garantiza que dichos datos y / o información estén libres de infección por virus informáticos u otra contaminación. Eta víaporte no respalda ni garantiza en ningún aspecto ningún producto o servicio de terceros en virtud de cualquier información, material o contenido referido o incluido en, o vinculado desde o hacia este sitio web. La responsabilidad total de Eta víaporte con respecto a sus obligaciones en virtud de este acuerdo o de otro modo con respecto al servicio o de otro modo no excederá el monto de la tarifa pagada por usted por los servicios en el mes en que ocurrió la obligación. Algunos estados no permiten la limitación de responsabilidad, por lo que es posible que la limitación anterior no se aplique en su caso. Todos los usuarios del sitio web acuerdan indemnizar y mantener a Eta víaporte, y sus subsidiarias, afiliadas, funcionarios, agentes, marcas conjuntas u otros socios y empleados, indemnes de cualquier reclamo o demanda, incluidos los honorarios razonables de abogados, realizados por terceros. Debido a o que surja del Contenido que envíe, publique, transmita o ponga a disposición a través del Servicio, su uso del Servicio, su conexión con el Servicio, su violación de los o su violación de cualquier derecho de otra persona.</p>
<h4>SIN TERCEROS BENEFICIARIOS</h4>
<p>Usted acepta que, salvo que se disponga expresamente lo contrario en este, no habrá terceros beneficiarios de este Acuerdo.</p>
<h4>INFORMACIÓN GENERAL</h4>
<p>Los constituyen el acuerdo completo entre usted y Eta víaporte y rigen su uso del Servicio, reemplazando cualquier acuerdo anterior (oral, escrito o electrónico) entre usted y Eta víaporte. También puede estar sujeto a términos y condiciones adicionales que pueden aplicarse cuando usa o compra otros servicios de Eta víaporte, servicios afiliados, contenido de terceros o software de terceros. El hecho de que Eta víaporte no ejerza o haga cumplir cualquier derecho o disposición no constituirá una renuncia a dicho derecho o disposición. Si un tribunal de jurisdicción competente determina que alguna disposición de los derechos es inválida, las partes acuerdan, no obstante, que el tribunal debe esforzarse por dar efecto a las intenciones de las partes tal como se refleja en la disposición, Usted reconoce que, al registrarse con nosotros, enviaremos información para su publicación o cualquier otro propósito o usar el sitio web y nuestro Servicio, no se crea ninguna relación fiduciaria, confidencial, implícita contractual o de otro tipo entre usted y nosotros que no sea la relación contractual expresa establecida. Usted acepta que, independientemente de cualquier estatuto o ley en contrario, cualquier reclamo o causa de acción que surja de o esté relacionado con el uso del servicio debe presentarse dentro de un (1) año después de que surgió dicho reclamo o causa de acción o será prohibido para siempre. Los títulos de las secciones son solo para conveniencia y no tienen ningún efecto legal o contractual.</p>
<h4>ASIGNACIÓN</h4>
<p>Nos reservamos el derecho de ceder nuestros derechos y obligaciones en virtud de los términos de servicio a una o más de nuestros afiliados cualquier entidad sucesora mediante fusión, consolidación o de otro modo. No tiene derecho a ceder el registro de su cuenta ni ninguno de sus derechos o responsabilidades en virtud de es TDS sin nuestro consentimiento expreso por escrito. Eso redundará en beneficio de nuestros sucesores y cesionarios, serán vinculantes y exigibles para ellos.</p>
<h4>AVISOS</h4>
<p>Eta víaporte puede proporcionarle avisos, incluidos los relacionados con cambios en los términos de servicio, por correo electrónico, correo postal o publicaciones en el Servicio.</p>
<h4>CAMBIOS EN LOS TÉRMINOS</h4>
<p>Eta víaporte puede actualizar los Términos de servicio para reflejar los cambios en nuestros servicios y los comentarios de los clientes. Si Eta víaporte realiza cambios en los términos o condiciones de los Términos de servicio, los cambios se publicarán en nuestros sitios web de manera oportuna.</p>
<h4>VIOLACIONES</h4>
<p>Por favor, infórmenos de cualquier infracción de los términos de servicio por correo electrónico.</p>
</div>
</template>
<style scoped>
p{
font-size: 1.1rem;
}
h3 {
font-size: 1.4rem;
text-align: center;
margin: 1rem 0px;
}
h4 {
font-size: 1.2rem;
text-align: left;
margin: 1rem 0px;
font-weight: normal;
}
ol > li {
font-size: 1.1rem;
margin: 0.5rem 0px;
}
</style>

View File

@@ -0,0 +1,130 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useLoadsStore } from '../stores/loads';
import { useRoute } from 'vue-router';
import Spiner from '../components/ui/Spiner.vue';
import CardLoad from '../components/CardLoad.vue';
import useDirectionsRender from '../composables/useDirectionRender';
import { GoogleMap, Marker, CustomMarker } from 'vue3-google-map';
import CardEmpty from '../components/CardEmpty.vue';
const isLoading = ref(true);
const loadStore = useLoadsStore();
const { geocodeAddress } = useDirectionsRender()
const route = useRoute();
const load = ref(null);
const zoom = ref(6);
const heightMap = ref(768);
const vehicleLastLocation = ref(null);
const originCoords = ref(null);
const destinationCoords = ref(null);
const isLoadActive = ref(false);
const windowWidth = ref(window.innerWidth);
onMounted(() => {
window.addEventListener('resize', handleResize);
if(window.innerWidth <= 1024) {
zoom.value = 4;
heightMap.value = 420;
}
initData();
});
const initData = async() => {
isLoading.value = true;
const filter = "?shipment_code[$in]=" + route.params['code'];
const resp = await loadStore.getLoad(filter);
if(resp.total > 0) {
load.value = resp.data[0];
originCoords.value = await geocodeAddress(load.value.origin_formatted_address);
destinationCoords.value = await geocodeAddress(load.value.destination_formatted_address);
if(load.value.vehicle) {
vehicleLastLocation.value = {
lat: parseFloat(load.value.vehicle.last_location_lat),
lng: parseFloat(load.value.vehicle.last_location_lng)
}
}
switch (load.value.load_status) {
case 'Loading':
isLoadActive.value = true;
break;
case 'Transit':
isLoadActive.value = true;
break;
case 'Downloading':
isLoadActive.value = true;
break;
default:
isLoadActive.value = false;
break;
}
}
isLoading.value = false;
}
const handleResize = () => {
windowWidth.value = window.innerWidth
if(windowWidth.value <= 1024){
zoom.value = 4
heightMap.value = 420;
} else {
zoom.value = 6;
heightMap.value = 768;
}
}
</script>
<template>
<h2 class="title text-center">Seguimiento de carga</h2>
<Spiner v-if="isLoading"/>
<div v-else>
<div v-if="load">
<CardLoad :load="load" :read-only="true"/>
<GoogleMap
api-key="AIzaSyAJtfvrAKy7vnUSv2nzk4dYQkOs3OP4MMs"
:center="{lat:19.432600, lng:-99.133209}"
:zoom="zoom"
:min-zoom="2"
:max-zoom="12"
:style="{width: 100 + '%', height: heightMap + 'px'}"
:options="{
zoomControl: true,
mapTypeControl: true,
scaleControl: true,
streetViewControl: true,
rotateControl: true,
fullscreenControl: true
}"
>
<Marker v-if="originCoords" :options="{position: originCoords, label: 'O', title: 'Destino'}" />
<Marker v-if="destinationCoords" :options="{position: destinationCoords, label: 'D', title: 'Origen'}" />
<CustomMarker
v-if="vehicleLastLocation && load.vehicle.background_tracking && isLoadActive"
:options="{position: vehicleLastLocation}"
:clickable="false"
:draggable="false"
>
<div style="text-align: center">
<!-- <img src="/images/freeTruck.png" width="25" height="25" /> -->
<i class="fa-solid fa-truck" :style="{fontSize: 25 + 'px', color: 'green'}"></i>
</div>
</CustomMarker>
<!-- <Polyline :options="{
path: polyline,
// geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 2
}" /> -->
</GoogleMap>
</div>
<CardEmpty v-else text="No hay información disponible"/>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,88 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useCompanyStore } from '../stores/company';
import Spiner from '../components/ui/Spiner.vue';
import CardProposal from '../components/CardProposal.vue';
import LoadDetailModal from '../components/LoadDetailModal.vue';
import MakeProposalModal from '../components/MakeProposalModal.vue';
import Pagination from '../components/pagination.vue';
const companyStore = useCompanyStore();
const loading = ref(false);
const proposalCurrent = ref(null);
const openModal = ref(false);
const openModalProposal = ref(false);
const limit = 3;
onMounted(() =>{
getInitData();
})
const getInitData = async() => {
loading.value = true;
const filter = '$limit=' + limit + "&$skip=0";
await companyStore.getProposalsCompany(filter, false);
loading.value = false;
}
const getProposalsByPage = async(data) => {
console.log(data)
loading.value = true;
const filter = '$limit=' + limit + "&$skip=" + data.skip;
companyStore.proposalsCurrentPage = data.page;
await companyStore.getProposalsCompany(filter, true);
loading.value = false;
}
const handleSetCurrentProposal = (data) => {
if(data.modal === 'detail') {
openModal.value = true;
} else {
openModalProposal.value = true;
}
proposalCurrent.value = data.proposal;
}
const handleResetCurrentProposal = () => {
openModal.value = false;
openModalProposal.value = false;
proposalCurrent.value = null;
console.log('clear proposal');
}
</script>
<template>
<LoadDetailModal
v-if="openModal"
:proposal="proposalCurrent"
@reset-proposal="handleResetCurrentProposal"
/>
<MakeProposalModal
v-if="openModalProposal"
:proposal="proposalCurrent"
:load="proposalCurrent.load"
@reset-load="handleResetCurrentProposal"
/>
<div>
<h2 class="title mb-5">Mis ofertas aceptadas</h2>
<Spiner v-if="loading"/>
<CardProposal
v-else
v-for="proposal in companyStore.proposals"
:key="proposal._id"
:proposal="proposal"
@set-proposal="handleSetCurrentProposal"
/>
<Pagination
v-if="!loading"
:limit="limit"
:current-page="companyStore.proposalsCurrentPage"
:total="companyStore.proposalsTotal"
@get-elements="getProposalsByPage"
/>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,10 +1,83 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue';
import Spiner from '../components/ui/Spiner.vue';
import CardUser from '../components/CardUser.vue';
import { useCompanyStore } from '../stores/company';
import CreateUserModal from '../components/CreateUserModal.vue';
import Pagination from '../components/Pagination.vue';
const companyStore = useCompanyStore();
onMounted(() => {
getInitData();
});
const currentUser = ref(null);
const openModal = ref(false);
const limit = 3;
const getInitData = async() => {
console.log('callll')
loading.value = true;
await companyStore.getUsersCompany(limit);
loading.value = false;
}
const getUsersByPage = async(data) => {
console.log(data)
loading.value = true;
companyStore.usersCurrentPage = data.page
await companyStore.getUsersCompany(limit, data.skip, true);
loading.value = false;
}
const loading = ref(false);
const handleSetCurrentUser = (user) => {
openModal.value = true;
currentUser.value = user;
}
const handleResetCurrentUser = () => {
openModal.value = false;
currentUser.value = null;
}
</script> </script>
<template> <template>
<CreateUserModal
v-if="openModal === true"
:user="currentUser"
@reset-user="handleResetCurrentUser"
/>
<div> <div>
<h2 class="title">Usuarios</h2> <h2 class="title mb-4">Usuarios</h2>
<div class="btn-row mb-4">
<button
class="btn-primary-sm"
data-toggle="modal"
data-target="#userModal"
@click="handleSetCurrentUser(null)"
>
<i class="fa-solid fa-plus"></i> Agregar usuario
</button>
</div>
<Spiner v-if="loading"/>
<div v-else>
<CardUser
v-for="user in companyStore.users"
:user="user"
:readonly="false"
@set-user="handleSetCurrentUser(user)"
/>
<Pagination
:limit="limit"
:total="companyStore.usersTotal"
:current-page="companyStore.usersCurrentPage"
@get-elements="getUsersByPage"
/>
</div>
</div> </div>
</template> </template>

View File

@@ -1,13 +1,202 @@
<script setup> <script setup>
import { onMounted, ref, watch } from 'vue';
import { useCompanyStore } from '../stores/company';
import Spiner from '../components/ui/Spiner.vue';
import { useVehiclesStore } from '../stores/vehicles';
import CardVehicle from '../components/CardVehicle.vue';
import CardEmpty from '../components/CardEmpty.vue';
import CreateVehicleModal from '../components/CreateVehicleModal.vue';
import StatusVehicleModal from '../components/StatusVehicleModal.vue';
import DriverVehicleModal from '../components/DriverVehicleModal.vue';
import Pagination from '../components/pagination.vue';
const companyStore = useCompanyStore();
const vehicleStore = useVehiclesStore();
const loading = ref(false);
const filterQuery = ref([]);
const query = ref('');
const vehicleCurrent = ref(null);
const openModal = ref(false);
const editStatusVehicle = ref(false);
const editDriverVehicle = ref(false);
const limit = 2;
onMounted(() => {
getInitData();
})
const getInitData = async() => {
loading.value = true;
filterQuery.value.limit = '$limit=' + limit;
filterQuery.value.skip = "$skip=0"
filterQuery.value.company = "company="+ localStorage.getItem('id');
await vehicleStore.fetchVehicles(filterQuery.value, false);
await companyStore.getDrivers();
loading.value = false;
}
const getVehiclesWithFilters = async(filter) => {
loading.value = true;
await vehicleStore.fetchVehicles(filter, true);
loading.value = false;
}
watch(query, () => {
filterQuery.value.skip = "$skip="+ 0;
filterQuery.value.limit = "$limit="+ 100;
if(query.value.length === 0){
clearRequest()
filterQuery.value.search = "";
getVehiclesWithFilters(filterQuery.value);
}
});
const search = () => {
if(query.value.length >= 2){
// filterQuery.value = "company_name[$regex]=" + query.value + "&company_name[$options]=i";
filterQuery.value.search = "truck_type[$regex]=" + query.value +"&truck_type[$options]=i";
getVehiclesWithFilters(filterQuery.value);
}
}
const clearFilter = () => {
clearRequest()
filterQuery.value.search = "";
filterQuery.value.company = "company="+ localStorage.getItem('id');
if(query.value == ''){
getInitData();
} else {
query.value = '';
}
}
const clearRequest = () => {
filterQuery.value.skip = "$skip="+ 0;
filterQuery.value.limit = "$limit="+ limit;
vehicleStore.vehiclesCurrentPage = 1;
}
const handleSetCurrentVehicle = (data) => {
console.log(data);
if(data.modal === 'form') {
openModal.value = true;
} else if (data.modal === 'driver') {
editDriverVehicle.value = true;
} else {
editStatusVehicle.value = true;
}
vehicleCurrent.value = data.vehicle;
}
const handleResetCurrentVehicle = () => {
openModal.value = false;
editDriverVehicle.value = false;
editStatusVehicle.value = false;
vehicleCurrent.value = null;
}
const getVehiclesByPage = async(data) => {
loading.value = true;
filterQuery.value.company = "company="+ localStorage.getItem('id');
filterQuery.value.skip = "$skip="+ data.skip;
vehicleStore.vehiclesCurrentPage = data.page
await vehicleStore.fetchVehicles(filterQuery.value, true);
loading.value = false;
}
</script> </script>
<template> <template>
<div> <div>
<CreateVehicleModal
v-if="openModal === true"
:vehicle="vehicleCurrent"
@reset-vehicle="handleResetCurrentVehicle"
/>
<StatusVehicleModal
v-if="editStatusVehicle === true"
:vehicle="vehicleCurrent"
@reset-vehicle="handleResetCurrentVehicle"
/>
<DriverVehicleModal
v-if="editDriverVehicle === true"
:vehicle="vehicleCurrent"
@reset-vehicle="handleResetCurrentVehicle"
/>
<h2 class="title">Vehiculos</h2> <h2 class="title">Vehiculos</h2>
<div class="box-filters">
<div class="box-search">
<input class="form-control custom-search" type="search" name="" placeholder="Buscar vehicles" id="" @:input="search()" v-model="query" aria-label="Search">
</div>
<button
class="btn btn-danger bg-dark" type="button" @click="clearFilter">
<i class="fa-solid fa-arrow-rotate-right"></i>
<span class="clear-sm"> Reset</span><span class="clear-md"> filtros</span>
</button>
<button
class="btn-primary-sm radius-sm"
data-toggle="modal"
data-target="#createVehicleModal"
@click="handleSetCurrentVehicle({vehicle: null, modal: 'form'})"
><i class="fa-solid fa-plus"></i> <span class="clear-sm"> Agregar</span><span class="clear-md"> vehiculo</span></button>
</div>
<Spiner v-if="loading"/>
<div v-else>
<CardVehicle
v-if="vehicleStore.vehicles.length > 0"
v-for="vehicle in vehicleStore.vehicles"
:vehicle="vehicle"
:key="vehicle._id"
@set-vehicle="handleSetCurrentVehicle"
/>
<CardEmpty v-else text="No hay vehiculos agregados"/>
<Pagination
:current-page="vehicleStore.vehiclesCurrentPage"
:total="vehicleStore.vehiclesTotal"
:limit="limit"
@get-elements="getVehiclesByPage"
/>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.box-filters {
display: flex;
flex-direction: row;
justify-content: end;
gap: 1rem;
margin: 1.5rem 0px;
}
.box-search {
width: 60%;
}
.custom-search {
width: 100%;
padding: 12px 20px;
}
@media (max-width: 1024px) {
.box-search {
width: 60%;
}
.box-filters {
gap: .4rem;
}
}
@media (max-width: 768px) {
.box-search {
width: 100%;
}
.box-filters {
gap: .3rem;
}
}
</style> </style>