@fui-org/fui-cli 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +27 -7
  2. package/dist/fui-qrb1j9jz.js +397 -0
  3. package/dist/fui.js +1 -1
  4. package/package.json +1 -1
  5. package/skills/fui/SKILL.md +143 -38
  6. package/skills/fui/agents/fui-builder.md +62 -0
  7. package/skills/fui/agents/fui-tester.md +77 -0
  8. package/skills/{fui-skill → fui}/assets/projectdefaultstyle.css +489 -489
  9. package/skills/{fui-skill → fui}/examples/component.vue +162 -162
  10. package/skills/{fui-skill → fui}/references/INDEX.md +5 -4
  11. package/skills/{fui-skill → fui}/references/advanced-techniques.md +170 -168
  12. package/skills/{fui-skill → fui}/references/component-design.md +2 -2
  13. package/skills/{fui-skill → fui}/references/component-quickref.md +78 -78
  14. package/skills/{fui-skill → fui}/references/component-table.md +287 -287
  15. package/skills/{fui-skill → fui}/references/components-dialog.md +191 -191
  16. package/skills/{fui-skill → fui}/references/components-display.md +141 -141
  17. package/skills/{fui-skill → fui}/references/components-echart.md +316 -316
  18. package/skills/{fui-skill → fui}/references/components-input.md +335 -335
  19. package/skills/{fui-skill → fui}/references/controls-patterns.md +2 -2
  20. package/skills/{fui-skill → fui}/references/controls-styling-vocabulary.md +173 -173
  21. package/skills/{fui-skill → fui}/references/db-workflow.md +9 -9
  22. package/skills/{fui-skill → fui}/references/default-function.md +456 -456
  23. package/skills/{fui-skill → fui}/references/design-modes.md +57 -57
  24. package/skills/{fui-skill → fui}/references/echart-templates.md +489 -489
  25. package/skills/{fui-skill → fui}/references/fastproject.md +99 -99
  26. package/skills/{fui-skill → fui}/references/fullstack-workflow.md +314 -313
  27. package/skills/{fui-skill → fui}/references/module-data-patterns.md +117 -117
  28. package/skills/{fui-skill → fui}/references/module-json-anatomy.md +132 -132
  29. package/skills/{fui-skill → fui}/references/module-structure.md +144 -144
  30. package/skills/{fui-skill → fui}/references/new-session.md +1 -2
  31. package/skills/{fui-skill → fui}/references/pdfmake.md +60 -60
  32. package/skills/{fui-skill → fui}/references/permission-system.md +1 -1
  33. package/skills/{fui-skill → fui}/references/platform-architecture.md +269 -269
  34. package/skills/{fui-skill → fui}/references/project-config.md +303 -303
  35. package/skills/{fui-skill → fui}/references/project-provisioning.md +4 -4
  36. package/skills/{fui-skill → fui}/references/script-map.md +262 -262
  37. package/skills/{fui-skill → fui}/references/sql-clr-functions.md +225 -225
  38. package/skills/{fui-skill → fui}/references/system-design.md +2 -2
  39. package/skills/{fui-skill → fui}/references/tapi-permission-patterns.md +156 -156
  40. package/skills/{fui-skill → fui}/references/tapi-reference.md +474 -474
  41. package/skills/fui/references/team-run.md +141 -0
  42. package/skills/fui/references/tools-registry.md +211 -0
  43. package/skills/{fui-skill → fui}/references/ui-crosswindow-patterns.md +321 -321
  44. package/skills/{fui-skill → fui}/references/ui-dialog-patterns.md +255 -255
  45. package/skills/{fui-skill → fui}/references/ui-layout-patterns.md +176 -176
  46. package/skills/{fui-skill → fui}/references/ui-patterns.md +303 -303
  47. package/skills/{fui-skill → fui}/references/ui-screenshot-review.md +95 -95
  48. package/skills/{fui-skill → fui}/references/ui-table-cell-patterns.md +318 -318
  49. package/skills/{fui-skill → fui}/references/verification.md +11 -11
  50. package/skills/{fui-skill → fui}/references/watcher-patterns.md +163 -163
  51. package/skills/{fui-skill → fui}/references/websocket-realtime.md +271 -271
  52. package/skills/{fui-skill → fui}/scripts/component-3.0.js +2715 -2715
  53. package/skills/{fui-skill → fui}/scripts/component.js +3147 -3147
  54. package/skills/{fui-skill → fui}/scripts/componentTable-3.0.js +1038 -1038
  55. package/skills/{fui-skill → fui}/scripts/componentTable.js +891 -891
  56. package/skills/{fui-skill → fui}/scripts/defaultfunction-3.0.js +866 -866
  57. package/skills/{fui-skill → fui}/scripts/defaultfunction.js +1051 -1051
  58. package/skills/{fui-skill → fui}/scripts/fastproject-3.0.js +870 -870
  59. package/skills/{fui-skill → fui}/scripts/fastproject.js +828 -828
  60. package/skills/fui-target/hooks/register.tsx +2 -0
  61. package/skills/fui-target/types/index.d.ts +1 -0
  62. package/dist/fui-hk5frcn2.js +0 -381
  63. package/skills/fui-skill/SKILL.md +0 -139
  64. package/skills/fui-skill/references/tools-registry.md +0 -84
  65. /package/skills/{fui-skill → fui}/assets/projectdefaultstyle-3.0.css +0 -0
  66. /package/skills/{fui-skill → fui}/design-md/airbnb/DESIGN.md +0 -0
  67. /package/skills/{fui-skill → fui}/design-md/airbnb/README.md +0 -0
  68. /package/skills/{fui-skill → fui}/design-md/airtable/DESIGN.md +0 -0
  69. /package/skills/{fui-skill → fui}/design-md/airtable/README.md +0 -0
  70. /package/skills/{fui-skill → fui}/design-md/apple/DESIGN.md +0 -0
  71. /package/skills/{fui-skill → fui}/design-md/apple/README.md +0 -0
  72. /package/skills/{fui-skill → fui}/design-md/asu/DESIGN.md +0 -0
  73. /package/skills/{fui-skill → fui}/design-md/asu/README.md +0 -0
  74. /package/skills/{fui-skill → fui}/design-md/binance/DESIGN.md +0 -0
  75. /package/skills/{fui-skill → fui}/design-md/binance/README.md +0 -0
  76. /package/skills/{fui-skill → fui}/design-md/bmw/DESIGN.md +0 -0
  77. /package/skills/{fui-skill → fui}/design-md/bmw/README.md +0 -0
  78. /package/skills/{fui-skill → fui}/design-md/bmw-m/DESIGN.md +0 -0
  79. /package/skills/{fui-skill → fui}/design-md/bmw-m/README.md +0 -0
  80. /package/skills/{fui-skill → fui}/design-md/bugatti/DESIGN.md +0 -0
  81. /package/skills/{fui-skill → fui}/design-md/bugatti/README.md +0 -0
  82. /package/skills/{fui-skill → fui}/design-md/cal/DESIGN.md +0 -0
  83. /package/skills/{fui-skill → fui}/design-md/cal/README.md +0 -0
  84. /package/skills/{fui-skill → fui}/design-md/claude/DESIGN.md +0 -0
  85. /package/skills/{fui-skill → fui}/design-md/claude/README.md +0 -0
  86. /package/skills/{fui-skill → fui}/design-md/clay/DESIGN.md +0 -0
  87. /package/skills/{fui-skill → fui}/design-md/clay/README.md +0 -0
  88. /package/skills/{fui-skill → fui}/design-md/clickhouse/DESIGN.md +0 -0
  89. /package/skills/{fui-skill → fui}/design-md/clickhouse/README.md +0 -0
  90. /package/skills/{fui-skill → fui}/design-md/cohere/DESIGN.md +0 -0
  91. /package/skills/{fui-skill → fui}/design-md/cohere/README.md +0 -0
  92. /package/skills/{fui-skill → fui}/design-md/coinbase/DESIGN.md +0 -0
  93. /package/skills/{fui-skill → fui}/design-md/coinbase/README.md +0 -0
  94. /package/skills/{fui-skill → fui}/design-md/composio/DESIGN.md +0 -0
  95. /package/skills/{fui-skill → fui}/design-md/composio/README.md +0 -0
  96. /package/skills/{fui-skill → fui}/design-md/cursor/DESIGN.md +0 -0
  97. /package/skills/{fui-skill → fui}/design-md/cursor/README.md +0 -0
  98. /package/skills/{fui-skill → fui}/design-md/elevenlabs/DESIGN.md +0 -0
  99. /package/skills/{fui-skill → fui}/design-md/elevenlabs/README.md +0 -0
  100. /package/skills/{fui-skill → fui}/design-md/expo/DESIGN.md +0 -0
  101. /package/skills/{fui-skill → fui}/design-md/expo/README.md +0 -0
  102. /package/skills/{fui-skill → fui}/design-md/ferrari/DESIGN.md +0 -0
  103. /package/skills/{fui-skill → fui}/design-md/ferrari/README.md +0 -0
  104. /package/skills/{fui-skill → fui}/design-md/figma/DESIGN.md +0 -0
  105. /package/skills/{fui-skill → fui}/design-md/figma/README.md +0 -0
  106. /package/skills/{fui-skill → fui}/design-md/framer/DESIGN.md +0 -0
  107. /package/skills/{fui-skill → fui}/design-md/framer/README.md +0 -0
  108. /package/skills/{fui-skill → fui}/design-md/fui/DESIGN.md +0 -0
  109. /package/skills/{fui-skill → fui}/design-md/hashicorp/DESIGN.md +0 -0
  110. /package/skills/{fui-skill → fui}/design-md/hashicorp/README.md +0 -0
  111. /package/skills/{fui-skill → fui}/design-md/ibm/DESIGN.md +0 -0
  112. /package/skills/{fui-skill → fui}/design-md/ibm/README.md +0 -0
  113. /package/skills/{fui-skill → fui}/design-md/intercom/DESIGN.md +0 -0
  114. /package/skills/{fui-skill → fui}/design-md/intercom/README.md +0 -0
  115. /package/skills/{fui-skill → fui}/design-md/kraken/DESIGN.md +0 -0
  116. /package/skills/{fui-skill → fui}/design-md/kraken/README.md +0 -0
  117. /package/skills/{fui-skill → fui}/design-md/lamborghini/DESIGN.md +0 -0
  118. /package/skills/{fui-skill → fui}/design-md/lamborghini/README.md +0 -0
  119. /package/skills/{fui-skill → fui}/design-md/linear.app/DESIGN.md +0 -0
  120. /package/skills/{fui-skill → fui}/design-md/linear.app/README.md +0 -0
  121. /package/skills/{fui-skill → fui}/design-md/lovable/DESIGN.md +0 -0
  122. /package/skills/{fui-skill → fui}/design-md/lovable/README.md +0 -0
  123. /package/skills/{fui-skill → fui}/design-md/mastercard/DESIGN.md +0 -0
  124. /package/skills/{fui-skill → fui}/design-md/mastercard/README.md +0 -0
  125. /package/skills/{fui-skill → fui}/design-md/meta/DESIGN.md +0 -0
  126. /package/skills/{fui-skill → fui}/design-md/meta/README.md +0 -0
  127. /package/skills/{fui-skill → fui}/design-md/minimax/DESIGN.md +0 -0
  128. /package/skills/{fui-skill → fui}/design-md/minimax/README.md +0 -0
  129. /package/skills/{fui-skill → fui}/design-md/mintlify/DESIGN.md +0 -0
  130. /package/skills/{fui-skill → fui}/design-md/mintlify/README.md +0 -0
  131. /package/skills/{fui-skill → fui}/design-md/miro/DESIGN.md +0 -0
  132. /package/skills/{fui-skill → fui}/design-md/miro/README.md +0 -0
  133. /package/skills/{fui-skill → fui}/design-md/mistral.ai/DESIGN.md +0 -0
  134. /package/skills/{fui-skill → fui}/design-md/mistral.ai/README.md +0 -0
  135. /package/skills/{fui-skill → fui}/design-md/mongodb/DESIGN.md +0 -0
  136. /package/skills/{fui-skill → fui}/design-md/mongodb/README.md +0 -0
  137. /package/skills/{fui-skill → fui}/design-md/nike/DESIGN.md +0 -0
  138. /package/skills/{fui-skill → fui}/design-md/nike/README.md +0 -0
  139. /package/skills/{fui-skill → fui}/design-md/notion/DESIGN.md +0 -0
  140. /package/skills/{fui-skill → fui}/design-md/notion/README.md +0 -0
  141. /package/skills/{fui-skill → fui}/design-md/nvidia/DESIGN.md +0 -0
  142. /package/skills/{fui-skill → fui}/design-md/nvidia/README.md +0 -0
  143. /package/skills/{fui-skill → fui}/design-md/ollama/DESIGN.md +0 -0
  144. /package/skills/{fui-skill → fui}/design-md/ollama/README.md +0 -0
  145. /package/skills/{fui-skill → fui}/design-md/opencode.ai/DESIGN.md +0 -0
  146. /package/skills/{fui-skill → fui}/design-md/opencode.ai/README.md +0 -0
  147. /package/skills/{fui-skill → fui}/design-md/pinterest/DESIGN.md +0 -0
  148. /package/skills/{fui-skill → fui}/design-md/pinterest/README.md +0 -0
  149. /package/skills/{fui-skill → fui}/design-md/playstation/DESIGN.md +0 -0
  150. /package/skills/{fui-skill → fui}/design-md/playstation/README.md +0 -0
  151. /package/skills/{fui-skill → fui}/design-md/posthog/DESIGN.md +0 -0
  152. /package/skills/{fui-skill → fui}/design-md/posthog/README.md +0 -0
  153. /package/skills/{fui-skill → fui}/design-md/raycast/DESIGN.md +0 -0
  154. /package/skills/{fui-skill → fui}/design-md/raycast/README.md +0 -0
  155. /package/skills/{fui-skill → fui}/design-md/renault/DESIGN.md +0 -0
  156. /package/skills/{fui-skill → fui}/design-md/renault/README.md +0 -0
  157. /package/skills/{fui-skill → fui}/design-md/replicate/DESIGN.md +0 -0
  158. /package/skills/{fui-skill → fui}/design-md/replicate/README.md +0 -0
  159. /package/skills/{fui-skill → fui}/design-md/resend/DESIGN.md +0 -0
  160. /package/skills/{fui-skill → fui}/design-md/resend/README.md +0 -0
  161. /package/skills/{fui-skill → fui}/design-md/revolut/DESIGN.md +0 -0
  162. /package/skills/{fui-skill → fui}/design-md/revolut/README.md +0 -0
  163. /package/skills/{fui-skill → fui}/design-md/runwayml/DESIGN.md +0 -0
  164. /package/skills/{fui-skill → fui}/design-md/runwayml/README.md +0 -0
  165. /package/skills/{fui-skill → fui}/design-md/sanity/DESIGN.md +0 -0
  166. /package/skills/{fui-skill → fui}/design-md/sanity/README.md +0 -0
  167. /package/skills/{fui-skill → fui}/design-md/sentry/DESIGN.md +0 -0
  168. /package/skills/{fui-skill → fui}/design-md/sentry/README.md +0 -0
  169. /package/skills/{fui-skill → fui}/design-md/shopify/DESIGN.md +0 -0
  170. /package/skills/{fui-skill → fui}/design-md/shopify/README.md +0 -0
  171. /package/skills/{fui-skill → fui}/design-md/slack/DESIGN.md +0 -0
  172. /package/skills/{fui-skill → fui}/design-md/spacex/DESIGN.md +0 -0
  173. /package/skills/{fui-skill → fui}/design-md/spacex/README.md +0 -0
  174. /package/skills/{fui-skill → fui}/design-md/spotify/DESIGN.md +0 -0
  175. /package/skills/{fui-skill → fui}/design-md/spotify/README.md +0 -0
  176. /package/skills/{fui-skill → fui}/design-md/starbucks/DESIGN.md +0 -0
  177. /package/skills/{fui-skill → fui}/design-md/starbucks/README.md +0 -0
  178. /package/skills/{fui-skill → fui}/design-md/stripe/DESIGN.md +0 -0
  179. /package/skills/{fui-skill → fui}/design-md/stripe/README.md +0 -0
  180. /package/skills/{fui-skill → fui}/design-md/supabase/DESIGN.md +0 -0
  181. /package/skills/{fui-skill → fui}/design-md/supabase/README.md +0 -0
  182. /package/skills/{fui-skill → fui}/design-md/superhuman/DESIGN.md +0 -0
  183. /package/skills/{fui-skill → fui}/design-md/superhuman/README.md +0 -0
  184. /package/skills/{fui-skill → fui}/design-md/tesla/DESIGN.md +0 -0
  185. /package/skills/{fui-skill → fui}/design-md/tesla/README.md +0 -0
  186. /package/skills/{fui-skill → fui}/design-md/theverge/DESIGN.md +0 -0
  187. /package/skills/{fui-skill → fui}/design-md/theverge/README.md +0 -0
  188. /package/skills/{fui-skill → fui}/design-md/together.ai/DESIGN.md +0 -0
  189. /package/skills/{fui-skill → fui}/design-md/together.ai/README.md +0 -0
  190. /package/skills/{fui-skill → fui}/design-md/uber/DESIGN.md +0 -0
  191. /package/skills/{fui-skill → fui}/design-md/uber/README.md +0 -0
  192. /package/skills/{fui-skill → fui}/design-md/vercel/DESIGN.md +0 -0
  193. /package/skills/{fui-skill → fui}/design-md/vercel/README.md +0 -0
  194. /package/skills/{fui-skill → fui}/design-md/vodafone/DESIGN.md +0 -0
  195. /package/skills/{fui-skill → fui}/design-md/vodafone/README.md +0 -0
  196. /package/skills/{fui-skill → fui}/design-md/voltagent/DESIGN.md +0 -0
  197. /package/skills/{fui-skill → fui}/design-md/voltagent/README.md +0 -0
  198. /package/skills/{fui-skill → fui}/design-md/warp/DESIGN.md +0 -0
  199. /package/skills/{fui-skill → fui}/design-md/warp/README.md +0 -0
  200. /package/skills/{fui-skill → fui}/design-md/webflow/DESIGN.md +0 -0
  201. /package/skills/{fui-skill → fui}/design-md/webflow/README.md +0 -0
  202. /package/skills/{fui-skill → fui}/design-md/wired/DESIGN.md +0 -0
  203. /package/skills/{fui-skill → fui}/design-md/wired/README.md +0 -0
  204. /package/skills/{fui-skill → fui}/design-md/wise/DESIGN.md +0 -0
  205. /package/skills/{fui-skill → fui}/design-md/wise/README.md +0 -0
  206. /package/skills/{fui-skill → fui}/design-md/x.ai/DESIGN.md +0 -0
  207. /package/skills/{fui-skill → fui}/design-md/x.ai/README.md +0 -0
  208. /package/skills/{fui-skill → fui}/design-md/zapier/DESIGN.md +0 -0
  209. /package/skills/{fui-skill → fui}/design-md/zapier/README.md +0 -0
  210. /package/skills/{fui-skill → fui}/examples/f-table-patterns.json +0 -0
  211. /package/skills/{fui-skill → fui}/examples/module-patterns.json +0 -0
  212. /package/skills/{fui-skill → fui}/examples/project-patterns.json +0 -0
  213. /package/skills/{fui-skill → fui}/references/coding-standards.md +0 -0
  214. /package/skills/{fui-skill → fui}/references/db-table-design.md +0 -0
  215. /package/skills/{fui-skill → fui}/references/fsheet.md +0 -0
  216. /package/skills/{fui-skill → fui}/references/tapi-file-api.md +0 -0
  217. /package/skills/{fui-skill → fui}/references/ui-templates.md +0 -0
  218. /package/skills/{fui-skill → fui}/scripts/fechart.js +0 -0
  219. /package/skills/{fui-skill → fui}/scripts/fsheet.js +0 -0
