diff --git a/apps/ops/.quasar/client-entry.js b/apps/ops/.quasar/client-entry.js
index 8c0244d..a9a7b74 100644
--- a/apps/ops/.quasar/client-entry.js
+++ b/apps/ops/.quasar/client-entry.js
@@ -150,7 +150,11 @@ createQuasarApp(createApp, quasarUserOptions)
return Promise[ method ]([
- import('boot/pinia')
+ import('boot/net-timeout'),
+
+ import('boot/pinia'),
+
+ import('boot/tooltip-ux')
]).then(bootFiles => {
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
diff --git a/apps/ops/.quasar/quasar-user-options.js b/apps/ops/.quasar/quasar-user-options.js
index 8eb88f9..69ca7cb 100644
--- a/apps/ops/.quasar/quasar-user-options.js
+++ b/apps/ops/.quasar/quasar-user-options.js
@@ -17,5 +17,5 @@ import {Notify,Loading,LocalStorage,Dialog} from 'quasar'
-export default { config: {},plugins: {Notify,Loading,LocalStorage,Dialog} }
+export default { config: {"brand":{"primary":"#16a34a","positive":"#16a34a","negative":"#ef4444","warning":"#f59e0b","info":"#2563eb"}},plugins: {Notify,Loading,LocalStorage,Dialog} }
diff --git a/apps/ops/knip.json b/apps/ops/knip.json
new file mode 100644
index 0000000..1d5afba
--- /dev/null
+++ b/apps/ops/knip.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://unpkg.com/knip@latest/schema.json",
+ "paths": { "src/*": ["./src/*"] },
+ "entry": [
+ "quasar.config.js",
+ "src/App.vue",
+ "src/router/index.js",
+ "src/boot/*.js",
+ "src/modules/**/routes.js"
+ ],
+ "project": ["src/**/*.{js,vue}"],
+ "ignore": [".quasar/**", "src-pwa/**", "dist/**"],
+ "ignoreDependencies": []
+}
diff --git a/apps/ops/package-lock.json b/apps/ops/package-lock.json
index 4b0a620..f78eaaf 100644
--- a/apps/ops/package-lock.json
+++ b/apps/ops/package-lock.json
@@ -10,11 +10,15 @@
"dependencies": {
"@quasar/extras": "^1.16.12",
"@twilio/voice-sdk": "^2.18.1",
+ "@vue-flow/background": "^1.3.2",
+ "@vue-flow/controls": "^1.1.3",
+ "@vue-flow/core": "^1.48.2",
"chart.js": "^4.5.1",
"cytoscape": "^3.33.2",
"grapesjs": "^0.22.16",
"grapesjs-mjml": "^1.0.8",
"grapesjs-preset-newsletter": "^1.0.2",
+ "hy-vue-gantt": "^5.2.1",
"idb-keyval": "^6.2.1",
"lucide-vue-next": "^1.0.0",
"pinia": "^2.1.7",
@@ -1611,7 +1615,6 @@
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1777,6 +1780,79 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz",
+ "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz",
+ "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-brands-svg-icons": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.2.0.tgz",
+ "integrity": "sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "peer": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-regular-svg-icons": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.2.0.tgz",
+ "integrity": "sha512-iycmlN51EULlQ4D/UU9WZnHiN0CvjJ2TuuCrAh+1MVdzD+4ViKYH2deNAll4XAAYlZa8WAefHR5taSK8hYmSMw==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "peer": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-solid-svg-icons": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz",
+ "integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "peer": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/vue-fontawesome": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.2.0.tgz",
+ "integrity": "sha512-7BwGjTZn8QDvVEIu8fvkHhsDRRv//tq7jtsldaDhF3dE1fyWLIQcEg3zvIzy33su7kcppWsZZ6XRYP5wp3UCgQ==",
+ "license": "MIT",
+ "peer": true,
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7",
+ "vue": ">= 3.0.0 < 4"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -2643,6 +2719,12 @@
"sourcemap-codec": "^1.4.8"
}
},
+ "node_modules/@tsconfig/node22": {
+ "version": "22.0.5",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.5.tgz",
+ "integrity": "sha512-hLf2ld+sYN/BtOJjHUWOk568dvjFQkHnLNa6zce25GIH+vxKfvTgm3qpaH6ToF5tu/NN0IH66s+Bb5wElHrLcw==",
+ "license": "MIT"
+ },
"node_modules/@twilio/voice-errors": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@twilio/voice-errors/-/voice-errors-1.7.0.tgz",
@@ -2833,6 +2915,13 @@
"undici-types": "~7.18.0"
}
},
+ "node_modules/@types/pako": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
+ "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
@@ -2840,6 +2929,14 @@
"dev": true,
"license": "MIT"
},
+ "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==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
@@ -2894,7 +2991,7 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@types/underscore": {
@@ -2903,6 +3000,13 @@
"integrity": "sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==",
"license": "MIT"
},
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@@ -2930,6 +3034,84 @@
"vue": "^3.2.25"
}
},
+ "node_modules/@vue-flow/background": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@vue-flow/background/-/background-1.3.2.tgz",
+ "integrity": "sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@vue-flow/core": "^1.23.0",
+ "vue": "^3.3.0"
+ }
+ },
+ "node_modules/@vue-flow/controls": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@vue-flow/controls/-/controls-1.1.3.tgz",
+ "integrity": "sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@vue-flow/core": "^1.23.0",
+ "vue": "^3.3.0"
+ }
+ },
+ "node_modules/@vue-flow/core": {
+ "version": "1.48.2",
+ "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.48.2.tgz",
+ "integrity": "sha512-raxhgKWE+G/mcEvXJjGFUDYW9rAI3GOtiHR3ZkNpwBWuIaCC1EYiBmKGwJOoNzVFgwO7COgErnK7i08i287AFA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vueuse/core": "^10.5.0",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.0"
+ }
+ },
+ "node_modules/@vue-flow/core/node_modules/@types/web-bluetooth": {
+ "version": "0.0.20",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+ "license": "MIT"
+ },
+ "node_modules/@vue-flow/core/node_modules/@vueuse/core": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
+ "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "10.11.1",
+ "@vueuse/shared": "10.11.1",
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vue-flow/core/node_modules/@vueuse/metadata": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
+ "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vue-flow/core/node_modules/@vueuse/shared": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
+ "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/@vue/compiler-core": {
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz",
@@ -3048,6 +3230,47 @@
"integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
"license": "MIT"
},
+ "node_modules/@vueuse/core": {
+ "version": "14.3.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz",
+ "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "14.3.0",
+ "@vueuse/shared": "14.3.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "14.3.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz",
+ "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "14.3.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz",
+ "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -3266,7 +3489,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
"license": "Python-2.0"
},
"node_modules/array-buffer-byte-length": {
@@ -3325,6 +3547,16 @@
"node": ">=8"
}
},
+ "node_modules/astronomia": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.2.0.tgz",
+ "integrity": "sha512-mTvpBGyXB80aSsDhAAiuwza5VqAyqmj5yzhjBrFhRy17DcWDzJrb8Vdl4Sm+g276S+mY7bk/5hi6akZ5RQFeHg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -3447,6 +3679,16 @@
"dev": true,
"license": "MIT"
},
+ "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==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3661,6 +3903,19 @@
"node": ">= 0.8"
}
},
+ "node_modules/caldate": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/caldate/-/caldate-2.0.5.tgz",
+ "integrity": "sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==",
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "moment-timezone": "^0.5.43"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -3753,6 +4008,27 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/canvg": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
+ "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
+ "license": "MIT",
+ "optional": true,
+ "peer": 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/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -4091,6 +4367,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/core-js": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
+ "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/core-js-compat": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
@@ -4164,6 +4453,16 @@
"node": ">=8"
}
},
+ "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==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -4192,6 +4491,111 @@
"node": ">=0.10"
}
},
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -4246,6 +4650,85 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/date-bengali-revised": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/date-bengali-revised/-/date-bengali-revised-2.0.2.tgz",
+ "integrity": "sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/date-chinese": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/date-chinese/-/date-chinese-2.1.4.tgz",
+ "integrity": "sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "astronomia": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/date-easter": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/date-easter/-/date-easter-1.0.3.tgz",
+ "integrity": "sha512-aOViyIgpM4W0OWUiLqivznwTtuMlD/rdUWhc5IatYnplhPiWrLv75cnifaKYhmQwUBLAMWLNG4/9mlLIbXoGBQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/date-holidays": {
+ "version": "3.30.2",
+ "resolved": "https://registry.npmjs.org/date-holidays/-/date-holidays-3.30.2.tgz",
+ "integrity": "sha512-B7GLBc/7ucEw4KZeRKRMZin1NlTbQJtXn4tW4lHoRKdRzB68A/O6s/Jxlr/zStkZv43IeAQivYii/0dJytikGw==",
+ "license": "(ISC AND CC-BY-3.0)",
+ "peer": true,
+ "dependencies": {
+ "date-holidays-parser": "^3.4.7",
+ "js-yaml": "^4.1.1",
+ "lodash": "^4.18.1",
+ "prepin": "^1.0.3"
+ },
+ "bin": {
+ "holidays2json": "scripts/holidays2json.cjs"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/date-holidays-parser": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/date-holidays-parser/-/date-holidays-parser-3.4.7.tgz",
+ "integrity": "sha512-h09ZEtM6u5cYM6m1bX+1Ny9f+nLO9KVZUKNPEnH7lhbXYTfqZogaGTnhONswGeIJFF91UImIftS3CdM9HLW5oQ==",
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "astronomia": "^4.1.1",
+ "caldate": "^2.0.5",
+ "date-bengali-revised": "^2.0.2",
+ "date-chinese": "^2.1.4",
+ "date-easter": "^1.0.3",
+ "deepmerge": "^4.3.1",
+ "jalaali-js": "^1.2.7",
+ "moment-timezone": "^0.5.47"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.21",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
+ "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -4267,7 +4750,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -4377,6 +4859,17 @@
"node": ">=6.0.0"
}
},
+ "node_modules/dompurify": {
+ "version": "3.4.11",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
+ "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optional": true,
+ "peer": true,
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
@@ -5366,6 +5859,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-png": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
+ "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/pako": "^2.0.3",
+ "iobuffer": "^5.3.2",
+ "pako": "^2.1.0"
+ }
+ },
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
@@ -5393,6 +5898,13 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -6010,6 +6522,20 @@
"node": "^14.13.1 || >=16.0.0"
}
},
+ "node_modules/html2canvas": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "css-line-break": "^2.1.0",
+ "text-segmentation": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -6031,6 +6557,31 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/hy-vue-gantt": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/hy-vue-gantt/-/hy-vue-gantt-5.2.1.tgz",
+ "integrity": "sha512-b6UfjHWvsMtJ6UTm+kemPlsuEAPEbxy65eM8DgIEs1U22asPOQIMhmIJIuvE2m9IXK1rTeVqlJoW+Ggi9+NI0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@tsconfig/node22": "^22.0.0"
+ },
+ "peerDependencies": {
+ "@fortawesome/free-brands-svg-icons": "^7.1.0",
+ "@fortawesome/free-regular-svg-icons": "^7.1.0",
+ "@fortawesome/free-solid-svg-icons": "^7.1.0",
+ "@fortawesome/vue-fontawesome": "^3.0.8",
+ "@vueuse/core": "^14.2.0",
+ "date-holidays": "^3.23.14",
+ "dayjs": "^1.11.13",
+ "html2canvas": "^1.4.1",
+ "jspdf": "^4.1.0",
+ "lodash-es": "^4.17.21",
+ "papaparse": "^5.5.2",
+ "uuid": "^13.0.0",
+ "vue": "^3.5.13",
+ "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -6183,6 +6734,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/iobuffer": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
+ "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -6778,6 +7336,13 @@
"node": ">=10"
}
},
+ "node_modules/jalaali-js": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz",
+ "integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/jest-worker": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
@@ -6804,7 +7369,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -6883,6 +7447,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/jspdf": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz",
+ "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.28.6",
+ "fast-png": "^6.2.0",
+ "fflate": "^0.8.1"
+ },
+ "optionalDependencies": {
+ "canvg": "^3.0.11",
+ "core-js": "^3.6.0",
+ "dompurify": "^3.3.1",
+ "html2canvas": "^1.0.0-rc.5"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7004,12 +7586,18 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
- "dev": true,
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -7297,6 +7885,29 @@
"integrity": "sha512-Y3kpr3IFBk3Wm7AwONZ5vDAX7FxAaMk+RKbcKKewsuGI9oNCOSM2dWNcWVFdzZ9PF7awoaCgBSQudnJaJbUiBA==",
"license": "MIT"
},
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/moment-timezone": {
+ "version": "0.5.48",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
+ "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "moment": "^2.29.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -7598,6 +8209,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/pako": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
+ "license": "(MIT AND Zlib)",
+ "peer": true
+ },
+ "node_modules/papaparse": {
+ "version": "5.5.4",
+ "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.4.tgz",
+ "integrity": "sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -7687,6 +8312,14 @@
"dev": true,
"license": "MIT"
},
+ "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==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -7790,6 +8423,16 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prepin": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/prepin/-/prepin-1.0.3.tgz",
+ "integrity": "sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==",
+ "license": "Unlicense",
+ "peer": true,
+ "bin": {
+ "prepin": "bin/prepin.js"
+ }
+ },
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@@ -7892,6 +8535,17 @@
],
"license": "MIT"
},
+ "node_modules/raf": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+ "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "performance-now": "^2.1.0"
+ }
+ },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -8032,6 +8686,14 @@
"node": ">=4"
}
},
+ "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==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -8184,6 +8846,17 @@
"node": ">=0.10.0"
}
},
+ "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==",
+ "license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8.15"
+ }
+ },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -8810,6 +9483,17 @@
"node": ">=16"
}
},
+ "node_modules/stackblur-canvas": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+ "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.1.14"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -9033,6 +9717,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/table": {
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz",
@@ -9159,6 +9854,16 @@
"dev": true,
"license": "MIT"
},
+ "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==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -9508,6 +10213,30 @@
"node": ">= 0.4.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==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "13.0.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz",
+ "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -10168,6 +10897,19 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/xlsx": {
+ "version": "0.20.2",
+ "resolved": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz",
+ "integrity": "sha512-+nKZ39+nvK7Qq6i0PvWWRA4j/EkfWOtkP/YhMtupm+lJIiHxUrgTr1CcKv1nBk1rHtkRRQ3O2+Ih/q/sA+FXZA==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "xlsx": "bin/xlsx.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
diff --git a/apps/ops/package.json b/apps/ops/package.json
index bfec0b7..730469c 100644
--- a/apps/ops/package.json
+++ b/apps/ops/package.json
@@ -7,16 +7,21 @@
"scripts": {
"dev": "quasar dev",
"build": "quasar build -m pwa",
- "lint": "eslint --ext .js,.vue ./src"
+ "lint": "eslint --ext .js,.vue ./src",
+ "knip": "npx --yes knip"
},
"dependencies": {
"@quasar/extras": "^1.16.12",
"@twilio/voice-sdk": "^2.18.1",
+ "@vue-flow/background": "^1.3.2",
+ "@vue-flow/controls": "^1.1.3",
+ "@vue-flow/core": "^1.48.2",
"chart.js": "^4.5.1",
"cytoscape": "^3.33.2",
"grapesjs": "^0.22.16",
"grapesjs-mjml": "^1.0.8",
"grapesjs-preset-newsletter": "^1.0.2",
+ "hy-vue-gantt": "^5.2.1",
"idb-keyval": "^6.2.1",
"lucide-vue-next": "^1.0.0",
"pinia": "^2.1.7",
diff --git a/apps/ops/quasar.config.js b/apps/ops/quasar.config.js
index a6902da..5cf6d74 100644
--- a/apps/ops/quasar.config.js
+++ b/apps/ops/quasar.config.js
@@ -1,9 +1,17 @@
/* eslint-env node */
const { configure } = require('quasar/wrappers')
+// ⚠️ HARNAIS D'APERÇU LOCAL UNIQUEMENT — À RETIRER AVANT TOUT BUILD PROD.
+// Lit le jeton hub depuis .env.local (gitignored) et l'injecte CÔTÉ PROXY (jamais dans le bundle client).
+let _HUB_TOKEN = ''
+try {
+ const _m = require('fs').readFileSync(__dirname + '/.env.local', 'utf8').match(/^HUB_TOKEN=(.+)$/m)
+ _HUB_TOKEN = _m ? _m[1].trim() : ''
+} catch (e) { /* pas de .env.local → proxy /hub inactif */ }
+
module.exports = configure(function () {
return {
- boot: ['pinia', 'tooltip-ux'],
+ boot: ['net-timeout', 'pinia', 'tooltip-ux'],
css: ['app.scss', 'tech.scss'],
@@ -34,11 +42,20 @@ module.exports = configure(function () {
target: 'https://erp.gigafibre.ca',
changeOrigin: true,
},
+ // ⚠️ HARNAIS APERÇU LOCAL — /hub → hub live, jeton injecté côté proxy (jamais bundlé). À RETIRER avant build prod.
+ '/hub': {
+ target: 'https://msg.gigafibre.ca',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/hub/, ''),
+ headers: { Authorization: 'Bearer ' + _HUB_TOKEN, 'X-Authentik-Email': 'louis@targo.ca' },
+ },
},
},
framework: {
- config: {},
+ // Aligne les couleurs Quasar sur la palette de marque (vert). primary = accent (--ops-accent).
+ // Après migration color="indigo-6/7"→"primary", tout l'accent devient vert d'un coup.
+ config: { brand: { primary: '#16a34a', positive: '#16a34a', negative: '#ef4444', warning: '#f59e0b', info: '#2563eb' } },
plugins: ['Notify', 'Loading', 'LocalStorage', 'Dialog'],
},
diff --git a/apps/ops/src/App.vue b/apps/ops/src/App.vue
index 7471cee..f73455e 100644
--- a/apps/ops/src/App.vue
+++ b/apps/ops/src/App.vue
@@ -5,7 +5,13 @@
diff --git a/apps/ops/src/api/auth.js b/apps/ops/src/api/auth.js
index 33c797b..cb62f23 100644
--- a/apps/ops/src/api/auth.js
+++ b/apps/ops/src/api/auth.js
@@ -7,6 +7,11 @@ const SERVICE_TOKEN = import.meta.env.VITE_ERP_TOKEN || ''
// Reconnexion auto sur session Authentik expirée, anti-boucle (1 reload / 20 s max).
let _lastReload = 0
function _reauth (status) {
+ // DEV local (pas d'Authentik devant) : un 401 ERPNext ne doit PAS déclencher un rechargement en boucle.
+ // On renvoie le 401 et l'UI affiche ses états vides. En prod (Authentik) le comportement est inchangé.
+ if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
+ return new Response('{}', { status: status || 401 })
+ }
const now = Date.now()
if (now - _lastReload > 20000) {
_lastReload = now
@@ -24,18 +29,23 @@ export async function authFetch (url, opts = {}) {
try {
res = await fetch(url, opts)
} catch (e) {
- // Coupure réseau transitoire (ex: backend qui redémarre) → 1 retry court avant d'abandonner
+ // Coupure réseau transitoire (backend qui redémarre) OU timeout du garde net (session lente) → 1 retry court
await new Promise(r => setTimeout(r, 800))
- res = await fetch(url, opts)
+ try {
+ res = await fetch(url, opts)
+ } catch (e2) {
+ // 2e échec (réseau/timeout) : NE PAS planter l'appelant ni boucler sur reload → réponse vide, l'app reste réactive
+ return new Response('{}', { status: 504 })
+ }
}
// Détection « session Authentik expirée » (la cause du "il faut recharger") :
// - 401/403 explicites
// - réponse redirigée vers l'IdP (auth.targo.ca / /if/flow/)
- // - page HTML de login renvoyée à la place du JSON attendu sur un /api/
+ // - page HTML de login renvoyée à la place du JSON attendu sur un endpoint app (/api, /auth, /hub)
const ct = (res.headers && res.headers.get && res.headers.get('content-type')) || ''
const redirectedToIdp = res.redirected && /auth\.targo\.ca|\/if\/flow\//.test(res.url || '')
- const htmlForApi = res.status === 200 && ct.includes('text/html') && /\/api\//.test(String(url))
+ const htmlForApi = res.status === 200 && ct.includes('text/html') && /\/(api|auth|hub)\//.test(String(url))
if (res.status === 401 || res.status === 403 || redirectedToIdp || htmlForApi) {
return _reauth(res.status)
}
@@ -43,6 +53,8 @@ export async function authFetch (url, opts = {}) {
}
export async function getLoggedUser () {
+ // ⚠️ APERÇU LOCAL UNIQUEMENT : identité forcée via VITE_DEV_USER (gardé localhost + env). Inerte en prod. À RETIRER avec le harnais.
+ if ((window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && import.meta.env.VITE_DEV_USER) return import.meta.env.VITE_DEV_USER
// First try nginx whoami endpoint (returns Authentik email header)
try {
const res = await fetch(BASE_URL + '/auth/whoami')
diff --git a/apps/ops/src/api/billing.js b/apps/ops/src/api/billing.js
new file mode 100644
index 0000000..ace3602
--- /dev/null
+++ b/apps/ops/src/api/billing.js
@@ -0,0 +1,35 @@
+/**
+ * api/billing.js — Client for Hub /billing approval endpoints (P1).
+ *
+ * Porte d'approbation de facturation : lister les installations complétées en
+ * attente, prévisualiser le prorata (LECTURE SEULE), approuver (= active le
+ * service ; la facturation reste gatée côté hub par PRORATION_WRITE).
+ */
+import { HUB_URL } from 'src/config/hub'
+
+async function hubFetch (path, { method = 'GET', body } = {}) {
+ const opts = { method, headers: { 'Content-Type': 'application/json' } }
+ if (body) opts.body = JSON.stringify(body)
+ const res = await fetch(`${HUB_URL}${path}`, opts)
+ const text = await res.text()
+ let data
+ try { data = text ? JSON.parse(text) : {} } catch { throw new Error(`Invalid JSON from ${path}: ${text.slice(0, 200)}`) }
+ if (!res.ok) { const err = new Error(data.error || `HTTP ${res.status}`); err.status = res.status; throw err }
+ return data
+}
+
+// Activations en attente d'approbation (enrichies client + adresse).
+export function getApprovals () {
+ return hubFetch('/billing/approvals').then(r => r.pending || [])
+}
+
+// Aperçu prorata consolidé pour un job (LECTURE SEULE — aucune facture créée).
+export function getApprovalPreview (job) {
+ return hubFetch(`/billing/approvals/preview?job=${encodeURIComponent(job)}`)
+}
+
+// Approuve → active les abonnements. L'approbateur est attribué côté serveur
+// (en-tête x-authentik-email). La facturation reste gatée (PRORATION_WRITE).
+export function approveActivation (job) {
+ return hubFetch('/billing/approvals/approve', { method: 'POST', body: { job } })
+}
diff --git a/apps/ops/src/api/campaigns.js b/apps/ops/src/api/campaigns.js
index 967d78b..d054663 100644
--- a/apps/ops/src/api/campaigns.js
+++ b/apps/ops/src/api/campaigns.js
@@ -92,6 +92,37 @@ export function listGifts () {
return hubFetch('/campaigns/gifts').then(r => r.gifts || [])
}
+// Pool de liens-cadeaux récupérés (jamais cliqués) prêts à réallouer à d'autres clients.
+export function getGiftPool () {
+ return hubFetch('/campaigns/gifts/pool')
+}
+
+// Récupère les liens non cliqués d'une campagne vers le pool (dry_run par défaut = aperçu).
+export function reclaimGifts ({ campaign_id, dry_run = true, revoke_live = true }) {
+ return hubFetch('/campaigns/gifts/reclaim', { method: 'POST', body: { campaign_id, dry_run, revoke_live } })
+}
+
+// Envoi unitaire d'une récompense depuis le pool vers un client (dry_run pour aperçu).
+export function rewardSend (body) {
+ return hubFetch('/campaigns/reward-send', { method: 'POST', body })
+}
+
+// Récompenses déjà préparées/envoyées à un client (pour la fiche : voir / copier le lien).
+export function rewardByCustomer (customer_id, email) {
+ const q = customer_id ? 'customer_id=' + encodeURIComponent(customer_id) : 'email=' + encodeURIComponent(email || '')
+ return hubFetch('/campaigns/reward/by-customer?' + q)
+}
+
+// Supprime un brouillon / révoque une récompense non cliquée (remet le lien en réserve ; refusé si déjà ouverte).
+export function revokeRewardByToken (gift_token) {
+ return hubFetch('/campaigns/reward/revoke', { method: 'POST', body: { gift_token } })
+}
+
+// Révoque une récompense déjà envoyée + remet le lien Giftbit au pool (refusé si déjà cliqué/redirigé).
+export function revokeReward (gift_url) {
+ return hubFetch('/campaigns/gifts/revoke-reward', { method: 'POST', body: { gift_url } })
+}
+
// Kill switch — manually expire a single recipient's wrapper token so
// the underlying Giftbit URL becomes reassignable before the natural
// expiry date.
diff --git a/apps/ops/src/api/reports.js b/apps/ops/src/api/reports.js
index 82ee17c..afc5673 100644
--- a/apps/ops/src/api/reports.js
+++ b/apps/ops/src/api/reports.js
@@ -91,3 +91,23 @@ export function overpricedInternetCsvUrl ({ threshold = 90, segment = 'residenti
})
return `${HUB}/reports/legacy/overpriced-internet.csv?${qs}`
}
+
+/**
+ * Explorateur de revenus — MRR net récurrent regroupé par DIMENSION (un seul rapport,
+ * critères ajoutables). Remplace les rapports F fixes (par service / code postal / ville / segment).
+ * @param {object} opts { dimension:'category|zip|city|segment|plan', segment, type:'internet|tv|phone|all', active, threshold, limit }
+ */
+export function fetchRevenueExplorer ({ dimension = 'category', segment = 'all', type = 'all', active = true, threshold = 0, limit = 300 } = {}) {
+ const qs = new URLSearchParams({
+ dimension, segment, type, active: active ? '1' : '0', threshold: String(threshold), limit: String(limit),
+ })
+ return hubFetch(`/reports/legacy/revenue-explorer?${qs}`)
+}
+
+/** URL CSV directe pour l'explorateur de revenus (mêmes critères). */
+export function revenueExplorerCsvUrl ({ dimension = 'category', segment = 'all', type = 'all', active = true, threshold = 0 } = {}) {
+ const qs = new URLSearchParams({
+ dimension, segment, type, active: active ? '1' : '0', threshold: String(threshold),
+ })
+ return `${HUB}/reports/legacy/revenue-explorer.csv?${qs}`
+}
diff --git a/apps/ops/src/api/roster.js b/apps/ops/src/api/roster.js
index 621fef5..9c3e510 100644
--- a/apps/ops/src/api/roster.js
+++ b/apps/ops/src/api/roster.js
@@ -28,6 +28,10 @@ async function jput (path, body) {
export const listTechnicians = () => jget('/roster/technicians')
export const listTemplates = () => jget('/roster/templates')
export const createTemplate = (t) => jpost('/roster/templates', t)
+// Fériés QC déterministes (calendrier) + préavis de disponibilité avant un férié.
+export const holidays = (from, to) => jget(`/roster/holidays?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`)
+export const holidayNotice = (days = 35) => jget('/roster/holiday-notice?days=' + days)
+export const sendHolidayNotice = (date) => jpost('/roster/holiday-notice', { date })
export const listRequirements = (start, days = 7) => jget(`/roster/requirements?start=${start}&days=${days}`)
export const createRequirement = (r) => jpost('/roster/requirements', r)
export const listAssignments = (start, days = 7) => jget(`/roster/assignments?start=${start}&days=${days}`)
@@ -98,6 +102,20 @@ export const pushLegacyPreview = () => jget('/dispatch/legacy-sync/push-assignme
export const pushLegacyApply = (notify = true) => jpost('/dispatch/legacy-sync/push-assignments' + (notify ? '' : '?notify=0'), {})
// Fil complet d'un ticket legacy (osTicket) : messages + réponses, pour l'expand au clic
export const ticketThread = (id) => jget('/dispatch/legacy-sync/ticket-thread?id=' + encodeURIComponent(id))
+// Poster au fil d'un ticket : isPublic=false (défaut) = note INTERNE (jamais au client) · true = message public
+export const postTicket = (ticket, msg, isPublic) => jpost('/dispatch/legacy-sync/ticket-post', { ticket, msg, public: !!isPublic })
+// GÉNÉRALISÉ par JOB (osTicket OU ERPNext-natif) : fil+contact (lecture seule) et résolution de conversation pour répondre.
+export const jobThread = (job) => jget('/dispatch/legacy-sync/job-thread?job=' + encodeURIComponent(job))
+export const jobConversation = (job) => jget('/dispatch/legacy-sync/job-conversation?job=' + encodeURIComponent(job))
+// P2 — réponse au CLIENT via le MÊME chemin que la Boîte (/conversations/{token}/messages → Outbox courriel/SMS + miroir osTicket).
+// X-Authentik-Email REQUIS : sans lui le hub enregistre l'envoi comme message CLIENT (from='customer') + déclenche l'IA. À passer (useAuthStore().user).
+export async function sendConvMessage (token, text, agentEmail) {
+ const headers = { 'Content-Type': 'application/json' }
+ if (agentEmail) headers['X-Authentik-Email'] = agentEmail
+ const r = await fetch(HUB + '/conversations/' + encodeURIComponent(token) + '/messages', { method: 'POST', headers, body: JSON.stringify({ text }) })
+ if (!r.ok) throw new Error('Envoi conv ' + r.status)
+ return r.json()
+}
// Fermer un ticket dans le legacy (status=closed + date_closed + closed_by=acteur + réouverture enfants)
export const closeLegacyTicket = (ticket) => jpost('/dispatch/legacy-sync/close-ticket?ticket=' + encodeURIComponent(ticket), {})
// Fermeture EN LOT (1 appel pour N tickets) — efficace
@@ -112,6 +130,10 @@ export const reorderJobs = (updates) => jpost('/roster/reorder-jobs', { updates
export const unassignJobRoster = (job) => jpost('/roster/unassign-job', { job })
// Situer manuellement un job « hors carte » : pose latitude/longitude (+ adresse) choisis sur la carte
export const setJobLocation = (job, lat, lon, address) => jpost('/roster/job/set-location', { job, lat, lon, address })
+// Actions rapides du pool (mobile) : patch d'un Dispatch Job (priority/status/required_skill/scheduled_date/notes — liste blanche côté hub).
+export const updateJob = (job, patch) => jpost('/roster/job/update', { job, patch })
+// Persister UN quart immédiatement (sinon addShift reste local/Proposé et disparaît au rechargement). Idempotent côté hub.
+export const createShift = (s) => jpost('/roster/shift', s)
// ÉQUIPE d'un job (assistants en renfort) — le lead reste inchangé ; l'assistant épinglé voit un bloc hachuré dans son horaire.
export const getJobTeam = (job) => jget('/roster/job/team?job=' + encodeURIComponent(job))
export const addAssistant = (job, a) => jpost('/roster/job/team', { job, add: a })
@@ -139,3 +161,14 @@ export const notifyReschedule = (body) => jpost('/roster/job/notify-reschedule',
export const gpxUrl = (tech, from, to) => HUB + '/traccar/gpx?tech=' + encodeURIComponent(tech) + '&from=' + encodeURIComponent(from) + '&to=' + encodeURIComponent(to)
// Tracé GPS (breadcrumb) d'un tech sur UNE journée → { coords:[[lon,lat]…], count }. Plage exacte (1 jour) obligatoire (Traccar lourd sinon).
export const traccarTrack = (tech, from, to) => jget('/traccar/track?tech=' + encodeURIComponent(tech) + '&from=' + encodeURIComponent(from) + '&to=' + encodeURIComponent(to))
+// Validation « service livré » d'un job : statut service+modem (en ligne + signal) pour le client/adresse du job.
+// Croise tech-présent (geofence) + service-en-ligne → preuve de complétion d'install.
+export const jobServiceStatus = (job) => jget('/roster/job-service-status?job=' + encodeURIComponent(job))
+// Aperçu croisement temps-sur-site (Traccar × emplacement job) + ancre d'activation GenieACS (clamped_minutes / service_anchor).
+export const onsiteCross = (days = 30) => jget('/traccar/onsite?days=' + days)
+// Position GPS LIVE (dernière connue) d'un tech → { ok, lat, lon, time, speed } ; pour fixer son point de départ (domicile).
+export const techLivePosition = (tech) => jget('/traccar/live?tech=' + encodeURIComponent(tech))
+// Liste des appareils Traccar (pour associer un tech à son GPS) — { id, name, uniqueId, status, lastUpdate }.
+export const listTraccarDevices = () => jget('/traccar/devices')
+// Associer / changer (device_id) ou dissocier ('') l'appareil Traccar d'un tech — INLINE depuis Planif.
+export const setTechTraccarDevice = (id, deviceId) => jpost('/roster/technician/' + encodeURIComponent(id) + '/traccar-device', { device_id: deviceId })
diff --git a/apps/ops/src/api/subcontractor.js b/apps/ops/src/api/subcontractor.js
new file mode 100644
index 0000000..3187a38
--- /dev/null
+++ b/apps/ops/src/api/subcontractor.js
@@ -0,0 +1,31 @@
+/**
+ * Sous-traitants payés à l'acte — appelle targo-hub /acte/*.
+ * Barème (prix par acte), génération/envoi du lien à token, revue des soumissions.
+ * Voir services/targo-hub/lib/subcontractor.js. Token hub injecté par nginx /hub (même patron que roster.js).
+ */
+import { HUB_URL as HUB } from 'src/config/hub'
+
+async function jget (p) { const r = await fetch(HUB + p); if (!r.ok) throw new Error('Acte API ' + r.status); return r.json() }
+async function jpost (p, b) {
+ const r = await fetch(HUB + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(b || {}) })
+ if (!r.ok) throw new Error('Acte API ' + r.status); return r.json()
+}
+
+export const getRates = (sub) => jget('/acte/rates' + (sub ? '?sub=' + encodeURIComponent(sub) : '')) // sub → barème effectif du sous-traitant (global + surcharge)
+export const saveRates = (rates, sub) => jpost('/acte/rates', sub ? { rates, sub } : { rates }) // sub → surcharge par sous-traitant ; sinon barème global
+export const listSubmissions = (ticket) => jget('/acte/list' + (ticket ? '?ticket=' + encodeURIComponent(ticket) : ''))
+export const sendLink = (ticket, subcontractor, phone) => jpost('/acte/send', { ticket, subcontractor, phone })
+export const approve = (id, action, reason) => jpost('/acte/approve', { id, action, reason }) // action: 'approve' | 'reject' (gaté admin/superviseur côté hub)
+export const imgUrl = (file) => HUB + '/acte/img?file=' + encodeURIComponent(file) // servi gaté via le proxy /hub
+
+// ── Autofacturation : config fiscale par sous-traitant, relevé par période, marquer payé, export Acomba (CSV) ──
+export const getSubs = () => jget('/acte/subs') // { subs:[{name,registrant,gst_no,qst_no,email,phone}], names:[...vus en soumission] }
+export const saveSub = (sub) => jpost('/acte/subs', sub) // {name, registrant, gst_no, qst_no, email, phone} — gaté admin/superviseur
+const qstr = (sub, from, to) => '?subcontractor=' + encodeURIComponent(sub) + (from ? '&from=' + from : '') + (to ? '&to=' + to : '')
+export const getReleve = (sub, from, to) => jget('/acte/releve' + qstr(sub, from, to)) // { releve:{config,lines,subtotal,tps,tvq,total,...} }
+export const markPaid = (body) => jpost('/acte/mark-paid', body) // { subcontractor, from, to } OU { ids:[] } — gaté admin/superviseur
+export const exportUrl = (sub, from, to) => HUB + '/acte/export' + qstr(sub, from, to) // CSV (téléchargé via le proxy /hub)
+
+// ── Entente de services : lien web ou PDF des conditions (montré au sous-traitant à la certification) ──
+export const getEntente = () => jget('/acte/entente') // { url }
+export const saveEntente = (body) => jpost('/acte/entente', body) // { url } | { pdf: dataUrl } | { clear: true } — gaté admin/superviseur
diff --git a/apps/ops/src/boot/net-timeout.js b/apps/ops/src/boot/net-timeout.js
new file mode 100644
index 0000000..ebf38ff
--- /dev/null
+++ b/apps/ops/src/boot/net-timeout.js
@@ -0,0 +1,26 @@
+// Empêche TOUTE requête de l'app (same-origin : /api, /hub, /auth) de PENDRE indéfiniment.
+// Cause du gel « même un clic de menu ne répond plus » : sur session Authentik morte/lente, un fetch
+// sans timeout ne résout jamais → la page attendue ne s'affiche pas, la vue ne change pas au clic,
+// et les 69 appels /hub (fetch nu) restent suspendus. On borne chaque requête app à 30 s.
+// Externes (Mapbox api.mapbox.com, etc.) et appels fournissant déjà un signal : inchangés.
+// EventSource (SSE) n'utilise pas window.fetch → non affecté.
+import { boot } from 'quasar/wrappers'
+
+const APP_TIMEOUT_MS = 30000
+
+export default boot(() => {
+ if (typeof window === 'undefined' || window.__netTimeoutInstalled) return
+ window.__netTimeoutInstalled = true
+ const orig = window.fetch.bind(window)
+ const origin = window.location.origin
+ window.fetch = function (input, init) {
+ init = init || {}
+ let url = ''
+ try { url = typeof input === 'string' ? input : (input && input.url) || '' } catch { /* Request exotique */ }
+ const sameApp = url.startsWith('/') || url.startsWith(origin) // requêtes app ; exclut absolu cross-origin
+ if (!sameApp || init.signal) return orig(input, init) // externe ou signal déjà fourni → ne pas toucher
+ const ctrl = new AbortController()
+ const id = setTimeout(() => ctrl.abort(), APP_TIMEOUT_MS)
+ return orig(input, { ...init, signal: ctrl.signal }).finally(() => clearTimeout(id))
+ }
+})
diff --git a/apps/ops/src/components/customer/ChatterPanel.vue b/apps/ops/src/components/customer/ChatterPanel.vue
deleted file mode 100644
index 7dd5877..0000000
--- a/apps/ops/src/components/customer/ChatterPanel.vue
+++ /dev/null
@@ -1,544 +0,0 @@
-
- ::raccourci dans la zone de texte pour insérer une réponse rapide.
AI_LOCAL_API_KEY dans /opt/targo-hub/.env (les secrets restent côté serveur, jamais dans l'UI), puis recrée le conteneur.
{{ client }}, {{ agent }} (remplacées à l'insertion).