@@ -1,1051 +1,1051 @@
1
- $isMobile = navigator.userAgent.match(/mobile|touch|android|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|ip(ad|hone|od)|iris|kindle|lge |maemo|meego.+mobile|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino|playbook|silk/i) != null;
2
- $isPhone = navigator.userAgent.match(/mobile|touch|android|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|ip(hone|od)|iris|kindle|lge |maemo|meego.+mobile|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (phone)|xda|xiino|playbook|silk/i) != null;
3
- $isApple = navigator.userAgent.match(/ip(ad|hone|od)/i) != null;
4
- //===========================================================================
5
- var Loader = function () { }
6
- Loader.prototype = {
7
- require: function (scripts, callback) {
8
- this.loadCount = 0;
9
- this.totalRequired = scripts.length;
10
- this.callback = callback;
11
-
12
- for (var i = 0; i < scripts.length; i++) {
13
- this.writeScript(scripts[i]);
14
- }
15
- },
16
- loaded: function (evt) {
17
- this.loadCount++;
18
-
19
- if (this.loadCount == this.totalRequired && typeof this.callback == 'function') this.callback.call();
20
- },
21
- writeScript: function (src) {
22
- var self = this;
23
- var s = document.createElement('script');
24
- s.type = "text/javascript";
25
- s.async = false;
26
- s.src = src;
27
- s.addEventListener('load', function (e) { self.loaded(e); }, false);
28
- var head = document.getElementsByTagName('head')[0];
29
- head.appendChild(s);
30
- }
31
- };
32
- //===================================================================================
33
- //===================================================================================
34
- //===========================================================================================
35
- function extractHostname(url) {
36
- var hostname;
37
- //find & remove protocol (http, ftp, etc.) and get hostname
38
-
39
- if (url.indexOf("//") > -1) {
40
- hostname = url.split('/')[2];
41
- } else {
42
- hostname = url.split('/')[0];
43
- }
44
-
45
- //find & remove port number
46
- hostname = hostname.split(':')[0];
47
- //find & remove "?"
48
- hostname = hostname.split('?')[0];
49
-
50
- return hostname;
51
- }
52
- //===========================================================================================
53
- const getDomainWithoutSubdomain = url => {
54
- const urlParts = new URL(url).hostname.split('.')
55
-
56
- return urlParts
57
- .slice(0)
58
- .slice(-(urlParts.length === 4 ? 3 : 2))
59
- .join('.')
60
- }
61
- //===========================================================================================
62
- function loadScripts(scriptsArray, callbackFunc) {
63
- var scriptsArray = scriptsArray || new Array();
64
- var callbackFunc = callbackFunc || function () { };
65
-
66
- if (scriptsArray.length == 0) {
67
- callbackFunc();
68
- return;
69
- }
70
-
71
- var loadScript = new Loader();
72
- loadScript.require(scriptsArray, callbackFunc);
73
- }
74
- //===================================================================================
75
- function pushRouter(router) {
76
- var curPath = window.location.pathname;
77
- if (curPath != router || curPath == '/') {
78
- window.history.pushState({ id: router }, document.title, router);
79
- }
80
- }
81
- //===================================================================================
82
- function CALL(obj, includeData) {
83
- runAction(obj, includeData);
84
- }
85
- //===================================================================================
86
- function windowSendMessage(window, cmd, data) {
87
- //console.log("windowSendMessage", cmd, data);
88
- if (_.isObject(window)) {
89
- window.postMessage({ cmd: cmd, value: JSON.parse(JSON.stringify(data)) }, '*');
90
- } else
91
- if (window == "#PARENT") {
92
- parent.postMessage({ cmd: cmd, value: JSON.parse(JSON.stringify(data)) }, '*');
93
- } else {
94
- var win = document.getElementById(window.slice(1, window.length));
95
- if (win) win.contentWindow.postMessage({ cmd: cmd, value: JSON.parse(JSON.stringify(data)) }, '*');
96
- else console.log("send DATA: Window ID not found")
97
- }
98
- }
99
- //===========================================================================================
100
- function appCommand(cmdData) {
101
- if (window.ReactNativeWebView) window.ReactNativeWebView.postMessage(cmdData);
102
- else if (window.webkit) window.webkit.messageHandlers["scriptHandler"].postMessage(cmdData);
103
- else if (window.scriptHandler) window.scriptHandler.postMessage(cmdData);
104
- }
105
- //===========================================================================================
106
- function buildHeader(obj, mapCol) {
107
- var datatable = {};
108
- var header = [];
109
-
110
- if (mapCol) {
111
- _.forEach(obj, function (o) {
112
- header.push({ "text": o[mapCol.text], "value": o[mapCol.value], "divider": true });
113
- });
114
- } else {
115
- if (obj && _.isArray(obj)) datatable = obj[0];
116
- else if (obj && _.isArray(obj.data)) datatable = obj.data[0];
117
-
118
- if (datatable) {
119
- _.forEach(datatable, function (value, key) {
120
- if (_.isNumber(value)) header.push({ "el": "t-num", "text": key, "value": key, "divider": true });
121
- else header.push({ "text": key, "value": key, "divider": true });
122
- });
123
- }
124
- }
125
-
126
- console.log("Build Header: ", header)
127
- return header;
128
- }
129
- //===========================================================================================
130
- function fillData(obj) {
131
- if (!obj.key && !obj.src) { // FILL ALL DATA
132
- var mapD = {};
133
- _.forEach(obj.map, function (value, key) {
134
- mapD[key] = bindData(value);
135
- });
136
-
137
- _.forEach(obj.des, function (desObj) {
138
- _.forEach(mapD, function (value, key) {
139
- desObj[key] = value;
140
- });
141
- });
142
- return
143
- }
144
-
145
- var GroupKeySRC = _.groupBy(obj.src, obj.key);
146
- _.forEach(obj.map, function (value, key) {
147
- _.forEach(obj.des, function (desObj) {
148
- var val = GroupKeySRC[desObj[obj.key]];
149
- if (_.isString(value)) {
150
- if (_.isArray(val)) val = val[0][value];
151
- else val = null;
152
- }
153
- else if (_.isPlainObject(value)) {
154
- if (_.isArray(val)) val = val[0];
155
- else val = null;
156
- }
157
- desObj[key] = val;
158
- });
159
- });
160
- }
161
- //===========================================================================================
162
- function groupBy(arrayObj, groupField, sumArr) { // vi du: groupBy(dsSinhVien, 'Khoa', ['SL Nghỉ Học','SL Tốt Nghiệp'])
163
- var gArr = _.groupBy(arrayObj, groupField);
164
- var returnArr = [];
165
- var sumObj = {};
166
- if (!_.isArray(sumArr)) sumArr = [sumArr];
167
- //---------------------------------
168
- _.forEach(gArr, function (fieldValue, fieldKey) {
169
- var n = 0;
170
- _.forEach(sumArr, function (v) {
171
- sumObj[v] = 0;
172
- });
173
- _.forEach(fieldValue, function (value) {
174
- n++;
175
- _.forEach(sumArr, function (v) {
176
- sumObj[v] = sumObj[v] + value[v];
177
- });
178
- });
179
- var o = {};
180
- o[groupField] = fieldKey;
181
- o["count"] = n;
182
- o = $.extend(o, sumObj);
183
- returnArr.push(o);
184
- });
185
- return returnArr;
186
- }
187
- //===========================================================================================
188
- function chartDataBuild(obj) {
189
- if (obj.dataChart) {
190
- var chartData = [];
191
- var dataInput = obj.dataChart.data;
192
-
193
- if (_.isUndefined(dataInput)) dataInput = obj.dataChart;
194
-
195
- if (_.isArray(dataInput[0])) {
196
- var chartName = JSON.parse(obj.dataChart.chartName);
197
- _.forEach(dataInput, function (d, k) {
198
- chartData.push({
199
- name: chartName[k],
200
- data: _.map(dataInput[k], function (n) {
201
- return [n.label, n.value];
202
- })
203
- });
204
- });
205
- } else {
206
- chartData.push({
207
- name: (obj.dataChart.chartName ? obj.dataChart.chartName : null),
208
- data: _.map(dataInput, function (n) {
209
- return [n.label, n.value];
210
- })
211
- });
212
- }
213
- //---------------------
214
- if (obj.allInOne) return [{ name: "", data: chartData }];
215
- else return chartData;
216
- }
217
- }
218
- //===========================================================================================
219
- function chartQABuild(obj) {
220
- if (obj.dataChart) {
221
- var chartData = [];
222
-
223
- _.forEach(obj.dataChart[0], function (v, i) { // Duyet tung cau hoi
224
- if (v.QType == 'TextArea') {
225
-
226
- } else {
227
- chartData.push({
228
- name: v.QText,
229
- data: _.map(_.filter(obj.dataChart[1], ['QID', v.QID]), function (n) {
230
- return [n.label, n.val];
231
- })
232
- });
233
- }
234
- });
235
- return chartData;
236
- }
237
- }
238
- //===========================================================================================
239
- function confirm(obj) {
240
- $.confirm({
241
- icon: (obj.icon ? obj.icon : 'mdi mdi-help-circle-outline'),
242
- title: obj.title,
243
- content: (obj.message ? obj.message : ''),
244
- type: (obj.type ? obj.type : 'blue'),
245
- columnClass: 'col-xs-12 col-sm-8 col-md-6 col-lg-4',
246
- animateFromElement: false,
247
- buttons: {
248
- ok: {
249
- text: '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;OK&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;',
250
- btnClass: 'btn-blue',
251
- keys: ['enter'],
252
- action: obj.action
253
- },
254
- cancel: (obj.cancel ? obj.cancel : function () { })
255
- }
256
- });
257
- }
258
- //===========================================================================================
259
- function showMessage(obj) {
260
- $.dialog({
261
- icon: (obj.icon ? obj.icon : 'mdi mdi-information-variant-circle-outline'),
262
- title: (obj.title ? obj.title : ''),
263
- backgroundDismiss: true,
264
- content: (obj.message ? obj.message : ''),
265
- type: (obj.type ? obj.type : 'blue'),
266
- columnClass: 'col-xs-12 col-sm-8 col-md-6 col-lg-4',
267
- animateFromElement: false,
268
- closeIcon: true,
269
- buttons: {
270
- ok: {
271
- text: '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Close&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;',
272
- btnClass: 'btn-' + (obj.type ? obj.type : 'blue')
273
- }
274
- },
275
- onClose: function () {
276
- if (obj.onclose) {
277
- runAction(obj.onclose, { $row: obj.$row, item: obj.item });
278
- }
279
- },
280
- });
281
- }
282
- //===========================================================================================
283
- function rightTest(rightObject) {
284
- var returnVal = true
285
- _.forEach(rightObject, function (rV, rK) {
286
- if (!returnVal) return;
287
- var A1 = [];
288
- var A2 = [];
289
-
290
- if (_.isArray(rV)) A1 = rV;
291
- else A1 = [rV];
292
-
293
- if (_.isArray(vueData.user[rK])) A2 = vueData.user[rK];
294
- else A2 = [vueData.user[rK]];
295
-
296
- if (_.intersection(A1, A2).length <= 0) returnVal = false;
297
- });
298
- return returnVal;
299
- }
300
- //===========================================================================================
301
- function openWindow(obj) {
302
- if (window === top) {
303
- setTimeout(function () {
304
- if (obj.id != undefined && vueData[obj.id] != undefined) {
305
- console.log("Cửa sổ bị trùng tên: ", obj.id)
306
- return;
307
- }
308
-
309
- if (!vueData.v_windowVM) vueData.v_windowVM = {};
310
- if (_.isString(obj)) obj = { url: obj };
311
-
312
- if (!obj.id) obj.id = "Window" + generateID();
313
- obj.open = true;
314
- vueData[obj.id] = obj;
315
- vueData.v_windowVM[obj.id] = new Vue({
316
- vuetify: new Vuetify(),
317
- render: Vue.compile("<f-window id=\"" + obj.id + "\"></f-window>").render,
318
- }).$mount();
319
- }, 10);
320
- }
321
- else {
322
- windowSendMessage('#PARENT', 'openWindow', obj);
323
- }
324
- }
325
- //===========================================================================================
326
- function redirect(obj, query) {
327
- let Q = '';
328
- if (obj && obj.url) Q = obj.url;
329
- else Q = obj;
330
-
331
- if ((query || (obj && obj.query)) && vueData.p_params != " ") {
332
- if (Q.indexOf("?") < 0) Q = Q + vueData.p_params;
333
- else Q = Q + vueData.p_params.replace("?", "");
334
- }
335
-
336
- location.assign(Q);
337
- }
338
- //===========================================================================================
339
- function reload() {
340
- location.assign(window.location.href);
341
- }
342
- //===========================================================================================
343
- function findInArray(obj) {
344
- if (_.isString(obj.value)) obj.value = JSON.parse(obj.value);
345
- return _.find(obj.array, obj.value);
346
- }
347
- //===========================================================================================
348
- function fixURL(url) {
349
- if (url.indexOf('http') == 0) return url;
350
- else return vueData.v_Set.apiDomain + url
351
- }
352
-
353
- //===========================================================================================
354
- function stringAttrToJson(str, removeVueEvent) {
355
- var jObj = {}
356
-
357
- if (typeof str == 'undefined') str = "";
358
-
359
- if (_.isPlainObject(str)) return { ...str };
360
-
361
- $(`<div ${str} >`).each(function () {
362
- $.each(this.attributes, function (i, a) {
363
- if (removeVueEvent && (typeof a.name == "string" && (a.name.substring(0, 1) == ":" || a.name.substring(0, 1) == "@" || a.name.substring(0, 2) == "v-"))) return;
364
- jObj[a.name] = a.value;
365
- })
366
- })
367
- return jObj
368
- }
369
-
370
- //===========================================================================================
371
- function json_data_parse(obj, level) {
372
- //--- convert to json data array --------
373
- if (level > 3) return;
374
-
375
- level++;
376
- if (_.isArray(obj) && obj.length > 0) {
377
- //console.log("json_data_parse", level)
378
- if (_.isArray(obj[0])) {
379
- _.forEach(obj, function (value) {
380
- json_data_parse(value, level);
381
- });
382
- } else if (_.isPlainObject(obj[0])) {
383
- var R = obj[0];
384
- var nameOfJson = [];
385
- _.forEach(R, function (value, key) {
386
- if (key.substring(0, 10) == 'json_data:') nameOfJson.push(key);
387
- });
388
- _.forEach(obj, function (row) {
389
- _.forEach(nameOfJson, function (key) {
390
- row[key.replace('json_data:', '')] = JSON.parse(row[key]);
391
- delete row[key];
392
- });
393
- });
394
- }
395
- } else if (_.isPlainObject(obj)) {
396
- _.forEach(obj, function (value, key) {
397
- if (key.substring(0, 10) == 'json_data:') {
398
- obj[key.replace('json_data:', '')] = JSON.parse(value);
399
- delete obj[key];
400
- } else {
401
- if (_.isObject(value)) json_data_parse(value, level);
402
- }
403
- });
404
- }
405
- }
406
- //===========================================================================================
407
- function ajaxCALL(URL, DATA, callBack, errorCallBack, header) {
408
- let loadingID = generateID();
409
- vueData.v_LoadingOverlay.push(loadingID);
410
- vueData.v_Loading = true;
411
-
412
- $.ajax({
413
- type: "POST",
414
- headers: (header ? header : {
415
- authorization: $awt
416
- }),
417
- url: fixURL(URL),
418
- contentType: "application/json; charset=utf-8",
419
- dataType: "json",
420
- crossDomain: true,
421
- data: JSON.stringify(DATA),
422
- success: function (d) {
423
- json_data_parse(d, 0);
424
- if (callBack) callBack(d);
425
- },
426
- error: function (xhr, ajaxOptions, thrownError) {
427
- console.log("API:", URL, "IN:", DATA);
428
- console.log("Error XHR:", xhr);
429
-
430
- if (errorCallBack) {
431
- errorCallBack(xhr);
432
- return
433
- }
434
-
435
- showMessage({
436
- icon: 'mdi mdi-alert-circle-outline',
437
- title: '',
438
- message: (xhr.responseJSON ? xhr.responseJSON.Message : (xhr.responseText ? xhr.responseText : "")),
439
- type: 'red',
440
- });
441
- },
442
- complete: function (data) {
443
- vueData.v_LoadingOverlay = _.without(vueData.v_LoadingOverlay, loadingID);
444
- if (vueData.v_LoadingOverlay.length == 0) vueData.v_Loading = false;
445
- }
446
- });
447
- }
448
- //===========================================================================================
449
- function capacityText(numb) {
450
- if (numb < 1048576) return Math.round(numb / 1024 * 100 / 100) + ' Kb';
451
- else return Math.round(numb / 1048576 * 100) / 100 + ' Mb';
452
- }
453
- //===========================================================================================
454
- function generateID() {
455
- var d = new Date();
456
- var k = d.getTime();
457
- var str = k.toString(16).slice(1)
458
- var UUID = 'idxxxxxxxx'.replace(/[xy]/g, function (c) {
459
- var r = Math.random() * 16 | 0;
460
- v = c == 'x' ? r : (r & 3 | 8);
461
- return v.toString(16);
462
- });
463
- var newString = UUID.replace(/[z]/, str)
464
- return newString;
465
- }
466
- //===========================================================================================
467
- function printPDF(obj) {
468
- pdfMake.fonts = {
469
- Roboto: {
470
- normal: 'Roboto-Regular.ttf',
471
- bold: 'Roboto-Medium.ttf',
472
- italics: 'Roboto-Italic.ttf',
473
- bolditalics: 'Roboto-MediumItalic.ttf'
474
- },
475
- TimesNewRoman: {
476
- normal: 'times.ttf',
477
- bold: 'timesbd.ttf',
478
- italics: 'timesi.ttf',
479
- bolditalics: 'timesbi.ttf'
480
- }
481
- }
482
-
483
- if (obj.download) {
484
- pdfMake.createPdf(obj.data).download('fp_pdf_' + generateID() + '.pdf');
485
- } else if (obj.viewer) {
486
- pdfMake.createPdf(obj.data).getDataUrl(function (outDoc) {
487
- $(obj.viewer).attr("src", outDoc);
488
- });
489
- } else {
490
- pdfMake.createPdf(obj.data).getBase64(function (outDoc) {
491
- printJS({ printable: outDoc, type: 'pdf', base64: true });
492
- });
493
- }
494
- //-------------------------------------------------------
495
- if (_.isFunction(obj.callBack)) obj.callBack();
496
- }
497
-
498
- //===========================================================================================
499
- function colorLib(color) {
500
- // Hàm hỗ trợ phân tích và chuyển đổi màu
501
- function parseColor(input) {
502
- let r, g, b, a = 1;
503
-
504
- // Xử lý HEX (3 hoặc 6 ký tự)
505
- if (typeof input === 'string' && input.startsWith('#')) {
506
- let hex = input.replace('#', '');
507
- if (hex.length === 3) {
508
- hex = hex.split('').map(c => c + c).join('');
509
- }
510
- r = parseInt(hex.slice(0, 2), 16);
511
- g = parseInt(hex.slice(2, 4), 16);
512
- b = parseInt(hex.slice(4, 6), 16);
513
- return { r, g, b, a };
514
- }
515
-
516
- // Xử lý RGB/RGBA
517
- if (typeof input === 'string' && input.startsWith('rgb')) {
518
- const match = input.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
519
- if (match) {
520
- r = parseInt(match[1]);
521
- g = parseInt(match[2]);
522
- b = parseInt(match[3]);
523
- a = match[4] ? parseFloat(match[4]) : 1;
524
- return { r, g, b, a };
525
- }
526
- }
527
-
528
- // Xử lý HSL
529
- if (typeof input === 'string' && input.startsWith('hsl')) {
530
- const match = input.match(/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*([\d.]+))?\)/);
531
- if (match) {
532
- const h = parseInt(match[1]) / 360;
533
- const s = parseInt(match[2]) / 100;
534
- const l = parseInt(match[3]) / 100;
535
- a = match[4] ? parseFloat(match[4]) : 1;
536
- return hslToRgb(h, s, l, a);
537
- }
538
- }
539
-
540
- // Mặc định trả về màu đen nếu không phân tích được
541
- return { r: 0, g: 0, b: 0, a: 1 };
542
- }
543
-
544
- // Chuyển HSL sang RGB
545
- function hslToRgb(h, s, l, a) {
546
- let r, g, b;
547
- if (s === 0) {
548
- r = g = b = l; // Grayscale
549
- } else {
550
- const hue2rgb = (p, q, t) => {
551
- if (t < 0) t += 1;
552
- if (t > 1) t -= 1;
553
- if (t < 1 / 6) return p + (q - p) * 6 * t;
554
- if (t < 1 / 2) return q;
555
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
556
- return p;
557
- };
558
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
559
- const p = 2 * l - q;
560
- r = hue2rgb(p, q, h + 1 / 3);
561
- g = hue2rgb(p, q, h);
562
- b = hue2rgb(p, q, h - 1 / 3);
563
- }
564
- return {
565
- r: Math.round(r * 255),
566
- g: Math.round(g * 255),
567
- b: Math.round(b * 255),
568
- a
569
- };
570
- }
571
-
572
- // Chuyển RGB sang HSL
573
- function rgbToHsl(r, g, b) {
574
- r /= 255;
575
- g /= 255;
576
- b /= 255;
577
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
578
- let h, s, l = (max + min) / 2;
579
-
580
- if (max === min) {
581
- h = s = 0; // Grayscale
582
- } else {
583
- const d = max - min;
584
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
585
- switch (max) {
586
- case r: h = (g - b) / d + (g < b ? 6 : 0); break;
587
- case g: h = (b - r) / d + 2; break;
588
- case b: h = (r - g) / d + 4; break;
589
- }
590
- h /= 6;
591
- }
592
- return { h, s, l };
593
- }
594
-
595
- // Hàm chuyển đổi số thành chuỗi hex 2 chữ số
596
- function toHex(value) {
597
- const hex = Math.round(value).toString(16);
598
- return hex.length === 1 ? '0' + hex : hex;
599
- }
600
-
601
- // Phân tích màu đầu vào
602
- const { r, g, b, a } = parseColor(color);
603
-
604
- // Đối tượng trả về với các phương thức
605
- return {
606
- // Trả về chuỗi HEX
607
- hexString() {
608
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
609
- },
610
-
611
- // Trả về chuỗi RGB hoặc RGBA
612
- rgbString() {
613
- return a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`;
614
- },
615
-
616
- // Trả về chuỗi HSL hoặc HSLA
617
- hslString() {
618
- const { h, s, l } = rgbToHsl(r, g, b);
619
- return a < 1
620
- ? `hsla(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%, ${a})`
621
- : `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
622
- },
623
-
624
- // Làm sáng màu (factor: 0 đến 1)
625
- lighten(factor) {
626
- const { h, s, l } = rgbToHsl(r, g, b);
627
- const newL = Math.min(1, l + l * factor);
628
- return colorLib(hslToRgb(h, s, newL, a).rgbString());
629
- },
630
-
631
- // Làm tối màu (factor: 0 đến 1)
632
- darken(factor) {
633
- const { h, s, l } = rgbToHsl(r, g, b);
634
- const newL = Math.max(0, l - l * factor);
635
- return colorLib(hslToRgb(h, s, newL, a).rgbString());
636
- },
637
-
638
- // Đặt độ trong suốt
639
- alpha(value) {
640
- return colorLib(`rgba(${r}, ${g}, ${b}, ${value})`);
641
- }
642
- };
643
- }
644
- //===========================================================================================
645
- function transparentize(value, opacity) {
646
- var alpha = opacity === undefined ? 0.5 : 1 - opacity;
647
- return colorLib(value).alpha(alpha).rgbString();
648
- }
649
- //===========================================================================================
650
- function jsonToExcel(Obj) {
651
- if (typeof ExcelJS == 'undefined') {
652
- var loadScript = new Loader();
653
- loadScript.require(["/include/excel/exceljs.min.js"], function () {
654
- writeExcelFile(Obj);
655
- });
656
- } else writeExcelFile(Obj);
657
- function writeExcelFile(inputObj) {
658
- var props = {
659
- data: null, // Json to download (for single sheet - backward compatibility)
660
- filename: "dataFP.xlsx", // filename to export
661
- worksheet: "SheetFP", // sheet name for single sheet
662
- sheets: null, // New property for multiple sheets
663
- // optional: [{header, key, type?: 'number'|'date'|'html', numFmt?, width?, sum?: true|numFmt}]
664
- // Có columns thì cột/tiêu đề/định dạng theo đúng danh sách này (dùng cho f-table excel="UI");
665
- // không có thì giữ hành vi cũ: cột = key của dòng đầu, giá trị thô. Mỗi sheet có thể có columns riêng.
666
- columns: null,
667
- onProgress: null, // optional: function({percent, processedRows, totalRows, phase, sheetName}) - phase: 'building'|'finalizing'|'done'|'error'
668
- showProgress: true // tự hiện thanh % cố định giữa màn hình khi xuất; truyền false để tắt UI này
669
- }
670
- Object.assign(props, inputObj);
671
- // Gom về một danh sách sheet chung cho cả 2 chế độ cũ (single) và mới (multi) để dùng chung logic tiến trình/chunk
672
- var sheetList;
673
- if (props.sheets && Array.isArray(props.sheets)) {
674
- if (!props.sheets.length) { showMessage({ title: "Sheets data is empty" }); return; }
675
- sheetList = props.sheets.filter(function (s) { return s.data && s.data.length; });
676
- if (!sheetList.length) { showMessage({ title: "Sheets data is empty" }); return; }
677
- } else {
678
- if (!props.data || !props.data.length) { showMessage({ title: "Data Null" }); return; }
679
- sheetList = [{ name: props.worksheet, data: props.data, columns: props.columns }];
680
- }
681
-
682
- var totalRows = sheetList.reduce(function (sum, s) { return sum + s.data.length; }, 0);
683
- var processedRows = 0;
684
- // Chunk động: mỗi đợt ghi ~2% tổng số dòng, nên số lần nhường luồng UI luôn ~50 lần dù
685
- // data ít hay nhiều (không còn đánh đổi mượt-vs-nhanh như chunk cố định) — data nhỏ thì
686
- // chặn dưới 50 dòng/đợt để khỏi nhường luồng vô ích cho vài chục dòng.
687
- var CHUNK_SIZE = Math.max(50, Math.ceil(totalRows * 0.02));
688
- var progressUI = props.showProgress ? createProgressUI() : null;
689
-
690
- function report(phase, sheetName) {
691
- // Giai đoạn ghi dòng ("building") chiếm tối đa 95%; ExcelJS không có hook tiến trình cho bước
692
- // nén file (writeBuffer) nên "finalizing" chỉ báo mốc cố định 98%, không nội suy %.
693
- var percent = totalRows ? Math.min(95, Math.floor(processedRows / totalRows * 95)) : 0;
694
- if (phase === 'finalizing') percent = 98;
695
- if (phase === 'done') percent = 100;
696
- if (typeof props.onProgress === 'function') {
697
- try { props.onProgress({ percent: percent, processedRows: processedRows, totalRows: totalRows, phase: phase, sheetName: sheetName }); }
698
- catch (e) { console.error('jsonToExcel onProgress error', e); }
699
- }
700
- if (progressUI) {
701
- progressUI.update({ percent: percent, phase: phase });
702
- if (phase === 'done' || phase === 'error') progressUI.destroy();
703
- }
704
- }
705
-
706
- var workbook = new ExcelJS.Workbook();
707
-
708
- function buildSheet(sheetIndex) {
709
- if (sheetIndex >= sheetList.length) { finalize(); return; }
710
- var sheetCfg = sheetList[sheetIndex];
711
- var worksheet = workbook.addWorksheet(sheetCfg.name || 'Sheet');
712
- var cols = Array.isArray(sheetCfg.columns) && sheetCfg.columns.length ? sheetCfg.columns : null;
713
- // Create header (chỉ cần đọc key của dòng đầu, tương đương logic cũ nhưng không lặp check index===0 mỗi vòng)
714
- if (cols) setupColumns(worksheet, cols, sheetCfg.data);
715
- else worksheet.columns = Object.keys(sheetCfg.data[0]).map(function (k) { return { header: k, key: k }; });
716
-
717
- var rows = sheetCfg.data;
718
- var i = 0;
719
- function nextChunk() {
720
- try {
721
- var end = Math.min(i + CHUNK_SIZE, rows.length);
722
- for (var r = i; r < end; r++) { worksheet.addRow(cols ? columnRowValues(cols, rows[r]) : rows[r]); }
723
- if (cols && end === rows.length) appendSumRow(worksheet, cols, rows);
724
- processedRows += (end - i);
725
- i = end;
726
- report('building', sheetCfg.name);
727
- } catch (err) {
728
- console.error('jsonToExcel build error', err);
729
- report('error', sheetCfg.name);
730
- showMessage({ title: 'Xuất Excel thất bại' });
731
- return;
732
- }
733
- if (i < rows.length) {
734
- // Nhường luồng UI giữa các đợt (thay vì ghi hết trong 1 vòng lặp đồng bộ) để tránh
735
- // trang bị "Not Responding" khi data lớn, đồng thời cho phép % cập nhật lên màn hình.
736
- (window.requestAnimationFrame || function (cb) { setTimeout(cb, 0); })(nextChunk);
737
- } else {
738
- buildSheet(sheetIndex + 1);
739
- }
740
- }
741
- nextChunk();
742
- }
743
-
744
- function finalize() {
745
- report('finalizing');
746
- workbook.xlsx.writeBuffer().then(function (data) {
747
- var blobType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
748
- var blob = new Blob([data], { type: blobType });
749
- var a = document.createElement('a');
750
- document.body.appendChild(a);
751
- a.style = 'display: none';
752
- var url = window.URL.createObjectURL(blob);
753
- a.href = url;
754
- a.download = props.filename;
755
- a.click();
756
- window.URL.revokeObjectURL(url);
757
- a.remove();
758
- report('done');
759
- }).catch(function (err) {
760
- console.error('jsonToExcel writeBuffer error', err);
761
- report('error');
762
- showMessage({ title: 'Xuất Excel thất bại' });
763
- });
764
- }
765
-
766
- buildSheet(0);
767
- }
768
-
769
- //---------------------------------------------------- chế độ `columns`
770
- function readColumnValue(row, key) {
771
- if (row === null || row === undefined) return null;
772
- if (row[key] !== undefined) return row[key];
773
- return _.get(row, key, null);
774
- }
775
- // ExcelJS ghi Date theo UTC → dựng lại mốc UTC từ giờ ĐỊA PHƯƠNG, nếu không ngày 00:00 ở UTC+7
776
- // sẽ lùi thành 17:00 hôm trước trong Excel. "yyyy-mm-dd" trần thì new Date() hiểu là UTC nên tách riêng.
777
- function toExcelDate(v) {
778
- if (v === null || v === undefined || v === '') return null;
779
- var m = typeof v === 'string' && /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
780
- if (m) return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
781
- var d = v instanceof Date ? v : new Date(v);
782
- if (isNaN(d.getTime())) return v;
783
- return new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()));
784
- }
785
- function htmlToText(v) {
786
- var s = String(v);
787
- if (s.indexOf('<') < 0 && s.indexOf('&') < 0) return s;
788
- // DOMParser không chạy script/handler — innerHTML trên element sống thì <img onerror> VẪN chạy.
789
- return new DOMParser().parseFromString(s, 'text/html').body.textContent;
790
- }
791
- function convertCellValue(col, v) {
792
- if (v === undefined) return null;
793
- if (col.type === 'date') return toExcelDate(v);
794
- if (v === null) return null;
795
- if (col.type === 'number') return (typeof v === 'string' && v.trim() !== '' && !isNaN(v)) ? Number(v) : v;
796
- if (col.type === 'html') return htmlToText(v);
797
- // object/array đưa thẳng vào ExcelJS sẽ bị hiểu nhầm là rich text/hyperlink/formula
798
- if (typeof v === 'object' && !(v instanceof Date)) return JSON.stringify(v);
799
- return v;
800
- }
801
- function columnRowValues(cols, row) {
802
- var out = new Array(cols.length);
803
- for (var c = 0; c < cols.length; c++) out[c] = convertCellValue(cols[c], readColumnValue(row, cols[c].key));
804
- return out;
805
- }
806
- function estimateWidth(col, rows) {
807
- var w = String(col.header != null ? col.header : col.key).length;
808
- if (col.type === 'date') w = Math.max(w, String(col.numFmt || 'dd/mm/yyyy').replace(/"/g, '').length);
809
- else {
810
- for (var i = 0, n = Math.min(rows.length, 100); i < n; i++) {
811
- var v = readColumnValue(rows[i], col.key);
812
- if (v !== null && v !== undefined) w = Math.max(w, String(v).length + (typeof v === 'number' ? 3 : 0));
813
- }
814
- }
815
- return Math.min(60, Math.max(8, w + 2));
816
- }
817
- function setupColumns(worksheet, cols, rows) {
818
- // key nội bộ theo vị trí: hai cột trùng tiêu đề/trùng field vẫn không đè nhau (dòng ghi bằng mảng)
819
- worksheet.columns = cols.map(function (c, idx) {
820
- var def = { header: c.header != null ? String(c.header) : String(c.key), key: '__c' + idx, width: c.width || estimateWidth(c, rows) };
821
- if (c.numFmt) def.style = { numFmt: c.numFmt };
822
- return def;
823
- });
824
- worksheet.getRow(1).font = { bold: true };
825
- worksheet.views = [{ state: 'frozen', ySplit: 1 }];
826
- }
827
- // Hàng tổng = công thức SUM trên chính các dòng đã xuất (kèm kết quả tính sẵn để trình xem không
828
- // tính công thức vẫn thấy số) — sửa số trong Excel thì tổng tự cập nhật.
829
- function appendSumRow(worksheet, cols, rows) {
830
- if (!rows.length || !cols.some(function (c) { return c.sum; })) return;
831
- var row = worksheet.addRow(cols.map(function (c, idx) {
832
- if (!c.sum) return null;
833
- var total = 0;
834
- for (var i = 0; i < rows.length; i++) {
835
- var n = Number(readColumnValue(rows[i], c.key));
836
- if (!isNaN(n)) total += n;
837
- }
838
- var letter = worksheet.getColumn(idx + 1).letter;
839
- return { formula: 'SUM(' + letter + '2:' + letter + (rows.length + 1) + ')', result: total };
840
- }));
841
- row.font = { bold: true };
842
- cols.forEach(function (c, idx) {
843
- if (typeof c.sum === 'string') row.getCell(idx + 1).numFmt = c.sum;
844
- });
845
- }
846
-
847
- // Thanh % mặc định, tự tạo/tự dọn bằng DOM thuần (CSS thủ công, không dùng v-dialog/framework nào)
848
- // — fix cố định giữa màn hình, không phụ thuộc Vue nên dùng chung được cho cả V2 và V3.
849
- function createProgressUI() {
850
- var STYLE_ID = 'fui-json-to-excel-progress-style';
851
- // Luôn ghi đè nội dung CSS mới nhất vào thẻ <style>, kể cả khi tag đã tồn tại từ lần
852
- // export trước trong cùng phiên trang (SPA không reload) — nếu chỉ tạo-1-lần-rồi-bỏ-qua
853
- // như trước, CSS cũ bị kẹt lại vĩnh viễn trong DOM, sửa file JS trên server sau đó
854
- // không có tác dụng cho tới khi người dùng F5 lại cả trang.
855
- var style = document.getElementById(STYLE_ID);
856
- if (!style) {
857
- style = document.createElement('style');
858
- style.id = STYLE_ID;
859
- document.head.appendChild(style);
860
- }
861
- style.innerHTML = '.fui-xlsx-progress{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:350px;' +
862
- 'box-sizing:border-box;z-index:99999;background:#fff;border-radius:8px;box-shadow:0 12px 40px rgba(0,0,0,.35);' +
863
- 'padding: 30px 30px 40px 30px;font:14px/1.4 inherit;transition:opacity .3s}' +
864
- '.fui-xlsx-progress .fui-xlsx-label{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;color:#333}' +
865
- '.fui-xlsx-progress .fui-xlsx-label span:first-child{font-size:15px;font-weight:600}' +
866
- '.fui-xlsx-progress .fui-xlsx-percent{font-size:22px;font-weight:700;color:#1976d2}' +
867
- '.fui-xlsx-progress .fui-xlsx-track{height:8px;border-radius:4px;background:#e0e0e0;overflow:hidden}' +
868
- '.fui-xlsx-progress .fui-xlsx-bar{height:100%;width:0;background:#1976d2;transition:width .2s ease-out}';
869
- var el = document.createElement('div');
870
- el.className = 'fui-xlsx-progress';
871
- el.innerHTML = '<div class="fui-xlsx-label"><span>Đang xuất Excel...</span><span class="fui-xlsx-percent">0%</span></div>' +
872
- '<div class="fui-xlsx-track"><div class="fui-xlsx-bar"></div></div>';
873
- document.body.appendChild(el);
874
- var percentEl = el.querySelector('.fui-xlsx-percent');
875
- var barEl = el.querySelector('.fui-xlsx-bar');
876
- var labelEl = el.querySelector('.fui-xlsx-label span');
877
- var destroyed = false;
878
- return {
879
- update: function (info) {
880
- if (destroyed) return;
881
- percentEl.textContent = info.percent + '%';
882
- barEl.style.width = info.percent + '%';
883
- if (info.phase === 'finalizing') labelEl.textContent = 'Đang tạo file...';
884
- if (info.phase === 'done') labelEl.textContent = 'Hoàn tất';
885
- if (info.phase === 'error') labelEl.textContent = 'Xuất thất bại';
886
- },
887
- destroy: function () {
888
- if (destroyed) return;
889
- destroyed = true;
890
- el.style.opacity = '0';
891
- setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 500);
892
- }
893
- };
894
- }
895
- }
896
- //===========================================================================================
897
- function copyToClipboard(textToCopy) {
898
- if (navigator.clipboard && window.isSecureContext) {
899
- console.log("Navigator copy: ", textToCopy);
900
- return navigator.clipboard.writeText(textToCopy);
901
- } else {
902
- let textArea = document.createElement("div");
903
- textArea.innerText = textToCopy;
904
- textArea.style.position = "fixed";
905
- textArea.style.left = "0px";
906
- textArea.style.top = "-999999px";
907
- document.body.appendChild(textArea);
908
-
909
- var range = document.createRange();
910
- range.selectNode(textArea);
911
- window.getSelection().removeAllRanges();
912
- window.getSelection().addRange(range);
913
- return new Promise((res, rej) => {
914
- document.execCommand('copy') ? res() : rej();
915
- console.log("Command copy: ", textToCopy);
916
- textArea.remove();
917
- });
918
- }
919
- };
920
- //===========================================================================================
921
- function fileNameClear(fname) {
922
- if (fname) return fname.replace(/[\!\@\#\$\^\&\%\*\(\)\+\=\-\[\]\\\/\{\}\|\:\<\>\?\,\.\`\"\'\~\;]/gi, '');
923
- return fname;
924
- }
925
- //===========================================================================================
926
- function hashCode(s) {
927
- var h = 0,
928
- l = s.length,
929
- i = 0;
930
- if (l > 0)
931
- while (i < l)
932
- h = (h << 5) - h + s.charCodeAt(i++) | 0;
933
- return h;
934
- };
935
- //===========================================================================================
936
- function getCookie(cname) {
937
- var name = cname + "=";
938
- var ca = document.cookie.split(';');
939
- for (var i = 0; i < ca.length; i++) {
940
- var c = ca[i];
941
- while (c.charAt(0) == ' ') {
942
- c = c.substring(1);
943
- }
944
- if (c.indexOf(name) == 0) {
945
- return c.substring(name.length, c.length);
946
- }
947
- }
948
- return "";
949
- }
950
-
951
- function setCookie(name, value, days, domain) {
952
- var expires = "";
953
- var domainString = "";
954
- if (days) {
955
- var date = new Date();
956
- date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
957
- expires = "; expires=" + date.toUTCString();
958
- }
959
- if (domain) {
960
- domainString = "; domain=" + domain
961
- }
962
- document.cookie = name + "=" + (value || "") + expires + domainString + ";path=/";
963
- }
964
- //===================================================================================
965
- function logout(cookiesName, domain, url) {
966
- if (!cookiesName) cookiesName = 'awt';
967
- if (typeof domain == "undefined" && vueData.v_Set.authDomain) domain = vueData.v_Set.authDomain;
968
-
969
- setCookie(cookiesName, '', 0, domain);
970
-
971
- if (!url) location.assign(location.href);
972
- else location.assign(url);
973
- }
974
-
975
- //===========================================================================================
976
- // WEBSOCKET
977
- //===========================================================================================
978
- function webSocketJoinGroup(groupObj, timeout, callBackFunc) {
979
- groupObj = _.cloneDeep(groupObj)
980
- _.forEach(groupObj, function (v, k) {
981
- if (!vueData.webSocket.hasOwnProperty(k)) {
982
- Vue.set(vueData.webSocket, k, v);
983
- }
984
- });
985
-
986
- setTimeout(function () {
987
- try {
988
- $.connection.hub.start().done(function () {
989
- _.forEach(groupObj, function (v, k) {
990
- $.connection.chatHub.server.joinGroup(k);
991
- });
992
- if (_.isFunction(callBackFunc)) callBackFunc();
993
- }).fail(function (e) {
994
- });
995
- }
996
- catch (err) {
997
-
998
- }
999
- }, (timeout ? timeout : 0));
1000
- }
1001
-
1002
- function webSocketConnection(wsURL, WS, wsState) {
1003
- $.connection.hub.url = wsURL;
1004
-
1005
- //=====================================================================
1006
- $.connection.hub.stateChanged(function (change) {
1007
- wsState.code = change.newState;
1008
- if (change.newState === $.signalR.connectionState.reconnecting) {
1009
- console.log("Websocket Reconnecting")
1010
- wsState.text = "Reconnecting";
1011
- }
1012
- else if (change.newState === $.signalR.connectionState.connected) {
1013
- console.log("Websocket Connected")
1014
- wsState.text = "Connected";
1015
- }
1016
- else if (change.newState === $.signalR.connectionState.disconnected) {
1017
- console.log("Websocket Disconnected")
1018
- wsState.text = "Disconnected";
1019
-
1020
- webSocketJoinGroup(WS, 5000);
1021
- }
1022
- });
1023
-
1024
- $.connection.chatHub.client.onError = function (Mess) {
1025
- console.log('Server error: ', Mess);
1026
- };
1027
-
1028
- $.connection.chatHub.client.onJoinGroup = function (mess) {
1029
- console.log("Group", mess)
1030
- };
1031
-
1032
- $.connection.chatHub.client.messageReceived = function (group, message) {
1033
- console.log("Message Received: ", group, message)
1034
- if (_.isArray(WS[group])) WS[group].push(message);
1035
- else WS[group] = message;
1036
- };
1037
-
1038
- $.connection.chatHub.client.messageSendLog = function (group, message) {
1039
- console.log("Message Sent: ", group, message)
1040
- };
1041
- }
1042
-
1043
- function webSocket_Send(obj) {
1044
- if (_.isPlainObject(obj.data)) $.connection.chatHub.server.messageSendObject(obj.group, obj.data);
1045
- else if (_.isArray(obj.data)) $.connection.chatHub.server.messageSendArray(obj.group, obj.data);
1046
- else $.connection.chatHub.server.messageSend(obj.group, obj.data);
1047
- }
1048
- //===========================================================================================
1049
- //===========================================================================================
1050
- //===========================================================================================
1051
- //===========================================================================================
1
+ $isMobile = navigator.userAgent.match(/mobile|touch|android|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|ip(ad|hone|od)|iris|kindle|lge |maemo|meego.+mobile|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino|playbook|silk/i) != null;
2
+ $isPhone = navigator.userAgent.match(/mobile|touch|android|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|ip(hone|od)|iris|kindle|lge |maemo|meego.+mobile|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (phone)|xda|xiino|playbook|silk/i) != null;
3
+ $isApple = navigator.userAgent.match(/ip(ad|hone|od)/i) != null;
4
+ //===========================================================================
5
+ var Loader = function () { }
6
+ Loader.prototype = {
7
+ require: function (scripts, callback) {
8
+ this.loadCount = 0;
9
+ this.totalRequired = scripts.length;
10
+ this.callback = callback;
11
+
12
+ for (var i = 0; i < scripts.length; i++) {
13
+ this.writeScript(scripts[i]);
14
+ }
15
+ },
16
+ loaded: function (evt) {
17
+ this.loadCount++;
18
+
19
+ if (this.loadCount == this.totalRequired && typeof this.callback == 'function') this.callback.call();
20
+ },
21
+ writeScript: function (src) {
22
+ var self = this;
23
+ var s = document.createElement('script');
24
+ s.type = "text/javascript";
25
+ s.async = false;
26
+ s.src = src;
27
+ s.addEventListener('load', function (e) { self.loaded(e); }, false);
28
+ var head = document.getElementsByTagName('head')[0];
29
+ head.appendChild(s);
30
+ }
31
+ };
32
+ //===================================================================================
33
+ //===================================================================================
34
+ //===========================================================================================
35
+ function extractHostname(url) {
36
+ var hostname;
37
+ //find & remove protocol (http, ftp, etc.) and get hostname
38
+
39
+ if (url.indexOf("//") > -1) {
40
+ hostname = url.split('/')[2];
41
+ } else {
42
+ hostname = url.split('/')[0];
43
+ }
44
+
45
+ //find & remove port number
46
+ hostname = hostname.split(':')[0];
47
+ //find & remove "?"
48
+ hostname = hostname.split('?')[0];
49
+
50
+ return hostname;
51
+ }
52
+ //===========================================================================================
53
+ const getDomainWithoutSubdomain = url => {
54
+ const urlParts = new URL(url).hostname.split('.')
55
+
56
+ return urlParts
57
+ .slice(0)
58
+ .slice(-(urlParts.length === 4 ? 3 : 2))
59
+ .join('.')
60
+ }
61
+ //===========================================================================================
62
+ function loadScripts(scriptsArray, callbackFunc) {
63
+ var scriptsArray = scriptsArray || new Array();
64
+ var callbackFunc = callbackFunc || function () { };
65
+
66
+ if (scriptsArray.length == 0) {
67
+ callbackFunc();
68
+ return;
69
+ }
70
+
71
+ var loadScript = new Loader();
72
+ loadScript.require(scriptsArray, callbackFunc);
73
+ }
74
+ //===================================================================================
75
+ function pushRouter(router) {
76
+ var curPath = window.location.pathname;
77
+ if (curPath != router || curPath == '/') {
78
+ window.history.pushState({ id: router }, document.title, router);
79
+ }
80
+ }
81
+ //===================================================================================
82
+ function CALL(obj, includeData) {
83
+ runAction(obj, includeData);
84
+ }
85
+ //===================================================================================
86
+ function windowSendMessage(window, cmd, data) {
87
+ //console.log("windowSendMessage", cmd, data);
88
+ if (_.isObject(window)) {
89
+ window.postMessage({ cmd: cmd, value: JSON.parse(JSON.stringify(data)) }, '*');
90
+ } else
91
+ if (window == "#PARENT") {
92
+ parent.postMessage({ cmd: cmd, value: JSON.parse(JSON.stringify(data)) }, '*');
93
+ } else {
94
+ var win = document.getElementById(window.slice(1, window.length));
95
+ if (win) win.contentWindow.postMessage({ cmd: cmd, value: JSON.parse(JSON.stringify(data)) }, '*');
96
+ else console.log("send DATA: Window ID not found")
97
+ }
98
+ }
99
+ //===========================================================================================
100
+ function appCommand(cmdData) {
101
+ if (window.ReactNativeWebView) window.ReactNativeWebView.postMessage(cmdData);
102
+ else if (window.webkit) window.webkit.messageHandlers["scriptHandler"].postMessage(cmdData);
103
+ else if (window.scriptHandler) window.scriptHandler.postMessage(cmdData);
104
+ }
105
+ //===========================================================================================
106
+ function buildHeader(obj, mapCol) {
107
+ var datatable = {};
108
+ var header = [];
109
+
110
+ if (mapCol) {
111
+ _.forEach(obj, function (o) {
112
+ header.push({ "text": o[mapCol.text], "value": o[mapCol.value], "divider": true });
113
+ });
114
+ } else {
115
+ if (obj && _.isArray(obj)) datatable = obj[0];
116
+ else if (obj && _.isArray(obj.data)) datatable = obj.data[0];
117
+
118
+ if (datatable) {
119
+ _.forEach(datatable, function (value, key) {
120
+ if (_.isNumber(value)) header.push({ "el": "t-num", "text": key, "value": key, "divider": true });
121
+ else header.push({ "text": key, "value": key, "divider": true });
122
+ });
123
+ }
124
+ }
125
+
126
+ console.log("Build Header: ", header)
127
+ return header;
128
+ }
129
+ //===========================================================================================
130
+ function fillData(obj) {
131
+ if (!obj.key && !obj.src) { // FILL ALL DATA
132
+ var mapD = {};
133
+ _.forEach(obj.map, function (value, key) {
134
+ mapD[key] = bindData(value);
135
+ });
136
+
137
+ _.forEach(obj.des, function (desObj) {
138
+ _.forEach(mapD, function (value, key) {
139
+ desObj[key] = value;
140
+ });
141
+ });
142
+ return
143
+ }
144
+
145
+ var GroupKeySRC = _.groupBy(obj.src, obj.key);
146
+ _.forEach(obj.map, function (value, key) {
147
+ _.forEach(obj.des, function (desObj) {
148
+ var val = GroupKeySRC[desObj[obj.key]];
149
+ if (_.isString(value)) {
150
+ if (_.isArray(val)) val = val[0][value];
151
+ else val = null;
152
+ }
153
+ else if (_.isPlainObject(value)) {
154
+ if (_.isArray(val)) val = val[0];
155
+ else val = null;
156
+ }
157
+ desObj[key] = val;
158
+ });
159
+ });
160
+ }
161
+ //===========================================================================================
162
+ function groupBy(arrayObj, groupField, sumArr) { // vi du: groupBy(dsSinhVien, 'Khoa', ['SL Nghỉ Học','SL Tốt Nghiệp'])
163
+ var gArr = _.groupBy(arrayObj, groupField);
164
+ var returnArr = [];
165
+ var sumObj = {};
166
+ if (!_.isArray(sumArr)) sumArr = [sumArr];
167
+ //---------------------------------
168
+ _.forEach(gArr, function (fieldValue, fieldKey) {
169
+ var n = 0;
170
+ _.forEach(sumArr, function (v) {
171
+ sumObj[v] = 0;
172
+ });
173
+ _.forEach(fieldValue, function (value) {
174
+ n++;
175
+ _.forEach(sumArr, function (v) {
176
+ sumObj[v] = sumObj[v] + value[v];
177
+ });
178
+ });
179
+ var o = {};
180
+ o[groupField] = fieldKey;
181
+ o["count"] = n;
182
+ o = $.extend(o, sumObj);
183
+ returnArr.push(o);
184
+ });
185
+ return returnArr;
186
+ }
187
+ //===========================================================================================
188
+ function chartDataBuild(obj) {
189
+ if (obj.dataChart) {
190
+ var chartData = [];
191
+ var dataInput = obj.dataChart.data;
192
+
193
+ if (_.isUndefined(dataInput)) dataInput = obj.dataChart;
194
+
195
+ if (_.isArray(dataInput[0])) {
196
+ var chartName = JSON.parse(obj.dataChart.chartName);
197
+ _.forEach(dataInput, function (d, k) {
198
+ chartData.push({
199
+ name: chartName[k],
200
+ data: _.map(dataInput[k], function (n) {
201
+ return [n.label, n.value];
202
+ })
203
+ });
204
+ });
205
+ } else {
206
+ chartData.push({
207
+ name: (obj.dataChart.chartName ? obj.dataChart.chartName : null),
208
+ data: _.map(dataInput, function (n) {
209
+ return [n.label, n.value];
210
+ })
211
+ });
212
+ }
213
+ //---------------------
214
+ if (obj.allInOne) return [{ name: "", data: chartData }];
215
+ else return chartData;
216
+ }
217
+ }
218
+ //===========================================================================================
219
+ function chartQABuild(obj) {
220
+ if (obj.dataChart) {
221
+ var chartData = [];
222
+
223
+ _.forEach(obj.dataChart[0], function (v, i) { // Duyet tung cau hoi
224
+ if (v.QType == 'TextArea') {
225
+
226
+ } else {
227
+ chartData.push({
228
+ name: v.QText,
229
+ data: _.map(_.filter(obj.dataChart[1], ['QID', v.QID]), function (n) {
230
+ return [n.label, n.val];
231
+ })
232
+ });
233
+ }
234
+ });
235
+ return chartData;
236
+ }
237
+ }
238
+ //===========================================================================================
239
+ function confirm(obj) {
240
+ $.confirm({
241
+ icon: (obj.icon ? obj.icon : 'mdi mdi-help-circle-outline'),
242
+ title: obj.title,
243
+ content: (obj.message ? obj.message : ''),
244
+ type: (obj.type ? obj.type : 'blue'),
245
+ columnClass: 'col-xs-12 col-sm-8 col-md-6 col-lg-4',
246
+ animateFromElement: false,
247
+ buttons: {
248
+ ok: {
249
+ text: '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;OK&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;',
250
+ btnClass: 'btn-blue',
251
+ keys: ['enter'],
252
+ action: obj.action
253
+ },
254
+ cancel: (obj.cancel ? obj.cancel : function () { })
255
+ }
256
+ });
257
+ }
258
+ //===========================================================================================
259
+ function showMessage(obj) {
260
+ $.dialog({
261
+ icon: (obj.icon ? obj.icon : 'mdi mdi-information-variant-circle-outline'),
262
+ title: (obj.title ? obj.title : ''),
263
+ backgroundDismiss: true,
264
+ content: (obj.message ? obj.message : ''),
265
+ type: (obj.type ? obj.type : 'blue'),
266
+ columnClass: 'col-xs-12 col-sm-8 col-md-6 col-lg-4',
267
+ animateFromElement: false,
268
+ closeIcon: true,
269
+ buttons: {
270
+ ok: {
271
+ text: '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Close&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;',
272
+ btnClass: 'btn-' + (obj.type ? obj.type : 'blue')
273
+ }
274
+ },
275
+ onClose: function () {
276
+ if (obj.onclose) {
277
+ runAction(obj.onclose, { $row: obj.$row, item: obj.item });
278
+ }
279
+ },
280
+ });
281
+ }
282
+ //===========================================================================================
283
+ function rightTest(rightObject) {
284
+ var returnVal = true
285
+ _.forEach(rightObject, function (rV, rK) {
286
+ if (!returnVal) return;
287
+ var A1 = [];
288
+ var A2 = [];
289
+
290
+ if (_.isArray(rV)) A1 = rV;
291
+ else A1 = [rV];
292
+
293
+ if (_.isArray(vueData.user[rK])) A2 = vueData.user[rK];
294
+ else A2 = [vueData.user[rK]];
295
+
296
+ if (_.intersection(A1, A2).length <= 0) returnVal = false;
297
+ });
298
+ return returnVal;
299
+ }
300
+ //===========================================================================================
301
+ function openWindow(obj) {
302
+ if (window === top) {
303
+ setTimeout(function () {
304
+ if (obj.id != undefined && vueData[obj.id] != undefined) {
305
+ console.log("Cửa sổ bị trùng tên: ", obj.id)
306
+ return;
307
+ }
308
+
309
+ if (!vueData.v_windowVM) vueData.v_windowVM = {};
310
+ if (_.isString(obj)) obj = { url: obj };
311
+
312
+ if (!obj.id) obj.id = "Window" + generateID();
313
+ obj.open = true;
314
+ vueData[obj.id] = obj;
315
+ vueData.v_windowVM[obj.id] = new Vue({
316
+ vuetify: new Vuetify(),
317
+ render: Vue.compile("<f-window id=\"" + obj.id + "\"></f-window>").render,
318
+ }).$mount();
319
+ }, 10);
320
+ }
321
+ else {
322
+ windowSendMessage('#PARENT', 'openWindow', obj);
323
+ }
324
+ }
325
+ //===========================================================================================
326
+ function redirect(obj, query) {
327
+ let Q = '';
328
+ if (obj && obj.url) Q = obj.url;
329
+ else Q = obj;
330
+
331
+ if ((query || (obj && obj.query)) && vueData.p_params != " ") {
332
+ if (Q.indexOf("?") < 0) Q = Q + vueData.p_params;
333
+ else Q = Q + vueData.p_params.replace("?", "");
334
+ }
335
+
336
+ location.assign(Q);
337
+ }
338
+ //===========================================================================================
339
+ function reload() {
340
+ location.assign(window.location.href);
341
+ }
342
+ //===========================================================================================
343
+ function findInArray(obj) {
344
+ if (_.isString(obj.value)) obj.value = JSON.parse(obj.value);
345
+ return _.find(obj.array, obj.value);
346
+ }
347
+ //===========================================================================================
348
+ function fixURL(url) {
349
+ if (url.indexOf('http') == 0) return url;
350
+ else return vueData.v_Set.apiDomain + url
351
+ }
352
+
353
+ //===========================================================================================
354
+ function stringAttrToJson(str, removeVueEvent) {
355
+ var jObj = {}
356
+
357
+ if (typeof str == 'undefined') str = "";
358
+
359
+ if (_.isPlainObject(str)) return { ...str };
360
+
361
+ $(`<div ${str} >`).each(function () {
362
+ $.each(this.attributes, function (i, a) {
363
+ if (removeVueEvent && (typeof a.name == "string" && (a.name.substring(0, 1) == ":" || a.name.substring(0, 1) == "@" || a.name.substring(0, 2) == "v-"))) return;
364
+ jObj[a.name] = a.value;
365
+ })
366
+ })
367
+ return jObj
368
+ }
369
+
370
+ //===========================================================================================
371
+ function json_data_parse(obj, level) {
372
+ //--- convert to json data array --------
373
+ if (level > 3) return;
374
+
375
+ level++;
376
+ if (_.isArray(obj) && obj.length > 0) {
377
+ //console.log("json_data_parse", level)
378
+ if (_.isArray(obj[0])) {
379
+ _.forEach(obj, function (value) {
380
+ json_data_parse(value, level);
381
+ });
382
+ } else if (_.isPlainObject(obj[0])) {
383
+ var R = obj[0];
384
+ var nameOfJson = [];
385
+ _.forEach(R, function (value, key) {
386
+ if (key.substring(0, 10) == 'json_data:') nameOfJson.push(key);
387
+ });
388
+ _.forEach(obj, function (row) {
389
+ _.forEach(nameOfJson, function (key) {
390
+ row[key.replace('json_data:', '')] = JSON.parse(row[key]);
391
+ delete row[key];
392
+ });
393
+ });
394
+ }
395
+ } else if (_.isPlainObject(obj)) {
396
+ _.forEach(obj, function (value, key) {
397
+ if (key.substring(0, 10) == 'json_data:') {
398
+ obj[key.replace('json_data:', '')] = JSON.parse(value);
399
+ delete obj[key];
400
+ } else {
401
+ if (_.isObject(value)) json_data_parse(value, level);
402
+ }
403
+ });
404
+ }
405
+ }
406
+ //===========================================================================================
407
+ function ajaxCALL(URL, DATA, callBack, errorCallBack, header) {
408
+ let loadingID = generateID();
409
+ vueData.v_LoadingOverlay.push(loadingID);
410
+ vueData.v_Loading = true;
411
+
412
+ $.ajax({
413
+ type: "POST",
414
+ headers: (header ? header : {
415
+ authorization: $awt
416
+ }),
417
+ url: fixURL(URL),
418
+ contentType: "application/json; charset=utf-8",
419
+ dataType: "json",
420
+ crossDomain: true,
421
+ data: JSON.stringify(DATA),
422
+ success: function (d) {
423
+ json_data_parse(d, 0);
424
+ if (callBack) callBack(d);
425
+ },
426
+ error: function (xhr, ajaxOptions, thrownError) {
427
+ console.log("API:", URL, "IN:", DATA);
428
+ console.log("Error XHR:", xhr);
429
+
430
+ if (errorCallBack) {
431
+ errorCallBack(xhr);
432
+ return
433
+ }
434
+
435
+ showMessage({
436
+ icon: 'mdi mdi-alert-circle-outline',
437
+ title: '',
438
+ message: (xhr.responseJSON ? xhr.responseJSON.Message : (xhr.responseText ? xhr.responseText : "")),
439
+ type: 'red',
440
+ });
441
+ },
442
+ complete: function (data) {
443
+ vueData.v_LoadingOverlay = _.without(vueData.v_LoadingOverlay, loadingID);
444
+ if (vueData.v_LoadingOverlay.length == 0) vueData.v_Loading = false;
445
+ }
446
+ });
447
+ }
448
+ //===========================================================================================
449
+ function capacityText(numb) {
450
+ if (numb < 1048576) return Math.round(numb / 1024 * 100 / 100) + ' Kb';
451
+ else return Math.round(numb / 1048576 * 100) / 100 + ' Mb';
452
+ }
453
+ //===========================================================================================
454
+ function generateID() {
455
+ var d = new Date();
456
+ var k = d.getTime();
457
+ var str = k.toString(16).slice(1)
458
+ var UUID = 'idxxxxxxxx'.replace(/[xy]/g, function (c) {
459
+ var r = Math.random() * 16 | 0;
460
+ v = c == 'x' ? r : (r & 3 | 8);
461
+ return v.toString(16);
462
+ });
463
+ var newString = UUID.replace(/[z]/, str)
464
+ return newString;
465
+ }
466
+ //===========================================================================================
467
+ function printPDF(obj) {
468
+ pdfMake.fonts = {
469
+ Roboto: {
470
+ normal: 'Roboto-Regular.ttf',
471
+ bold: 'Roboto-Medium.ttf',
472
+ italics: 'Roboto-Italic.ttf',
473
+ bolditalics: 'Roboto-MediumItalic.ttf'
474
+ },
475
+ TimesNewRoman: {
476
+ normal: 'times.ttf',
477
+ bold: 'timesbd.ttf',
478
+ italics: 'timesi.ttf',
479
+ bolditalics: 'timesbi.ttf'
480
+ }
481
+ }
482
+
483
+ if (obj.download) {
484
+ pdfMake.createPdf(obj.data).download('fp_pdf_' + generateID() + '.pdf');
485
+ } else if (obj.viewer) {
486
+ pdfMake.createPdf(obj.data).getDataUrl(function (outDoc) {
487
+ $(obj.viewer).attr("src", outDoc);
488
+ });
489
+ } else {
490
+ pdfMake.createPdf(obj.data).getBase64(function (outDoc) {
491
+ printJS({ printable: outDoc, type: 'pdf', base64: true });
492
+ });
493
+ }
494
+ //-------------------------------------------------------
495
+ if (_.isFunction(obj.callBack)) obj.callBack();
496
+ }
497
+
498
+ //===========================================================================================
499
+ function colorLib(color) {
500
+ // Hàm hỗ trợ phân tích và chuyển đổi màu
501
+ function parseColor(input) {
502
+ let r, g, b, a = 1;
503
+
504
+ // Xử lý HEX (3 hoặc 6 ký tự)
505
+ if (typeof input === 'string' && input.startsWith('#')) {
506
+ let hex = input.replace('#', '');
507
+ if (hex.length === 3) {
508
+ hex = hex.split('').map(c => c + c).join('');
509
+ }
510
+ r = parseInt(hex.slice(0, 2), 16);
511
+ g = parseInt(hex.slice(2, 4), 16);
512
+ b = parseInt(hex.slice(4, 6), 16);
513
+ return { r, g, b, a };
514
+ }
515
+
516
+ // Xử lý RGB/RGBA
517
+ if (typeof input === 'string' && input.startsWith('rgb')) {
518
+ const match = input.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
519
+ if (match) {
520
+ r = parseInt(match[1]);
521
+ g = parseInt(match[2]);
522
+ b = parseInt(match[3]);
523
+ a = match[4] ? parseFloat(match[4]) : 1;
524
+ return { r, g, b, a };
525
+ }
526
+ }
527
+
528
+ // Xử lý HSL
529
+ if (typeof input === 'string' && input.startsWith('hsl')) {
530
+ const match = input.match(/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*([\d.]+))?\)/);
531
+ if (match) {
532
+ const h = parseInt(match[1]) / 360;
533
+ const s = parseInt(match[2]) / 100;
534
+ const l = parseInt(match[3]) / 100;
535
+ a = match[4] ? parseFloat(match[4]) : 1;
536
+ return hslToRgb(h, s, l, a);
537
+ }
538
+ }
539
+
540
+ // Mặc định trả về màu đen nếu không phân tích được
541
+ return { r: 0, g: 0, b: 0, a: 1 };
542
+ }
543
+
544
+ // Chuyển HSL sang RGB
545
+ function hslToRgb(h, s, l, a) {
546
+ let r, g, b;
547
+ if (s === 0) {
548
+ r = g = b = l; // Grayscale
549
+ } else {
550
+ const hue2rgb = (p, q, t) => {
551
+ if (t < 0) t += 1;
552
+ if (t > 1) t -= 1;
553
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
554
+ if (t < 1 / 2) return q;
555
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
556
+ return p;
557
+ };
558
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
559
+ const p = 2 * l - q;
560
+ r = hue2rgb(p, q, h + 1 / 3);
561
+ g = hue2rgb(p, q, h);
562
+ b = hue2rgb(p, q, h - 1 / 3);
563
+ }
564
+ return {
565
+ r: Math.round(r * 255),
566
+ g: Math.round(g * 255),
567
+ b: Math.round(b * 255),
568
+ a
569
+ };
570
+ }
571
+
572
+ // Chuyển RGB sang HSL
573
+ function rgbToHsl(r, g, b) {
574
+ r /= 255;
575
+ g /= 255;
576
+ b /= 255;
577
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
578
+ let h, s, l = (max + min) / 2;
579
+
580
+ if (max === min) {
581
+ h = s = 0; // Grayscale
582
+ } else {
583
+ const d = max - min;
584
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
585
+ switch (max) {
586
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
587
+ case g: h = (b - r) / d + 2; break;
588
+ case b: h = (r - g) / d + 4; break;
589
+ }
590
+ h /= 6;
591
+ }
592
+ return { h, s, l };
593
+ }
594
+
595
+ // Hàm chuyển đổi số thành chuỗi hex 2 chữ số
596
+ function toHex(value) {
597
+ const hex = Math.round(value).toString(16);
598
+ return hex.length === 1 ? '0' + hex : hex;
599
+ }
600
+
601
+ // Phân tích màu đầu vào
602
+ const { r, g, b, a } = parseColor(color);
603
+
604
+ // Đối tượng trả về với các phương thức
605
+ return {
606
+ // Trả về chuỗi HEX
607
+ hexString() {
608
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
609
+ },
610
+
611
+ // Trả về chuỗi RGB hoặc RGBA
612
+ rgbString() {
613
+ return a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`;
614
+ },
615
+
616
+ // Trả về chuỗi HSL hoặc HSLA
617
+ hslString() {
618
+ const { h, s, l } = rgbToHsl(r, g, b);
619
+ return a < 1
620
+ ? `hsla(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%, ${a})`
621
+ : `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
622
+ },
623
+
624
+ // Làm sáng màu (factor: 0 đến 1)
625
+ lighten(factor) {
626
+ const { h, s, l } = rgbToHsl(r, g, b);
627
+ const newL = Math.min(1, l + l * factor);
628
+ return colorLib(hslToRgb(h, s, newL, a).rgbString());
629
+ },
630
+
631
+ // Làm tối màu (factor: 0 đến 1)
632
+ darken(factor) {
633
+ const { h, s, l } = rgbToHsl(r, g, b);
634
+ const newL = Math.max(0, l - l * factor);
635
+ return colorLib(hslToRgb(h, s, newL, a).rgbString());
636
+ },
637
+
638
+ // Đặt độ trong suốt
639
+ alpha(value) {
640
+ return colorLib(`rgba(${r}, ${g}, ${b}, ${value})`);
641
+ }
642
+ };
643
+ }
644
+ //===========================================================================================
645
+ function transparentize(value, opacity) {
646
+ var alpha = opacity === undefined ? 0.5 : 1 - opacity;
647
+ return colorLib(value).alpha(alpha).rgbString();
648
+ }
649
+ //===========================================================================================
650
+ function jsonToExcel(Obj) {
651
+ if (typeof ExcelJS == 'undefined') {
652
+ var loadScript = new Loader();
653
+ loadScript.require(["/include/excel/exceljs.min.js"], function () {
654
+ writeExcelFile(Obj);
655
+ });
656
+ } else writeExcelFile(Obj);
657
+ function writeExcelFile(inputObj) {
658
+ var props = {
659
+ data: null, // Json to download (for single sheet - backward compatibility)
660
+ filename: "dataFP.xlsx", // filename to export
661
+ worksheet: "SheetFP", // sheet name for single sheet
662
+ sheets: null, // New property for multiple sheets
663
+ // optional: [{header, key, type?: 'number'|'date'|'html', numFmt?, width?, sum?: true|numFmt}]
664
+ // Có columns thì cột/tiêu đề/định dạng theo đúng danh sách này (dùng cho f-table excel="UI");
665
+ // không có thì giữ hành vi cũ: cột = key của dòng đầu, giá trị thô. Mỗi sheet có thể có columns riêng.
666
+ columns: null,
667
+ onProgress: null, // optional: function({percent, processedRows, totalRows, phase, sheetName}) - phase: 'building'|'finalizing'|'done'|'error'
668
+ showProgress: true // tự hiện thanh % cố định giữa màn hình khi xuất; truyền false để tắt UI này
669
+ }
670
+ Object.assign(props, inputObj);
671
+ // Gom về một danh sách sheet chung cho cả 2 chế độ cũ (single) và mới (multi) để dùng chung logic tiến trình/chunk
672
+ var sheetList;
673
+ if (props.sheets && Array.isArray(props.sheets)) {
674
+ if (!props.sheets.length) { showMessage({ title: "Sheets data is empty" }); return; }
675
+ sheetList = props.sheets.filter(function (s) { return s.data && s.data.length; });
676
+ if (!sheetList.length) { showMessage({ title: "Sheets data is empty" }); return; }
677
+ } else {
678
+ if (!props.data || !props.data.length) { showMessage({ title: "Data Null" }); return; }
679
+ sheetList = [{ name: props.worksheet, data: props.data, columns: props.columns }];
680
+ }
681
+
682
+ var totalRows = sheetList.reduce(function (sum, s) { return sum + s.data.length; }, 0);
683
+ var processedRows = 0;
684
+ // Chunk động: mỗi đợt ghi ~2% tổng số dòng, nên số lần nhường luồng UI luôn ~50 lần dù
685
+ // data ít hay nhiều (không còn đánh đổi mượt-vs-nhanh như chunk cố định) — data nhỏ thì
686
+ // chặn dưới 50 dòng/đợt để khỏi nhường luồng vô ích cho vài chục dòng.
687
+ var CHUNK_SIZE = Math.max(50, Math.ceil(totalRows * 0.02));
688
+ var progressUI = props.showProgress ? createProgressUI() : null;
689
+
690
+ function report(phase, sheetName) {
691
+ // Giai đoạn ghi dòng ("building") chiếm tối đa 95%; ExcelJS không có hook tiến trình cho bước
692
+ // nén file (writeBuffer) nên "finalizing" chỉ báo mốc cố định 98%, không nội suy %.
693
+ var percent = totalRows ? Math.min(95, Math.floor(processedRows / totalRows * 95)) : 0;
694
+ if (phase === 'finalizing') percent = 98;
695
+ if (phase === 'done') percent = 100;
696
+ if (typeof props.onProgress === 'function') {
697
+ try { props.onProgress({ percent: percent, processedRows: processedRows, totalRows: totalRows, phase: phase, sheetName: sheetName }); }
698
+ catch (e) { console.error('jsonToExcel onProgress error', e); }
699
+ }
700
+ if (progressUI) {
701
+ progressUI.update({ percent: percent, phase: phase });
702
+ if (phase === 'done' || phase === 'error') progressUI.destroy();
703
+ }
704
+ }
705
+
706
+ var workbook = new ExcelJS.Workbook();
707
+
708
+ function buildSheet(sheetIndex) {
709
+ if (sheetIndex >= sheetList.length) { finalize(); return; }
710
+ var sheetCfg = sheetList[sheetIndex];
711
+ var worksheet = workbook.addWorksheet(sheetCfg.name || 'Sheet');
712
+ var cols = Array.isArray(sheetCfg.columns) && sheetCfg.columns.length ? sheetCfg.columns : null;
713
+ // Create header (chỉ cần đọc key của dòng đầu, tương đương logic cũ nhưng không lặp check index===0 mỗi vòng)
714
+ if (cols) setupColumns(worksheet, cols, sheetCfg.data);
715
+ else worksheet.columns = Object.keys(sheetCfg.data[0]).map(function (k) { return { header: k, key: k }; });
716
+
717
+ var rows = sheetCfg.data;
718
+ var i = 0;
719
+ function nextChunk() {
720
+ try {
721
+ var end = Math.min(i + CHUNK_SIZE, rows.length);
722
+ for (var r = i; r < end; r++) { worksheet.addRow(cols ? columnRowValues(cols, rows[r]) : rows[r]); }
723
+ if (cols && end === rows.length) appendSumRow(worksheet, cols, rows);
724
+ processedRows += (end - i);
725
+ i = end;
726
+ report('building', sheetCfg.name);
727
+ } catch (err) {
728
+ console.error('jsonToExcel build error', err);
729
+ report('error', sheetCfg.name);
730
+ showMessage({ title: 'Xuất Excel thất bại' });
731
+ return;
732
+ }
733
+ if (i < rows.length) {
734
+ // Nhường luồng UI giữa các đợt (thay vì ghi hết trong 1 vòng lặp đồng bộ) để tránh
735
+ // trang bị "Not Responding" khi data lớn, đồng thời cho phép % cập nhật lên màn hình.
736
+ (window.requestAnimationFrame || function (cb) { setTimeout(cb, 0); })(nextChunk);
737
+ } else {
738
+ buildSheet(sheetIndex + 1);
739
+ }
740
+ }
741
+ nextChunk();
742
+ }
743
+
744
+ function finalize() {
745
+ report('finalizing');
746
+ workbook.xlsx.writeBuffer().then(function (data) {
747
+ var blobType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
748
+ var blob = new Blob([data], { type: blobType });
749
+ var a = document.createElement('a');
750
+ document.body.appendChild(a);
751
+ a.style = 'display: none';
752
+ var url = window.URL.createObjectURL(blob);
753
+ a.href = url;
754
+ a.download = props.filename;
755
+ a.click();
756
+ window.URL.revokeObjectURL(url);
757
+ a.remove();
758
+ report('done');
759
+ }).catch(function (err) {
760
+ console.error('jsonToExcel writeBuffer error', err);
761
+ report('error');
762
+ showMessage({ title: 'Xuất Excel thất bại' });
763
+ });
764
+ }
765
+
766
+ buildSheet(0);
767
+ }
768
+
769
+ //---------------------------------------------------- chế độ `columns`
770
+ function readColumnValue(row, key) {
771
+ if (row === null || row === undefined) return null;
772
+ if (row[key] !== undefined) return row[key];
773
+ return _.get(row, key, null);
774
+ }
775
+ // ExcelJS ghi Date theo UTC → dựng lại mốc UTC từ giờ ĐỊA PHƯƠNG, nếu không ngày 00:00 ở UTC+7
776
+ // sẽ lùi thành 17:00 hôm trước trong Excel. "yyyy-mm-dd" trần thì new Date() hiểu là UTC nên tách riêng.
777
+ function toExcelDate(v) {
778
+ if (v === null || v === undefined || v === '') return null;
779
+ var m = typeof v === 'string' && /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
780
+ if (m) return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
781
+ var d = v instanceof Date ? v : new Date(v);
782
+ if (isNaN(d.getTime())) return v;
783
+ return new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()));
784
+ }
785
+ function htmlToText(v) {
786
+ var s = String(v);
787
+ if (s.indexOf('<') < 0 && s.indexOf('&') < 0) return s;
788
+ // DOMParser không chạy script/handler — innerHTML trên element sống thì <img onerror> VẪN chạy.
789
+ return new DOMParser().parseFromString(s, 'text/html').body.textContent;
790
+ }
791
+ function convertCellValue(col, v) {
792
+ if (v === undefined) return null;
793
+ if (col.type === 'date') return toExcelDate(v);
794
+ if (v === null) return null;
795
+ if (col.type === 'number') return (typeof v === 'string' && v.trim() !== '' && !isNaN(v)) ? Number(v) : v;
796
+ if (col.type === 'html') return htmlToText(v);
797
+ // object/array đưa thẳng vào ExcelJS sẽ bị hiểu nhầm là rich text/hyperlink/formula
798
+ if (typeof v === 'object' && !(v instanceof Date)) return JSON.stringify(v);
799
+ return v;
800
+ }
801
+ function columnRowValues(cols, row) {
802
+ var out = new Array(cols.length);
803
+ for (var c = 0; c < cols.length; c++) out[c] = convertCellValue(cols[c], readColumnValue(row, cols[c].key));
804
+ return out;
805
+ }
806
+ function estimateWidth(col, rows) {
807
+ var w = String(col.header != null ? col.header : col.key).length;
808
+ if (col.type === 'date') w = Math.max(w, String(col.numFmt || 'dd/mm/yyyy').replace(/"/g, '').length);
809
+ else {
810
+ for (var i = 0, n = Math.min(rows.length, 100); i < n; i++) {
811
+ var v = readColumnValue(rows[i], col.key);
812
+ if (v !== null && v !== undefined) w = Math.max(w, String(v).length + (typeof v === 'number' ? 3 : 0));
813
+ }
814
+ }
815
+ return Math.min(60, Math.max(8, w + 2));
816
+ }
817
+ function setupColumns(worksheet, cols, rows) {
818
+ // key nội bộ theo vị trí: hai cột trùng tiêu đề/trùng field vẫn không đè nhau (dòng ghi bằng mảng)
819
+ worksheet.columns = cols.map(function (c, idx) {
820
+ var def = { header: c.header != null ? String(c.header) : String(c.key), key: '__c' + idx, width: c.width || estimateWidth(c, rows) };
821
+ if (c.numFmt) def.style = { numFmt: c.numFmt };
822
+ return def;
823
+ });
824
+ worksheet.getRow(1).font = { bold: true };
825
+ worksheet.views = [{ state: 'frozen', ySplit: 1 }];
826
+ }
827
+ // Hàng tổng = công thức SUM trên chính các dòng đã xuất (kèm kết quả tính sẵn để trình xem không
828
+ // tính công thức vẫn thấy số) — sửa số trong Excel thì tổng tự cập nhật.
829
+ function appendSumRow(worksheet, cols, rows) {
830
+ if (!rows.length || !cols.some(function (c) { return c.sum; })) return;
831
+ var row = worksheet.addRow(cols.map(function (c, idx) {
832
+ if (!c.sum) return null;
833
+ var total = 0;
834
+ for (var i = 0; i < rows.length; i++) {
835
+ var n = Number(readColumnValue(rows[i], c.key));
836
+ if (!isNaN(n)) total += n;
837
+ }
838
+ var letter = worksheet.getColumn(idx + 1).letter;
839
+ return { formula: 'SUM(' + letter + '2:' + letter + (rows.length + 1) + ')', result: total };
840
+ }));
841
+ row.font = { bold: true };
842
+ cols.forEach(function (c, idx) {
843
+ if (typeof c.sum === 'string') row.getCell(idx + 1).numFmt = c.sum;
844
+ });
845
+ }
846
+
847
+ // Thanh % mặc định, tự tạo/tự dọn bằng DOM thuần (CSS thủ công, không dùng v-dialog/framework nào)
848
+ // — fix cố định giữa màn hình, không phụ thuộc Vue nên dùng chung được cho cả V2 và V3.
849
+ function createProgressUI() {
850
+ var STYLE_ID = 'fui-json-to-excel-progress-style';
851
+ // Luôn ghi đè nội dung CSS mới nhất vào thẻ <style>, kể cả khi tag đã tồn tại từ lần
852
+ // export trước trong cùng phiên trang (SPA không reload) — nếu chỉ tạo-1-lần-rồi-bỏ-qua
853
+ // như trước, CSS cũ bị kẹt lại vĩnh viễn trong DOM, sửa file JS trên server sau đó
854
+ // không có tác dụng cho tới khi người dùng F5 lại cả trang.
855
+ var style = document.getElementById(STYLE_ID);
856
+ if (!style) {
857
+ style = document.createElement('style');
858
+ style.id = STYLE_ID;
859
+ document.head.appendChild(style);
860
+ }
861
+ style.innerHTML = '.fui-xlsx-progress{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:350px;' +
862
+ 'box-sizing:border-box;z-index:99999;background:#fff;border-radius:8px;box-shadow:0 12px 40px rgba(0,0,0,.35);' +
863
+ 'padding: 30px 30px 40px 30px;font:14px/1.4 inherit;transition:opacity .3s}' +
864
+ '.fui-xlsx-progress .fui-xlsx-label{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;color:#333}' +
865
+ '.fui-xlsx-progress .fui-xlsx-label span:first-child{font-size:15px;font-weight:600}' +
866
+ '.fui-xlsx-progress .fui-xlsx-percent{font-size:22px;font-weight:700;color:#1976d2}' +
867
+ '.fui-xlsx-progress .fui-xlsx-track{height:8px;border-radius:4px;background:#e0e0e0;overflow:hidden}' +
868
+ '.fui-xlsx-progress .fui-xlsx-bar{height:100%;width:0;background:#1976d2;transition:width .2s ease-out}';
869
+ var el = document.createElement('div');
870
+ el.className = 'fui-xlsx-progress';
871
+ el.innerHTML = '<div class="fui-xlsx-label"><span>Đang xuất Excel...</span><span class="fui-xlsx-percent">0%</span></div>' +
872
+ '<div class="fui-xlsx-track"><div class="fui-xlsx-bar"></div></div>';
873
+ document.body.appendChild(el);
874
+ var percentEl = el.querySelector('.fui-xlsx-percent');
875
+ var barEl = el.querySelector('.fui-xlsx-bar');
876
+ var labelEl = el.querySelector('.fui-xlsx-label span');
877
+ var destroyed = false;
878
+ return {
879
+ update: function (info) {
880
+ if (destroyed) return;
881
+ percentEl.textContent = info.percent + '%';
882
+ barEl.style.width = info.percent + '%';
883
+ if (info.phase === 'finalizing') labelEl.textContent = 'Đang tạo file...';
884
+ if (info.phase === 'done') labelEl.textContent = 'Hoàn tất';
885
+ if (info.phase === 'error') labelEl.textContent = 'Xuất thất bại';
886
+ },
887
+ destroy: function () {
888
+ if (destroyed) return;
889
+ destroyed = true;
890
+ el.style.opacity = '0';
891
+ setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 500);
892
+ }
893
+ };
894
+ }
895
+ }
896
+ //===========================================================================================
897
+ function copyToClipboard(textToCopy) {
898
+ if (navigator.clipboard && window.isSecureContext) {
899
+ console.log("Navigator copy: ", textToCopy);
900
+ return navigator.clipboard.writeText(textToCopy);
901
+ } else {
902
+ let textArea = document.createElement("div");
903
+ textArea.innerText = textToCopy;
904
+ textArea.style.position = "fixed";
905
+ textArea.style.left = "0px";
906
+ textArea.style.top = "-999999px";
907
+ document.body.appendChild(textArea);
908
+
909
+ var range = document.createRange();
910
+ range.selectNode(textArea);
911
+ window.getSelection().removeAllRanges();
912
+ window.getSelection().addRange(range);
913
+ return new Promise((res, rej) => {
914
+ document.execCommand('copy') ? res() : rej();
915
+ console.log("Command copy: ", textToCopy);
916
+ textArea.remove();
917
+ });
918
+ }
919
+ };
920
+ //===========================================================================================
921
+ function fileNameClear(fname) {
922
+ if (fname) return fname.replace(/[\!\@\#\$\^\&\%\*\(\)\+\=\-\[\]\\\/\{\}\|\:\<\>\?\,\.\`\"\'\~\;]/gi, '');
923
+ return fname;
924
+ }
925
+ //===========================================================================================
926
+ function hashCode(s) {
927
+ var h = 0,
928
+ l = s.length,
929
+ i = 0;
930
+ if (l > 0)
931
+ while (i < l)
932
+ h = (h << 5) - h + s.charCodeAt(i++) | 0;
933
+ return h;
934
+ };
935
+ //===========================================================================================
936
+ function getCookie(cname) {
937
+ var name = cname + "=";
938
+ var ca = document.cookie.split(';');
939
+ for (var i = 0; i < ca.length; i++) {
940
+ var c = ca[i];
941
+ while (c.charAt(0) == ' ') {
942
+ c = c.substring(1);
943
+ }
944
+ if (c.indexOf(name) == 0) {
945
+ return c.substring(name.length, c.length);
946
+ }
947
+ }
948
+ return "";
949
+ }
950
+
951
+ function setCookie(name, value, days, domain) {
952
+ var expires = "";
953
+ var domainString = "";
954
+ if (days) {
955
+ var date = new Date();
956
+ date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
957
+ expires = "; expires=" + date.toUTCString();
958
+ }
959
+ if (domain) {
960
+ domainString = "; domain=" + domain
961
+ }
962
+ document.cookie = name + "=" + (value || "") + expires + domainString + ";path=/";
963
+ }
964
+ //===================================================================================
965
+ function logout(cookiesName, domain, url) {
966
+ if (!cookiesName) cookiesName = 'awt';
967
+ if (typeof domain == "undefined" && vueData.v_Set.authDomain) domain = vueData.v_Set.authDomain;
968
+
969
+ setCookie(cookiesName, '', 0, domain);
970
+
971
+ if (!url) location.assign(location.href);
972
+ else location.assign(url);
973
+ }
974
+
975
+ //===========================================================================================
976
+ // WEBSOCKET
977
+ //===========================================================================================
978
+ function webSocketJoinGroup(groupObj, timeout, callBackFunc) {
979
+ groupObj = _.cloneDeep(groupObj)
980
+ _.forEach(groupObj, function (v, k) {
981
+ if (!vueData.webSocket.hasOwnProperty(k)) {
982
+ Vue.set(vueData.webSocket, k, v);
983
+ }
984
+ });
985
+
986
+ setTimeout(function () {
987
+ try {
988
+ $.connection.hub.start().done(function () {
989
+ _.forEach(groupObj, function (v, k) {
990
+ $.connection.chatHub.server.joinGroup(k);
991
+ });
992
+ if (_.isFunction(callBackFunc)) callBackFunc();
993
+ }).fail(function (e) {
994
+ });
995
+ }
996
+ catch (err) {
997
+
998
+ }
999
+ }, (timeout ? timeout : 0));
1000
+ }
1001
+
1002
+ function webSocketConnection(wsURL, WS, wsState) {
1003
+ $.connection.hub.url = wsURL;
1004
+
1005
+ //=====================================================================
1006
+ $.connection.hub.stateChanged(function (change) {
1007
+ wsState.code = change.newState;
1008
+ if (change.newState === $.signalR.connectionState.reconnecting) {
1009
+ console.log("Websocket Reconnecting")
1010
+ wsState.text = "Reconnecting";
1011
+ }
1012
+ else if (change.newState === $.signalR.connectionState.connected) {
1013
+ console.log("Websocket Connected")
1014
+ wsState.text = "Connected";
1015
+ }
1016
+ else if (change.newState === $.signalR.connectionState.disconnected) {
1017
+ console.log("Websocket Disconnected")
1018
+ wsState.text = "Disconnected";
1019
+
1020
+ webSocketJoinGroup(WS, 5000);
1021
+ }
1022
+ });
1023
+
1024
+ $.connection.chatHub.client.onError = function (Mess) {
1025
+ console.log('Server error: ', Mess);
1026
+ };
1027
+
1028
+ $.connection.chatHub.client.onJoinGroup = function (mess) {
1029
+ console.log("Group", mess)
1030
+ };
1031
+
1032
+ $.connection.chatHub.client.messageReceived = function (group, message) {
1033
+ console.log("Message Received: ", group, message)
1034
+ if (_.isArray(WS[group])) WS[group].push(message);
1035
+ else WS[group] = message;
1036
+ };
1037
+
1038
+ $.connection.chatHub.client.messageSendLog = function (group, message) {
1039
+ console.log("Message Sent: ", group, message)
1040
+ };
1041
+ }
1042
+
1043
+ function webSocket_Send(obj) {
1044
+ if (_.isPlainObject(obj.data)) $.connection.chatHub.server.messageSendObject(obj.group, obj.data);
1045
+ else if (_.isArray(obj.data)) $.connection.chatHub.server.messageSendArray(obj.group, obj.data);
1046
+ else $.connection.chatHub.server.messageSend(obj.group, obj.data);
1047
+ }
1048
+ //===========================================================================================
1049
+ //===========================================================================================
1050
+ //===========================================================================================
1051
+ //===========================================================================================