@fui-org/fui-cli 0.2.0 → 0.4.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 (212) hide show
  1. package/README.md +20 -2
  2. package/dist/fui.js +347 -44
  3. package/package.json +15 -3
  4. package/skills/fui/SKILL.md +149 -66
  5. package/skills/fui-skill/README.md +112 -0
  6. package/skills/fui-skill/SKILL.md +269 -0
  7. package/skills/fui-skill/assets/projectdefaultstyle.css +518 -0
  8. package/skills/fui-skill/design-md/airbnb/DESIGN.md +545 -0
  9. package/skills/fui-skill/design-md/airbnb/README.md +5 -0
  10. package/skills/fui-skill/design-md/airtable/DESIGN.md +554 -0
  11. package/skills/fui-skill/design-md/airtable/README.md +5 -0
  12. package/skills/fui-skill/design-md/apple/DESIGN.md +562 -0
  13. package/skills/fui-skill/design-md/apple/README.md +5 -0
  14. package/skills/fui-skill/design-md/asu/DESIGN.md +179 -0
  15. package/skills/fui-skill/design-md/asu/README.md +107 -0
  16. package/skills/fui-skill/design-md/binance/DESIGN.md +634 -0
  17. package/skills/fui-skill/design-md/binance/README.md +5 -0
  18. package/skills/fui-skill/design-md/bmw/DESIGN.md +544 -0
  19. package/skills/fui-skill/design-md/bmw/README.md +5 -0
  20. package/skills/fui-skill/design-md/bmw-m/DESIGN.md +503 -0
  21. package/skills/fui-skill/design-md/bmw-m/README.md +5 -0
  22. package/skills/fui-skill/design-md/bugatti/DESIGN.md +454 -0
  23. package/skills/fui-skill/design-md/bugatti/README.md +5 -0
  24. package/skills/fui-skill/design-md/cal/DESIGN.md +542 -0
  25. package/skills/fui-skill/design-md/cal/README.md +5 -0
  26. package/skills/fui-skill/design-md/claude/DESIGN.md +589 -0
  27. package/skills/fui-skill/design-md/claude/README.md +5 -0
  28. package/skills/fui-skill/design-md/clay/DESIGN.md +541 -0
  29. package/skills/fui-skill/design-md/clay/README.md +5 -0
  30. package/skills/fui-skill/design-md/clickhouse/DESIGN.md +544 -0
  31. package/skills/fui-skill/design-md/clickhouse/README.md +5 -0
  32. package/skills/fui-skill/design-md/cohere/DESIGN.md +451 -0
  33. package/skills/fui-skill/design-md/cohere/README.md +5 -0
  34. package/skills/fui-skill/design-md/coinbase/DESIGN.md +570 -0
  35. package/skills/fui-skill/design-md/coinbase/README.md +5 -0
  36. package/skills/fui-skill/design-md/composio/DESIGN.md +506 -0
  37. package/skills/fui-skill/design-md/composio/README.md +5 -0
  38. package/skills/fui-skill/design-md/cursor/DESIGN.md +537 -0
  39. package/skills/fui-skill/design-md/cursor/README.md +5 -0
  40. package/skills/fui-skill/design-md/elevenlabs/DESIGN.md +504 -0
  41. package/skills/fui-skill/design-md/elevenlabs/README.md +5 -0
  42. package/skills/fui-skill/design-md/expo/DESIGN.md +526 -0
  43. package/skills/fui-skill/design-md/expo/README.md +5 -0
  44. package/skills/fui-skill/design-md/ferrari/DESIGN.md +531 -0
  45. package/skills/fui-skill/design-md/ferrari/README.md +5 -0
  46. package/skills/fui-skill/design-md/figma/DESIGN.md +578 -0
  47. package/skills/fui-skill/design-md/figma/README.md +5 -0
  48. package/skills/fui-skill/design-md/framer/DESIGN.md +544 -0
  49. package/skills/fui-skill/design-md/framer/README.md +5 -0
  50. package/skills/fui-skill/design-md/fui/DESIGN.md +532 -0
  51. package/skills/fui-skill/design-md/hashicorp/DESIGN.md +575 -0
  52. package/skills/fui-skill/design-md/hashicorp/README.md +5 -0
  53. package/skills/fui-skill/design-md/ibm/DESIGN.md +550 -0
  54. package/skills/fui-skill/design-md/ibm/README.md +5 -0
  55. package/skills/fui-skill/design-md/intercom/DESIGN.md +546 -0
  56. package/skills/fui-skill/design-md/intercom/README.md +5 -0
  57. package/skills/fui-skill/design-md/kraken/DESIGN.md +125 -0
  58. package/skills/fui-skill/design-md/kraken/README.md +5 -0
  59. package/skills/fui-skill/design-md/lamborghini/DESIGN.md +288 -0
  60. package/skills/fui-skill/design-md/lamborghini/README.md +5 -0
  61. package/skills/fui-skill/design-md/linear.app/DESIGN.md +548 -0
  62. package/skills/fui-skill/design-md/linear.app/README.md +5 -0
  63. package/skills/fui-skill/design-md/lovable/DESIGN.md +298 -0
  64. package/skills/fui-skill/design-md/lovable/README.md +5 -0
  65. package/skills/fui-skill/design-md/mastercard/DESIGN.md +365 -0
  66. package/skills/fui-skill/design-md/mastercard/README.md +5 -0
  67. package/skills/fui-skill/design-md/meta/DESIGN.md +683 -0
  68. package/skills/fui-skill/design-md/meta/README.md +5 -0
  69. package/skills/fui-skill/design-md/minimax/DESIGN.md +746 -0
  70. package/skills/fui-skill/design-md/minimax/README.md +5 -0
  71. package/skills/fui-skill/design-md/mintlify/DESIGN.md +852 -0
  72. package/skills/fui-skill/design-md/mintlify/README.md +5 -0
  73. package/skills/fui-skill/design-md/miro/DESIGN.md +825 -0
  74. package/skills/fui-skill/design-md/miro/README.md +5 -0
  75. package/skills/fui-skill/design-md/mistral.ai/DESIGN.md +773 -0
  76. package/skills/fui-skill/design-md/mistral.ai/README.md +5 -0
  77. package/skills/fui-skill/design-md/mongodb/DESIGN.md +767 -0
  78. package/skills/fui-skill/design-md/mongodb/README.md +5 -0
  79. package/skills/fui-skill/design-md/nike/DESIGN.md +575 -0
  80. package/skills/fui-skill/design-md/nike/README.md +5 -0
  81. package/skills/fui-skill/design-md/notion/DESIGN.md +821 -0
  82. package/skills/fui-skill/design-md/notion/README.md +5 -0
  83. package/skills/fui-skill/design-md/nvidia/DESIGN.md +640 -0
  84. package/skills/fui-skill/design-md/nvidia/README.md +5 -0
  85. package/skills/fui-skill/design-md/ollama/DESIGN.md +539 -0
  86. package/skills/fui-skill/design-md/ollama/README.md +5 -0
  87. package/skills/fui-skill/design-md/opencode.ai/DESIGN.md +521 -0
  88. package/skills/fui-skill/design-md/opencode.ai/README.md +5 -0
  89. package/skills/fui-skill/design-md/pinterest/DESIGN.md +597 -0
  90. package/skills/fui-skill/design-md/pinterest/README.md +5 -0
  91. package/skills/fui-skill/design-md/playstation/DESIGN.md +661 -0
  92. package/skills/fui-skill/design-md/playstation/README.md +5 -0
  93. package/skills/fui-skill/design-md/posthog/DESIGN.md +690 -0
  94. package/skills/fui-skill/design-md/posthog/README.md +5 -0
  95. package/skills/fui-skill/design-md/raycast/DESIGN.md +669 -0
  96. package/skills/fui-skill/design-md/raycast/README.md +5 -0
  97. package/skills/fui-skill/design-md/renault/DESIGN.md +589 -0
  98. package/skills/fui-skill/design-md/renault/README.md +5 -0
  99. package/skills/fui-skill/design-md/replicate/DESIGN.md +616 -0
  100. package/skills/fui-skill/design-md/replicate/README.md +5 -0
  101. package/skills/fui-skill/design-md/resend/DESIGN.md +585 -0
  102. package/skills/fui-skill/design-md/resend/README.md +5 -0
  103. package/skills/fui-skill/design-md/revolut/DESIGN.md +636 -0
  104. package/skills/fui-skill/design-md/revolut/README.md +5 -0
  105. package/skills/fui-skill/design-md/runwayml/DESIGN.md +244 -0
  106. package/skills/fui-skill/design-md/runwayml/README.md +5 -0
  107. package/skills/fui-skill/design-md/sanity/DESIGN.md +357 -0
  108. package/skills/fui-skill/design-md/sanity/README.md +5 -0
  109. package/skills/fui-skill/design-md/sentry/DESIGN.md +551 -0
  110. package/skills/fui-skill/design-md/sentry/README.md +5 -0
  111. package/skills/fui-skill/design-md/shopify/DESIGN.md +516 -0
  112. package/skills/fui-skill/design-md/shopify/README.md +5 -0
  113. package/skills/fui-skill/design-md/slack/DESIGN.md +482 -0
  114. package/skills/fui-skill/design-md/spacex/DESIGN.md +363 -0
  115. package/skills/fui-skill/design-md/spacex/README.md +5 -0
  116. package/skills/fui-skill/design-md/spotify/DESIGN.md +246 -0
  117. package/skills/fui-skill/design-md/spotify/README.md +5 -0
  118. package/skills/fui-skill/design-md/starbucks/DESIGN.md +580 -0
  119. package/skills/fui-skill/design-md/starbucks/README.md +5 -0
  120. package/skills/fui-skill/design-md/stripe/DESIGN.md +487 -0
  121. package/skills/fui-skill/design-md/stripe/README.md +5 -0
  122. package/skills/fui-skill/design-md/supabase/DESIGN.md +462 -0
  123. package/skills/fui-skill/design-md/supabase/README.md +5 -0
  124. package/skills/fui-skill/design-md/superhuman/DESIGN.md +448 -0
  125. package/skills/fui-skill/design-md/superhuman/README.md +5 -0
  126. package/skills/fui-skill/design-md/tesla/DESIGN.md +286 -0
  127. package/skills/fui-skill/design-md/tesla/README.md +5 -0
  128. package/skills/fui-skill/design-md/theverge/DESIGN.md +339 -0
  129. package/skills/fui-skill/design-md/theverge/README.md +5 -0
  130. package/skills/fui-skill/design-md/together.ai/DESIGN.md +633 -0
  131. package/skills/fui-skill/design-md/together.ai/README.md +5 -0
  132. package/skills/fui-skill/design-md/uber/DESIGN.md +636 -0
  133. package/skills/fui-skill/design-md/uber/README.md +5 -0
  134. package/skills/fui-skill/design-md/vercel/DESIGN.md +736 -0
  135. package/skills/fui-skill/design-md/vercel/README.md +5 -0
  136. package/skills/fui-skill/design-md/vodafone/DESIGN.md +538 -0
  137. package/skills/fui-skill/design-md/vodafone/README.md +5 -0
  138. package/skills/fui-skill/design-md/voltagent/DESIGN.md +521 -0
  139. package/skills/fui-skill/design-md/voltagent/README.md +5 -0
  140. package/skills/fui-skill/design-md/warp/DESIGN.md +526 -0
  141. package/skills/fui-skill/design-md/warp/README.md +5 -0
  142. package/skills/fui-skill/design-md/webflow/DESIGN.md +588 -0
  143. package/skills/fui-skill/design-md/webflow/README.md +5 -0
  144. package/skills/fui-skill/design-md/wired/DESIGN.md +497 -0
  145. package/skills/fui-skill/design-md/wired/README.md +5 -0
  146. package/skills/fui-skill/design-md/wise/DESIGN.md +544 -0
  147. package/skills/fui-skill/design-md/wise/README.md +5 -0
  148. package/skills/fui-skill/design-md/x.ai/DESIGN.md +465 -0
  149. package/skills/fui-skill/design-md/x.ai/README.md +5 -0
  150. package/skills/fui-skill/design-md/zapier/DESIGN.md +537 -0
  151. package/skills/fui-skill/design-md/zapier/README.md +5 -0
  152. package/skills/fui-skill/examples/component.vue +162 -0
  153. package/skills/fui-skill/examples/f-table-patterns.json +331 -0
  154. package/skills/fui-skill/examples/module-patterns.json +973 -0
  155. package/skills/fui-skill/examples/project-patterns.json +222 -0
  156. package/skills/fui-skill/metadata.json +75 -0
  157. package/skills/fui-skill/references/INDEX.md +144 -0
  158. package/skills/fui-skill/references/advanced-techniques.md +160 -0
  159. package/skills/fui-skill/references/coding-standards.md +112 -0
  160. package/skills/fui-skill/references/component-design.md +455 -0
  161. package/skills/fui-skill/references/component-quickref.md +77 -0
  162. package/skills/fui-skill/references/component-table.md +276 -0
  163. package/skills/fui-skill/references/components-dialog.md +192 -0
  164. package/skills/fui-skill/references/components-display.md +147 -0
  165. package/skills/fui-skill/references/components-echart.md +391 -0
  166. package/skills/fui-skill/references/components-input.md +359 -0
  167. package/skills/fui-skill/references/controls-patterns.md +847 -0
  168. package/skills/fui-skill/references/controls-styling-vocabulary.md +140 -0
  169. package/skills/fui-skill/references/db-table-design.md +77 -0
  170. package/skills/fui-skill/references/db-workflow.md +504 -0
  171. package/skills/fui-skill/references/default-function.md +415 -0
  172. package/skills/fui-skill/references/design-modes.md +85 -0
  173. package/skills/fui-skill/references/echart-templates.md +481 -0
  174. package/skills/fui-skill/references/fastproject.md +97 -0
  175. package/skills/fui-skill/references/fsheet.md +218 -0
  176. package/skills/fui-skill/references/fullstack-workflow.md +351 -0
  177. package/skills/fui-skill/references/module-data-patterns.md +126 -0
  178. package/skills/fui-skill/references/module-json-anatomy.md +137 -0
  179. package/skills/fui-skill/references/module-structure.md +260 -0
  180. package/skills/fui-skill/references/new-session.md +108 -0
  181. package/skills/fui-skill/references/pdfmake.md +60 -0
  182. package/skills/fui-skill/references/permission-system.md +169 -0
  183. package/skills/fui-skill/references/platform-architecture.md +294 -0
  184. package/skills/fui-skill/references/project-config.md +335 -0
  185. package/skills/fui-skill/references/project-provisioning.md +383 -0
  186. package/skills/fui-skill/references/script-map.md +296 -0
  187. package/skills/fui-skill/references/sql-clr-functions.md +224 -0
  188. package/skills/fui-skill/references/system-design.md +116 -0
  189. package/skills/fui-skill/references/tapi-file-api.md +191 -0
  190. package/skills/fui-skill/references/tapi-permission-patterns.md +158 -0
  191. package/skills/fui-skill/references/tapi-reference.md +549 -0
  192. package/skills/fui-skill/references/tools-registry.md +460 -0
  193. package/skills/fui-skill/references/ui-crosswindow-patterns.md +317 -0
  194. package/skills/fui-skill/references/ui-dialog-patterns.md +229 -0
  195. package/skills/fui-skill/references/ui-layout-patterns.md +176 -0
  196. package/skills/fui-skill/references/ui-patterns.md +315 -0
  197. package/skills/fui-skill/references/ui-screenshot-review.md +94 -0
  198. package/skills/fui-skill/references/ui-table-cell-patterns.md +316 -0
  199. package/skills/fui-skill/references/ui-templates.md +29 -0
  200. package/skills/fui-skill/references/verification.md +246 -0
  201. package/skills/fui-skill/references/watcher-patterns.md +196 -0
  202. package/skills/fui-skill/references/websocket-realtime.md +254 -0
  203. package/skills/fui-skill/scripts/component-3.0.js +2549 -0
  204. package/skills/fui-skill/scripts/component.js +3142 -0
  205. package/skills/fui-skill/scripts/componentTable-3.0.js +909 -0
  206. package/skills/fui-skill/scripts/componentTable.js +769 -0
  207. package/skills/fui-skill/scripts/defaultfunction-3.0.js +781 -0
  208. package/skills/fui-skill/scripts/defaultfunction.js +966 -0
  209. package/skills/fui-skill/scripts/fastproject-3.0.js +870 -0
  210. package/skills/fui-skill/scripts/fastproject.js +828 -0
  211. package/skills/fui-skill/scripts/fechart.js +890 -0
  212. package/skills/fui-skill/scripts/fsheet.js +1330 -0
@@ -0,0 +1,2549 @@
1
+ var vuetifyDefaultSetting = {
2
+ global: {
3
+ ripple: false, // Tắt hiệu ứng ripple trên toàn bộ ứng dụng
4
+ },
5
+ options: {
6
+ customProperties: true, // Kích hoạt CSS Variables
7
+ "--v-medium-emphasis-opacity": 1, // Kích hoạt CSS Variables
8
+ },
9
+ defaults: {
10
+ VBtn: {
11
+ variant: "outlined",
12
+ },
13
+ VTextField: {
14
+ variant: "outlined",
15
+ density: "compact",
16
+ hideDetails: true,
17
+ },
18
+ VTextarea: {
19
+ variant: "outlined",
20
+ density: "compact",
21
+ hideDetails: true,
22
+ },
23
+ VCombobox: {
24
+ variant: "outlined",
25
+ density: "compact",
26
+ hideDetails: true,
27
+ returnObject: false
28
+ },
29
+ VSelect: {
30
+ variant: "outlined",
31
+ density: "compact",
32
+ hideDetails: true,
33
+ returnObject: false,
34
+ },
35
+ VAutocomplete: {
36
+ variant: "outlined",
37
+ density: "compact",
38
+ hideDetails: true,
39
+ returnObject: false,
40
+ autoSelectFirst: true
41
+ },
42
+ VCheckbox: {
43
+ hideDetails: true,
44
+ density: "compact"
45
+ },
46
+ VSwitch: {
47
+ density: "compact",
48
+ hideDetails: true,
49
+ },
50
+ VCard: {
51
+ },
52
+ VToolbar: {
53
+ density: "compact",
54
+ },
55
+ VCardActions: {
56
+ density: "compact",
57
+ },
58
+ VAlert: {
59
+ density: "compact",
60
+ hideDetails: true,
61
+ },
62
+ VDataTable: {
63
+ density: "compact",
64
+ showSearch: true,
65
+ },
66
+ VDateInput: {
67
+ variant: "outlined",
68
+ density: "compact",
69
+ hideDetails: true,
70
+ },
71
+ }
72
+ }
73
+
74
+ var defaultControlAttr = {
75
+ "v-text-field": {
76
+ attr: {
77
+ ":required": true,
78
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]"
79
+ }
80
+ },
81
+ "v-combobox": {
82
+ attr: {
83
+ ":required": true,
84
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]",
85
+ }
86
+ },
87
+ "v-select": {
88
+ attr: {
89
+ ":required": true,
90
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]",
91
+ }
92
+ },
93
+ "v-autocomplete": {
94
+ attr: {
95
+ ":required": true,
96
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]",
97
+ }
98
+ },
99
+ //----------------------------------------------
100
+ "v-jsoneditor": {
101
+ import: ["/include/jsonEditor/v-jsoneditor.min.js"]
102
+ },
103
+
104
+ "f-button": {
105
+ col: {
106
+ align: ($isPhone ? "end" : null)
107
+ },
108
+ },
109
+
110
+ "f-radiobox": {
111
+ attr: {
112
+ ":required": true,
113
+ ":rules": "[v => (!!v || !isNaN(parseFloat(v)))]"
114
+ }
115
+ },
116
+ "f-menu": {
117
+ col: {
118
+ align: ($isPhone ? "end" : null)
119
+ },
120
+ },
121
+ "f-sheet": {
122
+ import: ["/include/ag-grid/ag-grid-community.min.js", "/include/ag-grid/fsheet.js"]
123
+ },
124
+ "f-pdfmake": {
125
+ attr: {
126
+ "width": "100%",
127
+ "height": "500px"
128
+ },
129
+ import: ['/include/pdfmake/pdfmake.min.js', '/include/pdfmake/print.min.js', '/include/pdfmake/vfs_fonts.js']
130
+ },
131
+ "f-chart": {
132
+ import: ["/include/chart/chart.min.js"]
133
+ },
134
+ "f-echart": {
135
+ import: ["/include/chart/echarts.min.js", "/include/chart/fechart.js"]
136
+ },
137
+ "f-code-editor": {
138
+ import: [
139
+ "/include/monaco-editor/min/vs/loader.js",
140
+ ]
141
+ },
142
+ "f-qrcode": {
143
+ import: [
144
+ "/include/qrcode/qrcode.min.js",
145
+ ],
146
+ },
147
+ "f-qrcode-reader": {
148
+ import: [
149
+ "/include/qrcode/qrcode-reader.min.js",
150
+ ],
151
+ },
152
+
153
+ "f-file-upload": {
154
+ import: [
155
+ "/include/plupload-2.3.7/plupload.full.min.js",
156
+ ],
157
+ },
158
+ "f-image-update": {
159
+ import: [
160
+ "https://unpkg.com/vue-advanced-cropper@^1.0.0/dist/index.umd.js",
161
+ "https://unpkg.com/vue-advanced-cropper@^2.0.0/dist/style.css",
162
+ ],
163
+ },
164
+ "f-q-and-a": {
165
+ import: [
166
+ "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML",
167
+ "/include/QandA/q-and-a-ckedit.css",
168
+ "/include/vue-context-menu.js",
169
+ "/include/layoutEditor/Sortable.min.js",
170
+ "/include/layoutEditor/vuedraggable.umd.min.js",
171
+ "/include/ckeditor5/build/ckeditor.js",
172
+ "/include/plupload-2.3.7/plupload.full.min.js",
173
+ "/include/audio/vueaudio.js",
174
+ "/include/QandA/QandA.js",
175
+ ],
176
+ },
177
+ "f-etest-review": {
178
+ import: [
179
+ "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML",
180
+ "/include/QandA/q-and-a-ckedit.css",
181
+ "/include/vue-context-menu.js",
182
+ "/include/audio/vueaudio.js",
183
+ "/include/QandA/QandA.js",
184
+ ],
185
+ },
186
+ "f-audio": {
187
+ import: [
188
+ "/include/audio/vueaudio.js",
189
+ ],
190
+ },
191
+ "f-audio-recorder": {
192
+ import: [
193
+ "/include/audio/vueaudio.js",
194
+ ],
195
+ },
196
+ "f-editor": {
197
+ import: [
198
+ "/include/ckeditor5/build/ckeditor.js"
199
+ ],
200
+ },
201
+ "f-editor-dialog": {
202
+ import: [
203
+ "/include/ckeditor5/build/ckeditor.js"
204
+ ],
205
+ },
206
+ "f-excel-reader": {
207
+ col: {
208
+ align: ($isPhone ? "end" : null),
209
+ }
210
+ },
211
+ "f-google-login": {
212
+ import: [
213
+ "https://apis.google.com/js/platform.js"
214
+ ],
215
+ }
216
+ };
217
+ //===========================================================================================
218
+ //===========================================================================================
219
+ //===========================================================================================
220
+ Vue.component("FDynamicComponent", {
221
+ template: `<component :is="dynamicComponent" @update:innerDataOut="updateDataOut"></component>`,
222
+ props: {
223
+ componentData: {
224
+ type: Object,
225
+ default: () => ({}),
226
+ },
227
+ data: {
228
+ type: Object,
229
+ default: {},
230
+ },
231
+ },
232
+ methods: {
233
+ updateDataOut(newValue) {
234
+ this.$emit('update:dataOut', newValue);
235
+ }
236
+ },
237
+ computed: {
238
+ dynamicComponent() {
239
+ return { ...this.componentData }
240
+ },
241
+ },
242
+
243
+ });
244
+ // //===========================================================================================
245
+ // Vue.component("FOpenwindowControl", {
246
+ // template: `<component v-for="(w,index) in windowList" v-bind:is="w.el" v-bind="w.attr">{{w.innerHTML}}</component>`,
247
+ // props: {
248
+ // windowList: {
249
+ // default: [],
250
+ // required: true
251
+ // },
252
+ // },
253
+ // });
254
+
255
+ //===========================================================================================
256
+ Vue.component('FLabel', {
257
+ props: ['items', "text", "color", 'iconText'],
258
+ template: `
259
+ <div>
260
+ <v-chip v-bind="$attrs" class="mr-2" v-if="text">
261
+ <v-icon v-if="iconText" left>{{iconText}}</v-icon>{{text}}
262
+ </v-chip>
263
+ <v-chip v-bind="$attrs" v-for="(chip, k) in (typeof items === 'string'?JSON.parse(items):items)" :key="k" :color="(color ? color : chip.color)" class="mr-2" >
264
+ <v-icon v-if="chip['icon-text']" left>{{chip['icon-text']}}</v-icon>{{chip.text}}
265
+ </v-chip>
266
+ </div>
267
+ `
268
+ });
269
+
270
+ //===========================================================================================
271
+ Vue.component('FBox', {
272
+ props: ['rowFormat', 'items', 'label', 'edit', 'saveclick'],
273
+ template: `
274
+ <v-card v-bind="$attrs">
275
+ <v-list-item>
276
+ <v-list-item-title v-if="label" class="headline mb-2">{{label}}</v-list-item-title><br>
277
+ <table style="flex: initial; width:100%;" v-for="R in itemsObj">
278
+ <tr v-for="c in rowFormatArray">
279
+ <td class="py-1 text-no-wrap text-left">{{c.text}}</td>
280
+ <td class="py-1 px-2 text-center"><span v-if="!(edit && !c.noEdit)" >:</span></td>
281
+ <td v-if="!(edit && !c.noEdit)" class="py-1 text-left" style="width: 100%"><b>{{R[c.value]}}</b></td>
282
+ <td v-if="edit && !c.noEdit" class="py-1 text-left" style="width: 100%">
283
+ <v-text-field density="compact" variant="outlined" hide-details v-model="R[c.value]"></v-text-field>
284
+ </td>
285
+ </tr>
286
+ </table>
287
+ </v-list-item>
288
+ </v-card>
289
+ `,
290
+ data: function () {
291
+ return {
292
+ rowFormatArray: [],
293
+ itemsObj: [],
294
+ itemsIsArray: _.isArray(this.items)
295
+ }
296
+ },
297
+ mounted: function () {
298
+ this.buildDataItems();
299
+ },
300
+ watch: {
301
+ items: function (newVal, oldVal) {
302
+ this.buildDataItems();
303
+ },
304
+ rowFormat: function (newVal, oldVal) {
305
+ this.buildDataItems();
306
+ },
307
+ itemsObj: function (newVal, oldVal) {
308
+ if (this.itemsIsArray) this.$emit('update:modelValue', this.itemsObj);
309
+ else if (this.itemsObj.length > 0) this.$emit('update:modelValue', this.itemsObj[0]);
310
+ else this.$emit('update:modelValue', null);
311
+ }
312
+ },
313
+ methods: {
314
+ buildDataItems: function () {
315
+ if (this.items) {
316
+ if (!_.isArray(this.items)) this.itemsObj = _.castArray(this.items);
317
+ else this.itemsObj = this.items;
318
+ }
319
+
320
+ if (this.rowFormat) this.rowFormatArray = this.rowFormat;
321
+ else this.rowFormatArray = this.buildHeader(this.itemsObj);
322
+ },
323
+ buildHeader: function (obj) {
324
+ var datatable = {};
325
+ var header = [];
326
+ if (obj && _.isArray(obj)) datatable = obj[0];
327
+ else if (obj && _.isArray(obj.data)) datatable = obj.data[0];
328
+
329
+ if (datatable) {
330
+ _.forEach(datatable, function (value, key) {
331
+ if (_.isNumber(value)) header.push({ "el": "t-num", "text": key, "value": key, "divider": true });
332
+ else header.push({ "text": key, "value": key, "divider": true });
333
+ });
334
+ }
335
+ return header;
336
+ }
337
+ }
338
+ });
339
+ //===========================================================================================
340
+
341
+ Vue.component('FHeader', {
342
+ props: ['label'],
343
+ template: `<div class="text-h4 text-center text-blue-darken-3" v-html="label"></div>`
344
+ });
345
+
346
+ //===========================================================================================
347
+
348
+ Vue.component('FTitle', {
349
+ props: ['label'],
350
+ template: `<div class="text-h6 text-center text-blue-darken-2" v-html="label"></div>`
351
+ });
352
+
353
+ //===========================================================================================
354
+
355
+ Vue.component('FRadiobox', {
356
+ props: {
357
+ label: {
358
+ type: String,
359
+ default: undefined
360
+ },
361
+ items: {
362
+ type: Array,
363
+ default: []
364
+ },
365
+ itemValue: {
366
+ type: String,
367
+ default: 'value'
368
+ },
369
+ itemText: {
370
+ type: String,
371
+ default: 'text'
372
+ }
373
+ },
374
+ template: `<v-radio-group v-bind="$attrs" v-on:change="$emit('update:modelValue', $event)"><template v-slot:label><div class="text-body-1">{{label}}</div></template><v-radio v-for="n in items" :key="n[itemValue]" :label="n[itemText]" :value="n[itemValue]"></v-radio></v-radio-group>`,
375
+ });
376
+ //===========================================================================================
377
+
378
+ Vue.component('FMenu', {
379
+ props: {
380
+ label: {
381
+ type: String,
382
+ default: undefined
383
+ },
384
+ items: {
385
+ type: Array,
386
+ default: []
387
+ },
388
+ iconText: {
389
+ type: String,
390
+ default: undefined
391
+ },
392
+ origin: {
393
+ type: String,
394
+ default: "top center"
395
+ }
396
+ },
397
+ template: `<v-menu offset-y :origin="origin" transition="slide-y-transition" max-height = 500>
398
+ <template v-slot:activator="{ props }"><v-icon color="primary" v-bind="props" :icon="iconText"></v-icon></template>
399
+ <v-list >
400
+ <v-list-item v-bind="$attrs" v-for="(item, index) in items" :key="index" @click="onSelect(item)" >
401
+ <template v-slot:prepend><v-icon :color="item.color" :icon="item['icon-text']"></v-icon></template>
402
+ <v-list-item-title v-text="item.text"></v-list-item-title>
403
+ </v-list-item>
404
+ </v-list>
405
+ </v-menu>`,
406
+ data: function () {
407
+ return {
408
+ dataList: this.dataFix(this.items)
409
+ }
410
+ },
411
+ watch: {
412
+ items: function (newVal) {
413
+ this.dataList = this.dataFix(newVal)
414
+ }
415
+ },
416
+ methods: {
417
+ buttonclick: function (e) {
418
+ if (e.action) runAction(e.action);
419
+ },
420
+ dataFix: function (d) {
421
+ _.forEach(d, function (o) {
422
+ if (_.isUndefined(o.visible)) {
423
+ o.visible = true;
424
+ }
425
+ mapData(o, o, vueData);
426
+ });
427
+ return d;
428
+ }
429
+
430
+ }
431
+ });
432
+
433
+ //===========================================================================================
434
+ Vue.component('FSearch', {
435
+ props: {
436
+ api: {
437
+ type: String,
438
+ default: undefined
439
+ },
440
+ apiData: {
441
+ type: Object,
442
+ default: {}
443
+ },
444
+ items: {
445
+ type: Array,
446
+ default: []
447
+ },
448
+ },
449
+ template: `<v-autocomplete v-bind="$attrs" :loading="loading" :items="dataItems" @update:search="onChange"></v-autocomplete>`,
450
+ data: function () {
451
+ return {
452
+ loading: false,
453
+ dataItems: this.items,
454
+ getDataFormAPI: _.debounce((search) => {
455
+ if (search && search.length > 1) {
456
+ var self = this;
457
+ this.loading = true
458
+
459
+ if (typeof this.apiData != 'object') this.apiData = JSON.parse(this.apiData);
460
+
461
+ let inputdata = {}
462
+ mapData(this.apiData, inputdata, _.assignIn({}, vueData, { TEXT: search }));
463
+
464
+ ajaxCALL(this.api, inputdata, function (d) {
465
+ if (_.isArray(d.data)) self.dataItems = d.data;
466
+ else self.dataItems = d;
467
+
468
+ self.loading = false
469
+ }, function (e) {
470
+ console.log("API Error", e);
471
+ })
472
+ }
473
+ }, 500)
474
+ }
475
+ },
476
+ watch: {
477
+ items(val) {
478
+ this.dataItems = val
479
+ },
480
+ },
481
+ methods: {
482
+ onChange(e) {
483
+ this.getDataFormAPI(e);
484
+ },
485
+ },
486
+ });
487
+ //===========================================================================================
488
+ Vue.component('FButton', {
489
+ props: {
490
+ label: {
491
+ type: String,
492
+ default: 'OK'
493
+ },
494
+ checkvalid: {
495
+ type: Boolean,
496
+ default: true
497
+ },
498
+ ctrlhotkey: {
499
+ type: Number,
500
+ default: 0
501
+ },
502
+ iconText: {
503
+ default: undefined
504
+ },
505
+ action: {
506
+ default: undefined
507
+ },
508
+ includeData: {
509
+ default: undefined
510
+ }
511
+ },
512
+ template: `<v-btn v-on:click="buttonclick" :prepend-icon="iconText" :text="label" v-bind="$attrs"></v-btn>`,
513
+ mounted: function () {
514
+ if (this.ctrlhotkey > 0) {
515
+ document.addEventListener("keydown", this.keydownHotKey, false);
516
+ }
517
+ },
518
+ destroyed() {
519
+ document.removeEventListener("keydown", this.keydownHotKey, false);
520
+ },
521
+ methods: {
522
+ buttonclick: _.debounce(async function (e) {
523
+ var attr = this.$attrs;
524
+ if (attr.target == 'dialog') {
525
+ console.log("dialog onclose", attr.onclose)
526
+ openWindow({
527
+ id: attr.wid,
528
+ title: attr.title,
529
+ url: attr.url,
530
+ onclose: attr.onclose
531
+ });
532
+ return
533
+ }
534
+
535
+ if (this.checkvalid && !await app._component.methods.formvalidate()) {
536
+ if (attr.type != 'submit') showMessage({ icon: 'mdi mdi-alert-outline', type: "red", title: "Hãy điền đầy đủ dữ liệu bắt buộc !" })
537
+ return;
538
+ }
539
+
540
+ if (this.action) runAction(this.action, this.includeData);
541
+ else console.log("action:", this.action);
542
+
543
+ }, 500, { leading: true, trailing: false }),
544
+ keydownHotKey: (e) => {
545
+ if ((window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == this.ctrlhotkey) {
546
+ e.preventDefault();
547
+ this.buttonclick();
548
+ console.log("Button hotkey :", this.label);
549
+ }
550
+ }
551
+ }
552
+ });
553
+
554
+ //===========================================================================================
555
+
556
+ Vue.component('FFileUpload', {
557
+ props: {
558
+ label: {
559
+ type: String,
560
+ default: 'file upload'
561
+ },
562
+ url: {
563
+ type: String,
564
+ default: ''
565
+ },
566
+ autoclose: {
567
+ type: Boolean,
568
+ default: true
569
+ },
570
+ iconText: {
571
+ type: String,
572
+ default: "mdi-cloud-upload-outline"
573
+ },
574
+ filters: {
575
+ type: Object,
576
+ default: {
577
+ max_file_size: '5mb',
578
+ mime_types: [{ title: "All files", extensions: "*" }]
579
+ }
580
+ },
581
+ resize: {
582
+ type: Object,
583
+ default: {
584
+ width: 1900,
585
+ height: 1600,
586
+ quality: 80
587
+ }
588
+ },
589
+ onclose: {
590
+ default: undefined
591
+ },
592
+
593
+ },
594
+ template: `
595
+ <v-btn :id="elID" v-bind="$attrs">
596
+ <v-icon v-if="iconText" :left="label != ''">{{iconText}}</v-icon>{{label}}
597
+ <v-dialog v-model="dialog" persistent width="450">
598
+ <v-card>
599
+ <v-card-actions>
600
+ Upload file
601
+ <v-spacer></v-spacer>
602
+ <v-btn :loading="loading" icon="mdi-close" @click="dialog = false; UploadingError = []; UploadingFile = [];" ></v-btn>
603
+ </v-card-actions>
604
+ <v-sheet v-if="UploadingError.length>0" color="error" dark class="py-2">
605
+ <v-card-text class="py-1 px-4" v-for="(t, i) in UploadingError" :key="i">
606
+ <div><b>{{t.file.name}}</b></div>
607
+ <div>{{t.message}}</div>
608
+ </v-card-text>
609
+ </v-sheet>
610
+ <v-sheet v-if="UploadingFile.length>0" class="pb-2">
611
+ <v-card-text class="py-1 px-4" v-for="(t, i) in UploadingFile" :key="i">
612
+ <v-card-subtitle class="pa-0" >{{t.percent}}% - {{t.name}}</v-card-subtitle>
613
+ <v-progress-linear :value="t.percent" class="mb-2 mt-1"></v-progress-linear>
614
+ </v-card-text>
615
+ </v-sheet>
616
+ </v-card>
617
+ </v-dialog>
618
+ </v-btn>
619
+ `,
620
+ data: function () {
621
+ return {
622
+ dialog: false,
623
+ loading: true,
624
+ UploadingFile: [],
625
+ UploadingError: [],
626
+ elID: generateID(),
627
+ uploader: null,
628
+ filesUploaded: []
629
+ }
630
+ },
631
+ mounted: function () {
632
+ this.uploaderInit();
633
+ },
634
+ methods: {
635
+ uploaderInit: function () {
636
+ var thisObj = this;
637
+
638
+ var uploader = new plupload.Uploader({
639
+ runtimes: 'html5,html4',
640
+ browse_button: thisObj.elID,
641
+ headers: {
642
+ authorization: 'Bearer ' + getCookie('awt'),
643
+ uploaddata: Date.now()
644
+ },
645
+ resize: thisObj.resize,
646
+ filters: thisObj.filters,
647
+ init: {
648
+ PostInit: function () { },
649
+ OptionChanged: function (up, name, value, oldValue) { },
650
+ BeforeUpload: function (up, file) {
651
+ uploader.settings.url = thisObj.url;
652
+ },
653
+ FilesAdded: function (up, files) {
654
+ thisObj.dialog = true;
655
+ thisObj.UploadingFile = files;
656
+ thisObj.filesUploaded = [];
657
+ uploader.start();
658
+ },
659
+ FileUploaded: function (up, file, result) {
660
+ result = JSON.parse(result.response);
661
+ json_data_parse(result, 0);
662
+ thisObj.filesUploaded = result;
663
+ },
664
+ UploadComplete: function (up, files) {
665
+ if (thisObj.filesUploaded) thisObj.$emit('update:modelValue', thisObj.filesUploaded);
666
+ else thisObj.$emit('update:modelValue', files);
667
+
668
+ if (thisObj.UploadingError.length == 0 && thisObj.autoclose) thisObj.dialog = false;
669
+ thisObj.loading = false;
670
+ },
671
+ UploadProgress: function (up, file) { },
672
+ Error: function (up, err) {
673
+ thisObj.loading = false;
674
+ if (err.status == 500) {
675
+ err.message = JSON.parse(err.response).message;
676
+ }
677
+ console.log(err);
678
+ thisObj.dialog = true;
679
+ thisObj.UploadingError.push(err);
680
+ }
681
+ }
682
+ });
683
+ uploader.init();
684
+ thisObj.uploader = uploader;
685
+ }
686
+ },
687
+ watch: {
688
+ dialog: function (newVal, oldVal) {
689
+ if (!newVal && this.onclose) {
690
+ runAction(this.onclose);
691
+ }
692
+ },
693
+ filters: {
694
+ deep: true,
695
+ handler(newVal) {
696
+ this.uploader.setOption("filters", newVal)
697
+ var ext = newVal.mime_types[0].extensions.replace(/([ ]*)/gm, "")
698
+ ext = "." + ext.replace(/,+/gm, ",.")
699
+ $('#' + this.elID).parent().find('.moxie-shim > input[type="file"]').attr("accept", ext);
700
+ }
701
+ }
702
+ }
703
+ });
704
+ //=========================================================================================================
705
+ Vue.component('FImageUpdate', {
706
+ props: {
707
+ modelValue: {
708
+ default: null
709
+ },
710
+ label: {
711
+ type: String,
712
+ default: 'image upload'
713
+ },
714
+ title: {
715
+ type: String,
716
+ default: 'Cập nhật hình'
717
+ },
718
+ apiUpload: {
719
+ type: String,
720
+ default: ''
721
+ },
722
+ iconText: {
723
+ type: String,
724
+ default: "mdi-camera"
725
+ },
726
+ size: {
727
+ type: Object,
728
+ default: {
729
+ height: 800,
730
+ width: 800
731
+ }
732
+ },
733
+ dialogWidth: {
734
+ default: 450
735
+ },
736
+ imageBoxAttr: {
737
+ type: Object,
738
+ default: {
739
+ "class": "d-flex flex-column align-center",
740
+ "style": "position: relative;"
741
+ },
742
+ },
743
+ imageAttr: {
744
+ type: Object,
745
+ default: {
746
+ "class": "mb-2",
747
+ "style": "outline: 1px solid #c0c0c0;"
748
+ },
749
+ },
750
+ buttonAttr: {
751
+ type: Object,
752
+ default: {
753
+ "depressed": "",
754
+ "tile": "",
755
+ "color": "primary",
756
+ },
757
+ },
758
+ croperAttr: {
759
+ type: Object,
760
+ default: {
761
+ "stencil-props": { aspectRatio: 3 / 4 },
762
+ "transitions": true,
763
+ "image-restriction": "fit-area",
764
+ },
765
+ },
766
+ },
767
+ template: `
768
+ <v-dialog v-model="dialog" persistent :max-width="dialogWidth" :max-height="700" :key="cropperRerender">
769
+ <template v-slot:activator="{ attrs }">
770
+ <div v-bind="imageBoxAttr">
771
+ <img :src="modelValue" v-bind="imageAttr" />
772
+ <v-btn v-bind="buttonAttr"><v-icon v-if="iconText" :left="label != ''">{{iconText}}</v-icon>{{label}}</v-btn>
773
+ </div>
774
+ </template>
775
+ <v-card>
776
+ <v-toolbar elevation="0">
777
+ <v-toolbar-title>{{title}}</v-toolbar-title><v-spacer></v-spacer><v-btn icon @click="dialog = false"><v-icon>mdi-close</v-icon></v-btn>
778
+ </v-toolbar>
779
+
780
+ <v-card-text>
781
+ <cropper ref="cropper" :src="image" :canvas="size" v-bind="croperAttr"/>
782
+ </v-card-text>
783
+
784
+ <v-card-actions class="d-flex justify-space-around align-center">
785
+ <v-btn icon @click="flip(true, false)"><v-icon>mdi-flip-horizontal</v-icon></v-btn>
786
+ <v-btn icon @click="flip(false, true)"><v-icon>mdi-flip-vertical</v-icon></v-btn>
787
+ <v-btn icon @click="rotate(90)"><v-icon>mdi-rotate-right</v-icon></v-btn>
788
+ <v-btn icon @click="rotate(-90)"><v-icon>mdi-rotate-left</v-icon></v-btn>
789
+ <v-btn icon @click="onClickFileUpload"><v-icon>mdi-camera</v-icon></v-btn><input type="file" ref="fileUpload" @change="readFile" style="display: none;" accept="image/*" />
790
+ <v-divider vertical />
791
+ <v-btn @click="onSaved" color="primary" depressed><v-icon>mdi-cloud-upload-outline</v-icon></v-btn>
792
+ </v-card-actions>
793
+ </v-card>
794
+ <v-overlay :value="overlay"><v-progress-circular indeterminate size="64"></v-progress-circular></v-overlay>
795
+ </v-dialog>
796
+ `,
797
+ data: function () {
798
+ return {
799
+ dialog: false,
800
+ overlay: false,
801
+ image: this.modelValue,
802
+ cropperRerender: 0,
803
+ }
804
+ },
805
+ watch: {
806
+ modelValue: function (newVal, oldVal) {
807
+ this.image = newVal;
808
+ this.cropperRerender = Math.floor(Math.random() * 1000);
809
+ },
810
+ dialog: function (newVal, oldVal) {
811
+ if (newVal) {
812
+ if ($isPhone && this.dialogWidth < 300) this.dialogWidth = 300;
813
+ else if (this.dialogWidth < 450) this.dialogWidth = 450;
814
+ this.image = this.modelValue;
815
+ }
816
+ }
817
+ },
818
+ methods: {
819
+ flip(x, y) {
820
+ if (this.$refs.cropper.imageTransforms.rotate % 180 !== 0) {
821
+ this.$refs.cropper.flip(!x, !y);
822
+ } else {
823
+ this.$refs.cropper.flip(x, y);
824
+ }
825
+ },
826
+ rotate(angle) {
827
+ this.$refs.cropper.rotate(angle);
828
+ },
829
+ onClickFileUpload() {
830
+ this.$refs.fileUpload.click();
831
+ },
832
+ readFile(event) {
833
+ var self = this;
834
+ var input = event.target;
835
+ if (input.files && input.files[0]) {
836
+ var reader = new FileReader();
837
+ reader.onload = function (e) {
838
+ self.image = e.target.result;
839
+ }
840
+ reader.readAsDataURL(input.files[0]);
841
+ } else {
842
+ console.log("Sorry - you're device doesn't support the FileReader API");
843
+ }
844
+ },
845
+ onSaved(e) {
846
+ var self = this;
847
+ self.overlay = true;
848
+
849
+ this.$refs.cropper.getResult().canvas.toBlob(blobImage => {
850
+ var fd = new FormData();
851
+ fd.append('fname', 'image.jpg');
852
+ fd.append('data', blobImage);
853
+
854
+ $.ajax({
855
+ type: 'POST',
856
+ headers: {
857
+ authorization: $awt
858
+ },
859
+ url: self.apiUpload,
860
+ data: fd,
861
+ processData: false,
862
+ contentType: false,
863
+ success: function (data) {
864
+ self.$emit('update', { returnData: data, imageData: self.$refs.cropper.getResult().canvas.toDataURL() });
865
+ self.overlay = false;
866
+ self.dialog = false;
867
+ },
868
+ error: function (xhr) {
869
+ self.overlay = false;
870
+ console.log("Upload error: ", xhr.responseJSON.Message)
871
+ }
872
+ });
873
+ })
874
+ },
875
+ }
876
+ });
877
+
878
+ //===========================================================================================
879
+ Vue.component('FDate', {
880
+ template: `<v-menu v-model="showDatePicker" :close-on-content-click="false" offset-y max-width="330px" min-width="330px" :activator="$refs.dateInput">
881
+ <template v-slot:activator="{ props }">
882
+ <v-text-field ref="dateInput" v-model="displayValue" :label="label" v-bind="$attrs" :required="required" :rules="dateRules" @keydown="handleKeydown" @keypress="restrictToNumbers" @paste="handlePaste" @click="handleClick" @focus="handleFocus"
883
+ maxlength="10" @click:append-inner="showDatePicker = true" append-inner-icon="mdi-calendar-text"></v-text-field>
884
+ </template>
885
+ <v-date-picker v-if="showDatePicker" v-model="pickerDate" no-title show-adjacent-months scrollable @update:modelValue="handleDatePickerInput"></v-date-picker>
886
+ </v-menu>`,
887
+ props: {
888
+ modelValue: {
889
+ type: String,
890
+ default: null,
891
+ },
892
+ label: {
893
+ type: String,
894
+ default: null,
895
+ },
896
+ dateAdd: {
897
+ type: [Number, String],
898
+ default: null,
899
+ },
900
+ required: {
901
+ type: Boolean,
902
+ default: true,
903
+ },
904
+ },
905
+ data() {
906
+ return {
907
+ inputRef: null,
908
+ internalValue: ['', '', ''], // [day, month, year]
909
+ displayValue: 'dd/mm/yyyy',
910
+ cursorPosition: 0,
911
+ selectedField: null,
912
+ isEditingField: false,
913
+ showDatePicker: false,
914
+ pickerDate: null,
915
+ dateRules: [
916
+ (v) => !v || this.isValidDate(v) || 'Ngày không hợp lệ (dd/mm/yyyy)',
917
+ ],
918
+ };
919
+ },
920
+ computed: {
921
+ formattedDate() {
922
+ const [day, month, year] = this.internalValue;
923
+ return `${day.padStart(2, '0')}/${month.padStart(2, '0')}/${year.padStart(4, '0')}`;
924
+ },
925
+ },
926
+ methods: {
927
+ restrictToNumbers(event) {
928
+ // Chỉ cho phép nhập số (0-9)
929
+ const charCode = event.which ? event.which : event.keyCode;
930
+ if (charCode < 48 || charCode > 57) {
931
+ event.preventDefault();
932
+ }
933
+ },
934
+ handlePaste(event) {
935
+ // Chặn dán nếu dữ liệu chứa ký tự không phải số
936
+ const pastedData = event.clipboardData.getData('text');
937
+ if (!/^[0-9/]*$/.test(pastedData)) {
938
+ event.preventDefault();
939
+ }
940
+ },
941
+ handleKeydown(event) {
942
+ this.showDatePicker = false;
943
+ this.cursorPosition = this.inputRef.selectionStart;
944
+ const key = event.key;
945
+ const fields = { day: 0, month: 1, year: 2 };
946
+ const maxLengths = { day: 2, month: 2, year: 4 };
947
+ const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight'];
948
+ const isDigit = /^[0-9]$/.test(key);
949
+ const isAllowedSystemKey = /^F([1-9]|1[0-2])$/.test(key) || key === 'Tab';
950
+ if (isDigit) {
951
+ event.preventDefault();
952
+ const fieldIndex = fields[this.selectedField];
953
+ let fieldValue = this.internalValue[fieldIndex];
954
+ if (!this.isEditingField) {
955
+ fieldValue = '';
956
+ this.isEditingField = true;
957
+ }
958
+ if (fieldValue == '0') fieldValue = key;
959
+ else fieldValue += key;
960
+ fieldValue = fieldValue.slice(-maxLengths[this.selectedField]);
961
+ this.internalValue[fieldIndex] = fieldValue;
962
+ if (fieldValue.length === maxLengths[this.selectedField]) {
963
+ if (this.selectedField === 'day') this.selectedField = 'month';
964
+ else if (this.selectedField === 'month') this.selectedField = 'year';
965
+ this.isEditingField = false;
966
+ }
967
+ this.updateDisplayValue();
968
+ this.$nextTick(() => this.selectField());
969
+ return;
970
+ }
971
+ if (key === 'Backspace' || key === 'Delete') {
972
+ event.preventDefault();
973
+ const fieldIndex = fields[this.selectedField];
974
+ this.internalValue[fieldIndex] = '';
975
+ this.isEditingField = false;
976
+ this.updateDisplayValue();
977
+ this.$nextTick(() => this.selectField());
978
+ return;
979
+ }
980
+ if (key === 'ArrowLeft') {
981
+ event.preventDefault();
982
+ if (this.selectedField === 'year') this.selectedField = 'month';
983
+ else if (this.selectedField === 'month') this.selectedField = 'day';
984
+ this.isEditingField = false;
985
+ this.$nextTick(() => this.selectField());
986
+ return;
987
+ }
988
+ if (key === 'ArrowRight') {
989
+ event.preventDefault();
990
+ if (this.selectedField === 'day') this.selectedField = 'month';
991
+ else if (this.selectedField === 'month') this.selectedField = 'year';
992
+ this.isEditingField = false;
993
+ this.$nextTick(() => this.selectField());
994
+ return;
995
+ }
996
+ if (!isDigit && !allowedKeys.includes(key) && !isAllowedSystemKey) {
997
+ event.preventDefault();
998
+ }
999
+ },
1000
+ handleClick(event) {
1001
+ this.cursorPosition = this.inputRef.selectionStart;
1002
+ if (this.cursorPosition <= 2) {
1003
+ this.selectedField = 'day';
1004
+ } else if (this.cursorPosition <= 5) {
1005
+ this.selectedField = 'month';
1006
+ } else {
1007
+ this.selectedField = 'year';
1008
+ }
1009
+ this.isEditingField = false;
1010
+ this.$nextTick(() => {
1011
+ this.selectField();
1012
+ });
1013
+ },
1014
+ handleFocus() {
1015
+ if (!this.selectedField) {
1016
+ this.selectedField = 'day';
1017
+ }
1018
+ this.isEditingField = false;
1019
+ this.$nextTick(() => {
1020
+ this.selectField();
1021
+ });
1022
+ },
1023
+ selectField() {
1024
+ if (this.selectedField === 'day') {
1025
+ this.inputRef.setSelectionRange(0, 2);
1026
+ this.cursorPosition = 2;
1027
+ } else if (this.selectedField === 'month') {
1028
+ this.inputRef.setSelectionRange(3, 5);
1029
+ this.cursorPosition = 5;
1030
+ } else if (this.selectedField === 'year') {
1031
+ this.inputRef.setSelectionRange(6, 10);
1032
+ this.cursorPosition = 10;
1033
+ }
1034
+ },
1035
+ updateDisplayValue() {
1036
+ const [day, month, year] = this.internalValue;
1037
+ this.displayValue = `${day.padStart(2, (day ? '0' : '_')) || 'dd'}/${month.padStart(2, (month ? '0' : '_')) || 'mm'}/${year.padStart(4, (year ? '0' : '_')) || 'yyyy'}`;
1038
+ if (this.isValidDate(this.formattedDate)) {
1039
+ let date = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
1040
+ this.pickerDate = new Date(date);
1041
+ this.$emit('update:modelValue', date);
1042
+ } else
1043
+ this.$emit('update:modelValue', null);
1044
+ },
1045
+ isValidDate(value) {
1046
+ // Cho phép giá trị rỗng hoặc placeholder
1047
+ if (!this.required && (!value || value === '__/__/____' || value === 'dd/mm/yyyy' || value === 'yyyy/mm/dd' || value === 'dd-mm-yyyy' || value === 'yyyy-mm-dd')) return true;
1048
+ // Regex cho các định dạng
1049
+ const ddmmyyyySlashRegex = /^(\d{2})\/(\d{2})\/(\d{4})$/; // dd/mm/yyyy
1050
+ const yyyymmddSlashRegex = /^(\d{4})\/(\d{2})\/(\d{2})$/; // yyyy/mm/dd
1051
+ const ddmmyyyyDashRegex = /^(\d{2})-(\d{2})-(\d{4})$/; // dd-mm-yyyy
1052
+ const yyyymmddDashRegex = /^(\d{4})-(\d{2})-(\d{2})$/; // yyyy-mm-dd
1053
+ let day, month, year;
1054
+ // Kiểm tra định dạng dd/mm/yyyy
1055
+ if (ddmmyyyySlashRegex.test(value)) {
1056
+ [day, month, year] = value.split('/').map(Number);
1057
+ }
1058
+ // Kiểm tra định dạng yyyy/mm/dd
1059
+ else if (yyyymmddSlashRegex.test(value)) {
1060
+ [year, month, day] = value.split('/').map(Number);
1061
+ }
1062
+ // Kiểm tra định dạng dd-mm-yyyy
1063
+ else if (ddmmyyyyDashRegex.test(value)) {
1064
+ [day, month, year] = value.split('-').map(Number);
1065
+ }
1066
+ // Kiểm tra định dạng yyyy-mm-dd
1067
+ else if (yyyymmddDashRegex.test(value)) {
1068
+ [year, month, day] = value.split('-').map(Number);
1069
+ }
1070
+ // Nếu không khớp định dạng nào
1071
+ else {
1072
+ return false;
1073
+ }
1074
+ // Tạo đối tượng Date và kiểm tra tính hợp lệ
1075
+ const date = new Date(year, month - 1, day);
1076
+ return (
1077
+ date.getDate() === day &&
1078
+ date.getMonth() + 1 === month &&
1079
+ date.getFullYear() === year
1080
+ );
1081
+ },
1082
+ handleDatePickerInput(date) {
1083
+ if (date) {
1084
+ const year = String(date.getFullYear())
1085
+ const month = String(date.getMonth() + 1).padStart(2, '0')
1086
+ const day = String(date.getDate()).padStart(2, '0')
1087
+ this.internalValue = [day, month, year];
1088
+ this.updateDisplayValue();
1089
+ this.showDatePicker = false;
1090
+ }
1091
+ },
1092
+ initializeDate() {
1093
+ // Regex cho các định dạng
1094
+ const yyyymmddSlashRegex = /^(\d{4})\/(\d{2})\/(\d{2})$/; // yyyy/mm/dd
1095
+ const yyyymmddDashRegex = /^(\d{4})-(\d{2})-(\d{2})$/; // yyyy-mm-dd
1096
+ var modelValue = this.modelValue
1097
+ // Nếu có chuỗi dạng ISO có chứa 'T' (ví dụ: "2024-12-28T12:30:00")
1098
+ if (modelValue && typeof modelValue === 'string' && modelValue.includes('T')) {
1099
+ modelValue = modelValue.split('T')[0]; // Cắt phần thời gian, giữ lại yyyy-mm-dd
1100
+ }
1101
+ // Nếu có giá trị từ prop value
1102
+ if (modelValue && this.isValidDate(modelValue)) {
1103
+ let day, month, year;
1104
+ // Kiểm tra định dạng yyyy/mm/dd
1105
+ if (yyyymmddSlashRegex.test(modelValue)) {
1106
+ [year, month, day] = modelValue.split('/').map(Number);
1107
+ }
1108
+ // Kiểm tra định dạng yyyy-mm-dd
1109
+ else if (yyyymmddDashRegex.test(modelValue)) {
1110
+ [year, month, day] = modelValue.split('-').map(Number);
1111
+ }
1112
+ // Chuyển đổi sang chuỗi với định dạng 2 chữ số
1113
+ day = String(day).padStart(2, '0');
1114
+ month = String(month).padStart(2, '0');
1115
+ year = String(year);
1116
+ this.internalValue = [day, month, year];
1117
+ this.pickerDate = new Date(`${year}-${month}-${day}`);
1118
+ this.updateDisplayValue();
1119
+ }
1120
+ // Nếu không có value và dateAdd khác null
1121
+ else if (this.dateAdd !== null) {
1122
+ const date = new Date();
1123
+ date.setDate(date.getDate() + parseInt(this.dateAdd));
1124
+ const day = String(date.getDate()).padStart(2, '0');
1125
+ const month = String(date.getMonth() + 1).padStart(2, '0');
1126
+ const year = String(date.getFullYear());
1127
+ this.internalValue = [day, month, year];
1128
+ this.pickerDate = new Date(`${year}-${month}-${day}`);
1129
+ this.updateDisplayValue();
1130
+ }
1131
+ }
1132
+ },
1133
+ watch: {
1134
+ modelValue(newVal) {
1135
+ if (newVal && this.isValidDate(newVal)) {
1136
+ if (newVal != this.pickerDate) {
1137
+ this.initializeDate();
1138
+ }
1139
+ } else {
1140
+ if (this.isValidDate(this.formattedDate)) {
1141
+ this.internalValue = ['', '', ''];
1142
+ this.displayValue = 'dd/mm/yyyy';
1143
+ this.pickerDate = null;
1144
+ }
1145
+ }
1146
+ }
1147
+ },
1148
+ created() {
1149
+ this.initializeDate();
1150
+ },
1151
+ mounted() {
1152
+ this.inputRef = this.$refs.dateInput.$el.querySelector('input');
1153
+ }
1154
+ });
1155
+
1156
+ //===========================================================================================
1157
+ Vue.component('FTime', {
1158
+ template: `<v-menu v-model="showTimePicker" :close-on-content-click="false" offset-y max-width="330px" min-width="330px" :activator="$refs.timeInput">
1159
+ <template v-slot:activator="{ props }">
1160
+ <v-text-field ref="timeInput" v-model="displayValue" :label="label" :rules="timeRules" v-bind="$attrs" :required="required" @keydown="handleKeydown" @keypress="restrictToNumbers" @paste="handlePaste"
1161
+ @click="handleClick" @focus="handleFocus" maxlength="5" append-inner-icon="mdi-clock-outline"></v-text-field>
1162
+ </template>
1163
+ <v-time-picker v-if="showTimePicker" v-model="pickerTime" format="24hr" scrollable @update:modelValue="handleTimePickerInput"></v-time-picker>
1164
+ </v-menu>`,
1165
+ props: {
1166
+ modelValue: {
1167
+ type: String,
1168
+ default: null, // Expected format: HH:MM
1169
+ },
1170
+ label: {
1171
+ type: String,
1172
+ default: "Select Time",
1173
+ },
1174
+ timeAdd: {
1175
+ type: [Number, String],
1176
+ default: null, // Number of minutes to add to current time
1177
+ },
1178
+ required: {
1179
+ type: Boolean,
1180
+ default: true,
1181
+ },
1182
+ },
1183
+ data() {
1184
+ return {
1185
+ inputRef: null,
1186
+ internalValue: ["", ""], // [hour, minute]
1187
+ displayValue: "HH:MM",
1188
+ cursorPosition: 0,
1189
+ selectedField: null,
1190
+ isEditingField: false,
1191
+ showTimePicker: false,
1192
+ pickerTime: null,
1193
+ timeRules: [
1194
+ (v) => !v || this.isValidTime(v) || 'Thời gian không hợp lệ (HH:MM)',
1195
+ ],
1196
+ };
1197
+ },
1198
+ computed: {
1199
+ formattedTime() {
1200
+ const [hour, minute] = this.internalValue;
1201
+ return `${hour.padStart(2, hour ? "0" : "x")}:${minute.padStart(2, minute ? "0" : "X")}`;
1202
+ },
1203
+ },
1204
+ methods: {
1205
+ restrictToNumbers(event) {
1206
+ const charCode = event.which ? event.which : event.keyCode;
1207
+ if (charCode < 48 || charCode > 57) {
1208
+ event.preventDefault();
1209
+ }
1210
+ },
1211
+ handlePaste(event) {
1212
+ const pastedData = event.clipboardData.getData("text");
1213
+ if (!/^[0-9:]*$/.test(pastedData)) {
1214
+ event.preventDefault();
1215
+ }
1216
+ },
1217
+ handleKeydown(event) {
1218
+ this.showTimePicker = false;
1219
+ this.cursorPosition = this.inputRef.selectionStart;
1220
+ const key = event.key;
1221
+ const fields = { hour: 0, minute: 1 };
1222
+ const maxLengths = { hour: 2, minute: 2 };
1223
+ const allowedKeys = ["Backspace", "Delete", "ArrowLeft", "ArrowRight"];
1224
+ const isDigit = /^[0-9]$/.test(key);
1225
+ const isAllowedSystemKey = /^F([1-9]|1[0-2])$/.test(key) || key === "Tab";
1226
+
1227
+ if (isDigit) {
1228
+ event.preventDefault();
1229
+ const fieldIndex = fields[this.selectedField];
1230
+ let fieldValue = this.internalValue[fieldIndex];
1231
+ if (!this.isEditingField) {
1232
+ fieldValue = "";
1233
+ this.isEditingField = true;
1234
+ }
1235
+ if (fieldValue === "0") fieldValue = key;
1236
+ else fieldValue += key;
1237
+ fieldValue = fieldValue.slice(-maxLengths[this.selectedField]);
1238
+ this.internalValue[fieldIndex] = fieldValue;
1239
+
1240
+ if (fieldValue.length === maxLengths[this.selectedField]) {
1241
+ if (this.selectedField === "hour") this.selectedField = "minute";
1242
+ this.isEditingField = false;
1243
+ }
1244
+ this.updateDisplayValue();
1245
+ this.$nextTick(() => this.selectField());
1246
+ return;
1247
+ }
1248
+
1249
+ if (key === "Backspace" || key === "Delete") {
1250
+ event.preventDefault();
1251
+ const fieldIndex = fields[this.selectedField];
1252
+ this.internalValue[fieldIndex] = "";
1253
+ this.isEditingField = false;
1254
+ this.updateDisplayValue();
1255
+ this.$nextTick(() => this.selectField());
1256
+ return;
1257
+ }
1258
+
1259
+ if (key === "ArrowLeft" && this.selectedField === "minute") {
1260
+ event.preventDefault();
1261
+ this.selectedField = "hour";
1262
+ this.isEditingField = false;
1263
+ this.$nextTick(() => this.selectField());
1264
+ return;
1265
+ }
1266
+
1267
+ if (key === "ArrowRight" && this.selectedField === "hour") {
1268
+ event.preventDefault();
1269
+ this.selectedField = "minute";
1270
+ this.isEditingField = false;
1271
+ this.$nextTick(() => this.selectField());
1272
+ return;
1273
+ }
1274
+
1275
+ if (!isDigit && !allowedKeys.includes(key) && !isAllowedSystemKey) {
1276
+ event.preventDefault();
1277
+ }
1278
+ },
1279
+ handleClick(event) {
1280
+ this.cursorPosition = this.inputRef.selectionStart;
1281
+ if (this.cursorPosition <= 2) {
1282
+ this.selectedField = "hour";
1283
+ } else {
1284
+ this.selectedField = "minute";
1285
+ }
1286
+ this.isEditingField = false;
1287
+ this.$nextTick(() => this.selectField());
1288
+ },
1289
+ handleFocus() {
1290
+ if (!this.selectedField) {
1291
+ this.selectedField = "hour";
1292
+ }
1293
+ this.isEditingField = false;
1294
+ this.$nextTick(() => this.selectField());
1295
+ },
1296
+ selectField() {
1297
+ if (this.selectedField === "hour") {
1298
+ this.inputRef.setSelectionRange(0, 2);
1299
+ this.cursorPosition = 2;
1300
+ } else if (this.selectedField === "minute") {
1301
+ this.inputRef.setSelectionRange(3, 5);
1302
+ this.cursorPosition = 5;
1303
+ }
1304
+ },
1305
+ updateDisplayValue() {
1306
+ const [hour, minute] = this.internalValue;
1307
+ this.displayValue = `${hour.padStart(2, hour ? "0" : "_") || "HH"}:${minute.padStart(2, minute ? "0" : "_") || "MM"}`;
1308
+ if (this.isValidTime(this.formattedTime)) {
1309
+ this.pickerTime = `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`;
1310
+ this.$emit("update:modelValue", this.pickerTime);
1311
+ } else {
1312
+ this.$emit("update:modelValue", null);
1313
+ }
1314
+ },
1315
+ isValidTime(value) {
1316
+ if (!this.required && (!value || value === '__:__' || value === 'HH:MM')) return true;
1317
+ const timeRegex = /^(\d{2}):(\d{2})$/;
1318
+ if (!timeRegex.test(value)) return false;
1319
+ const [hour, minute] = value.split(":").map(Number);
1320
+ return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59;
1321
+ },
1322
+ handleTimePickerInput(time) {
1323
+ if (time) {
1324
+ const [hour, minute] = time.split(":");
1325
+ this.internalValue = [hour, minute];
1326
+ this.updateDisplayValue();
1327
+ this.showTimePicker = false;
1328
+ }
1329
+ },
1330
+ initializeTime() {
1331
+ if (this.modelValue && this.isValidTime(this.modelValue)) {
1332
+ const [hour, minute] = this.modelValue.split(":");
1333
+ this.internalValue = [hour, minute];
1334
+ this.pickerTime = this.modelValue;
1335
+ this.updateDisplayValue();
1336
+ } else if (this.timeAdd !== null) {
1337
+ const date = new Date();
1338
+ date.setMinutes(date.getMinutes() + parseInt(this.timeAdd));
1339
+ const hour = String(date.getHours()).padStart(2, "0");
1340
+ const minute = String(date.getMinutes()).padStart(2, "0");
1341
+ this.internalValue = [hour, minute];
1342
+ this.pickerTime = `${hour}:${minute}`;
1343
+ this.updateDisplayValue();
1344
+ }
1345
+ },
1346
+ },
1347
+ watch: {
1348
+ modelValue(newVal) {
1349
+ console.log("modelValue", newVal, this.displayValue, this.formattedTime)
1350
+
1351
+ if (newVal && this.isValidTime(newVal)) {
1352
+ if (newVal !== this.pickerTime) {
1353
+ this.initializeTime();
1354
+ }
1355
+ } else {
1356
+ if (this.isValidTime(this.formattedTime)) {
1357
+ console.log("hhhhhhhhhh", this.formattedTime)
1358
+
1359
+ this.internalValue = ["", ""];
1360
+ this.displayValue = "HH:MM";
1361
+ this.pickerTime = null;
1362
+ }
1363
+ }
1364
+ },
1365
+ },
1366
+ created() {
1367
+ this.initializeTime();
1368
+ },
1369
+ mounted() {
1370
+ this.inputRef = this.$refs.timeInput.$el.querySelector("input");
1371
+ },
1372
+ });
1373
+ //===========================================================================================
1374
+ Vue.component('FpProfile', {
1375
+ props: {
1376
+ urlAppList: {
1377
+ default: null
1378
+ },
1379
+ urlSignout: {
1380
+ default: null
1381
+ },
1382
+ urlChangePassword: {
1383
+ default: null
1384
+ },
1385
+ urlAccount: {
1386
+ default: null
1387
+ },
1388
+ urlAvatar: {
1389
+ default: null
1390
+ },
1391
+ color: {
1392
+ default: "primary"
1393
+ }
1394
+ },
1395
+ template: `
1396
+ <v-menu v-if="userInfo.UserID" v-model="menu" :close-on-content-click="false" offset-y slide-y transition="slide-y-transition">
1397
+ <template v-slot:activator="{ props }">
1398
+ <v-avatar v-bind="props" size="36" class="mx-2 cursor-pointer">
1399
+ <v-img v-if="urlAvatar" :src="avatarImg"></v-img>
1400
+ <v-icon v-else :color="color">mdi-card-account-details</v-icon>
1401
+ </v-avatar>
1402
+ </template>
1403
+
1404
+ <v-card width=350>
1405
+ <v-list-item><h4 class="">Thông tin người dùng</h4></v-list-item>
1406
+ <v-list>
1407
+ <v-list-item>
1408
+ <template v-slot:prepend>
1409
+ <v-avatar color="grey lighten-2" size="56">
1410
+ <v-img v-if="urlAvatar" :src="avatarImg"></v-img>
1411
+ <v-icon v-else>mdi-account-tie</v-icon>
1412
+ </v-avatar>
1413
+ </template>
1414
+
1415
+ <div v-if="userInfo">
1416
+ <v-list-item-title><v-btn color="primary" variant="text" :href="urlAccount">{{userInfo.FirstName}} {{userInfo.LastName}}</v-btn></v-list-item-title>
1417
+ <v-list-item-subtitle class="ml-4"><b style="color: #000; font-size: larger;">{{userInfo.UserName}}</b> - {{userInfo.UserID}} - {{userInfo.DepartmentID}}</v-list-item-subtitle>
1418
+ </div>
1419
+ </v-list-item>
1420
+ </v-list>
1421
+
1422
+ <v-card-actions>
1423
+ <v-spacer></v-spacer>
1424
+ <v-btn v-if="urlChangePassword" color="primary" text @click="clickChangePass" prepend-icon="mdi-key-arrow-right">Đổi mật khẩu</v-btn>
1425
+ <v-btn v-if="urlSignout" color="warning" text @click="clickSignout" prepend-icon="mdi-logout">Đăng xuất</v-btn>
1426
+ </v-card-actions>
1427
+
1428
+ <v-list-item v-if="appList.length > 0"><h4 class="">Ứng dụng của tôi</h4></v-list-item>
1429
+ <v-list v-if="appList.length > 0" class="fpScrollbar" style="display: grid; grid-template-columns: auto auto auto; max-height: 400px; ">
1430
+ <v-list-item v-for="(item, i) in appList" :key="i" :value="item" :href="item.URL" color="primary" class="pa-2" style="display: inline-block; text-align: center; cursor: pointer;">
1431
+ <v-icon :icon="item.Icon" color="primary" style="font-size: 30px;"></v-icon>
1432
+ <v-list-item-title v-text="item.AppName" class="mt-1" style="font-size: 11px; text-wrap: wrap;"></v-list-item-title>
1433
+ </v-list-item>
1434
+ </v-list>
1435
+ </v-card>
1436
+ </v-menu>
1437
+ `,
1438
+ data: function () {
1439
+ return {
1440
+ userInfo: vueData.user,
1441
+ menu: false,
1442
+ appList: [],
1443
+ firstLoad: false
1444
+ }
1445
+ },
1446
+ computed: {
1447
+ avatarImg: function () {
1448
+ return this.urlAvatar + this.userInfo.UserID + ".jpg"
1449
+ }
1450
+ },
1451
+ watch: {
1452
+ menu: function (e) {
1453
+ if (!this.firstLoad && this.urlAppList) {
1454
+ this.firstLoad = true
1455
+ ajaxCALL(this.urlAppList, {}, (d) => {
1456
+ this.appList = d.data;
1457
+ });
1458
+ }
1459
+ },
1460
+ },
1461
+ methods: {
1462
+ clickChangePass: function (e) {
1463
+ this.menu = false
1464
+ location.assign(this.urlChangePassword)
1465
+ },
1466
+ clickSignout: function (item) {
1467
+ this.menu = false
1468
+ location.assign(this.urlSignout)
1469
+ }
1470
+ },
1471
+ });
1472
+ //===========================================================================================
1473
+ Vue.component('HeaderBar', {
1474
+ props: {
1475
+ menu: {
1476
+ default: null
1477
+ },
1478
+ menuLeft: {
1479
+ default: null
1480
+ },
1481
+ title: {
1482
+ default: ''
1483
+ },
1484
+ logo: {
1485
+ default: ""
1486
+ },
1487
+ expand: {
1488
+ default: true
1489
+ },
1490
+ menuStyle: {
1491
+ default: {
1492
+ topmenu: {
1493
+ bgcolor: "primary",
1494
+ textcolor: "white"
1495
+ },
1496
+ leftmenu: {
1497
+ bgcolor: "primary",
1498
+ textcolor: "white"
1499
+ }
1500
+ }
1501
+ },
1502
+ component: {
1503
+ default: null
1504
+ },
1505
+ userProfile: {
1506
+ default: null
1507
+ },
1508
+ activeColor: {
1509
+ default: "rgba(255,255,255,0.15)"
1510
+ }
1511
+ },
1512
+ template: `
1513
+ <div>
1514
+ <v-app-bar v-if="isMobile || menu" light app density="compact" flat clipped-left :color="menuStyle.topmenu.bgcolor" :style="{ 'box-shadow': 'rgba(0, 0, 0, 0.5) 0px 0px '+ boxShadow +'px 0px !important'}">
1515
+ <v-img class="ml-2" v-if="logo" :src="logo" height="30" width="30" max-height="30" max-width="30" contain></v-img>
1516
+ <v-toolbar-title v-if="title" :class="['text-' + menuStyle.topmenu.textcolor,'ml-2']">{{title}}</v-toolbar-title>
1517
+ <v-spacer></v-spacer>
1518
+ <v-toolbar-items class="hidden-sm-and-down" v-if="menu">
1519
+ <v-menu offset-y v-for="(item, index) in menu.filter(info => checkright(info))" :key="index">
1520
+ <template v-slot:activator="{ props }">
1521
+ <v-btn v-bind="props" :prepend-icon="item.icon" :append-icon="!!item.submenu?'mdi-chevron-down':''" :href="(!item.submenu?item.url:'')" :target="item.target" text :class="['text-' + menuStyle.topmenu.textcolor]">{{ item.name }}</v-btn>
1522
+ </template>
1523
+ <v-list v-if="!!item.submenu">
1524
+ <v-list-item v-for="(i, index) in item.submenu.filter(info => checkright(info))" :key="index" :prepend-icon="i.icon" :title="i.name" :href="i.url" :target="i.target"></v-list-item>
1525
+ </v-list>
1526
+ </v-menu>
1527
+ </v-toolbar-items>
1528
+ <component v-if="!isMobile && component" v-for="(c, index) in component" :key="index" v-bind:is="c.el" v-bind="c.attr"></component>
1529
+ <fp-profile v-if="!isMobile && userProfile" v-bind="userProfile"></fp-profile>
1530
+ <v-app-bar-nav-icon v-if="isMobile && leftMenuAndMobile" :color="menuStyle.topmenu.textcolor" @click.stop="openLeftMenu=!openLeftMenu"></v-app-bar-nav-icon>
1531
+ </v-app-bar>
1532
+
1533
+ <v-navigation-drawer
1534
+ v-if="leftMenuAndMobile"
1535
+ :color="menuStyle.leftmenu.bgcolor"
1536
+ :class="['text-' + menuStyle.leftmenu.textcolor, 'fp-custom-scrollbar']"
1537
+ v-model="openLeftMenu"
1538
+ app
1539
+ clipped
1540
+ :location="isMobile?'right':'left'"
1541
+ :permanent="!isMobile"
1542
+ :rail="pinLeftMenu && !isMobile"
1543
+ :expand-on-hover="pinLeftMenu && !isMobile">
1544
+ <template v-slot:prepend v-if="!isMobile && !menu">
1545
+ <v-list-item class="mr-2 px-0 pt-3 pl-3">
1546
+ <template v-slot:prepend v-if="logo">
1547
+ <v-img :src="logo" height="30" width="30" max-height="30" max-width="30" contain></v-img>
1548
+ </template>
1549
+ <template v-slot:title v-if="title">
1550
+ <v-toolbar-title :class="['text-' + menuStyle.topmenu.textcolor]">{{title}}</v-toolbar-title>
1551
+ </template>
1552
+ </v-list-item>
1553
+ </template>
1554
+ <v-list v-if="leftMenuAndMobile" v-model:opened="openedGroups">
1555
+ <template v-for="(item, index) in leftMenuAndMobile.filter(info => checkright(info))" :key="index">
1556
+ <v-list-item
1557
+ v-if="!item.submenu"
1558
+ :href="item.url"
1559
+ :target="item.target"
1560
+ :title="item.name"
1561
+ :class="['text-' + menuStyle.leftmenu.textcolor]"
1562
+ :data-active="isActiveUrl(item.url) ? 'true' : null"
1563
+ :style="isActiveUrl(item.url) ? {backgroundColor: activeColor} : {}">
1564
+ <template v-slot:prepend v-if="item.icon">
1565
+ <v-icon :icon="item.icon" class="opacity-100"></v-icon>
1566
+ </template>
1567
+ </v-list-item>
1568
+ <v-list-group v-else :value="item.name">
1569
+ <template v-slot:activator="{ props }">
1570
+ <v-list-item v-bind="props" :title="item.name" :class="['text-' + menuStyle.leftmenu.textcolor]">
1571
+ <template v-slot:prepend v-if="item.icon"><v-icon :icon="item.icon" class="opacity-100"></v-icon></template>
1572
+ </v-list-item>
1573
+ </template>
1574
+
1575
+ <v-list-item
1576
+ v-for="(i, index) in item.submenu.filter(info => checkright(info))"
1577
+ :key="index"
1578
+ :href="i.url"
1579
+ :target="i.target"
1580
+ class="fp-submenu-item"
1581
+ :data-active="isActiveUrl(i.url) ? 'true' : null"
1582
+ :style="isActiveUrl(i.url) ? {backgroundColor: activeColor} : {}">
1583
+ <template v-slot:prepend v-if="i.icon"><v-icon :icon="i.icon" class="opacity-100"></v-icon></template>
1584
+ <v-list-item-title :class="['text-' + menuStyle.leftmenu.textcolor]" style="font-size: 0.9em;">{{ i.name }}</v-list-item-title>
1585
+ </v-list-item>
1586
+ </v-list-group>
1587
+ </template>
1588
+ </v-list>
1589
+ <template v-slot:append>
1590
+ <component v-if="!menu && component || isMobile" v-for="(c, index) in component" :key="index" v-bind:is="c.el" v-bind="c.attr"></component>
1591
+ <v-list-item class="px-0 py-2">
1592
+ <template v-slot:prepend v-if="!menu && userProfile || isMobile">
1593
+ <fp-profile v-bind="userProfile"></fp-profile>
1594
+ </template>
1595
+ <template v-slot:append>
1596
+ <v-btn v-if="(menu || leftMenuAndMobile) && !isMobile" @click.stop="on_of_menu" density="compact" variant="text" :color="menuStyle.leftmenu.textcolor" :icon="pinLeftMenu?'mdi-pin-outline':'mdi-pin-off-outline'"></v-btn>
1597
+ </template>
1598
+ </v-list-item>
1599
+ </template>
1600
+ </v-navigation-drawer>
1601
+ </div>
1602
+ `,
1603
+ data: function () {
1604
+ var leftMenuAndMobile = [];
1605
+ var display = Vuetify.useDisplay();
1606
+ var smAndDown = display.smAndDown;
1607
+
1608
+ if (smAndDown.value) {
1609
+ _.forEach(_.union(this.menu, this.menuLeft), function (value) {
1610
+ if (rightTest(value.right)) leftMenuAndMobile.push(value);
1611
+ });
1612
+
1613
+ if (leftMenuAndMobile.length == 0) leftMenuAndMobile = null;
1614
+ }
1615
+ else leftMenuAndMobile = this.menuLeft;
1616
+
1617
+ var currentFullUrl = window.location.pathname + window.location.search;
1618
+ var openedGroups = [];
1619
+
1620
+ if (leftMenuAndMobile && leftMenuAndMobile.length > 0) {
1621
+ _.forEach(leftMenuAndMobile, function (item) {
1622
+ if (item.submenu) {
1623
+ var hasActiveChild = _.some(item.submenu, function (subItem) {
1624
+ return subItem.url && currentFullUrl === subItem.url;
1625
+ });
1626
+ if (hasActiveChild) {
1627
+ openedGroups.push(item.name);
1628
+ }
1629
+ }
1630
+ });
1631
+ }
1632
+
1633
+ return {
1634
+ smAndDown: smAndDown,
1635
+ leftMenuAndMobile: leftMenuAndMobile,
1636
+ openLeftMenu: this.isMobile,
1637
+ pinLeftMenu: (sessionStorage.getItem("fpOpenmenuAppBar") == null ? !this.expand : sessionStorage.getItem("fpOpenmenuAppBar") == 'true'),
1638
+ boxShadow: 0,
1639
+ currentFullUrl: currentFullUrl,
1640
+ openedGroups: openedGroups
1641
+ };
1642
+ },
1643
+ computed: {
1644
+ isMobile: function() {
1645
+ if (this.smAndDown) return true;
1646
+ else return false;
1647
+ }
1648
+ },
1649
+ methods: {
1650
+ on_of_menu: function (e) {
1651
+ this.pinLeftMenu = !this.pinLeftMenu;
1652
+ sessionStorage.setItem("fpOpenmenuAppBar", this.pinLeftMenu);
1653
+ },
1654
+ checkright: function (item) {
1655
+ if (!item) return true;
1656
+ if (!item.right) return true;
1657
+ if (!vueData.user) return false;
1658
+
1659
+ return rightTest(item.right);
1660
+ },
1661
+ isActiveUrl: function (url) {
1662
+ if (!url) return false;
1663
+ return this.currentFullUrl === url;
1664
+ },
1665
+ scrollToActiveMenu: function () {
1666
+ if (!this.leftMenuAndMobile || this.leftMenuAndMobile.length === 0) return;
1667
+
1668
+ var self = this;
1669
+ this.$nextTick(function() {
1670
+ setTimeout(function() {
1671
+ var activeEl = self.$el.querySelector('[data-active="true"]');
1672
+ if (activeEl) {
1673
+ // Fallback cho trình duyệt cũ không hỗ trợ options
1674
+ if ('scrollBehavior' in document.documentElement.style) {
1675
+ activeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
1676
+ } else {
1677
+ activeEl.scrollIntoView(true);
1678
+ }
1679
+ }
1680
+ }, 300);
1681
+ });
1682
+ },
1683
+ injectScrollbarStyles: function () {
1684
+ if (!this.leftMenuAndMobile || this.leftMenuAndMobile.length === 0) return;
1685
+
1686
+ var styleId = 'fp-custom-scrollbar-style';
1687
+ if (document.getElementById(styleId)) return;
1688
+
1689
+ var thumbColor = 'rgba(255,255,255,0.3)';
1690
+ var thumbHoverColor = 'rgba(255,255,255,0.5)';
1691
+ var trackColor = 'rgba(0,0,0,0.1)';
1692
+
1693
+ var style = document.createElement('style');
1694
+ style.id = styleId;
1695
+ style.textContent = [
1696
+ '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar {',
1697
+ ' width: 5px;',
1698
+ '}',
1699
+ '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar-track {',
1700
+ ' background: ' + trackColor + ';',
1701
+ '}',
1702
+ '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar-thumb {',
1703
+ ' background: ' + thumbColor + ';',
1704
+ ' border-radius: 3px;',
1705
+ '}',
1706
+ '.fp-custom-scrollbar .v-navigation-drawer__content::-webkit-scrollbar-thumb:hover {',
1707
+ ' background: ' + thumbHoverColor + ';',
1708
+ '}',
1709
+ '.fp-custom-scrollbar .v-navigation-drawer__content {',
1710
+ ' scrollbar-width: thin;',
1711
+ ' scrollbar-color: ' + thumbColor + ' ' + trackColor + ';',
1712
+ '}',
1713
+ '.fp-custom-scrollbar .v-list-group__items .fp-submenu-item {',
1714
+ ' padding-left: 32px !important;',
1715
+ '}',
1716
+ '.fp-custom-scrollbar .v-list-group__items .v-list-item__prepend {',
1717
+ ' width: auto;',
1718
+ ' margin-right: 8px;',
1719
+ '}'
1720
+ ].join('\n');
1721
+ document.head.appendChild(style);
1722
+ }
1723
+ },
1724
+ created: function() {
1725
+ if (!this.$isServer) {
1726
+ var self = this;
1727
+ this._scrollListener = function() {
1728
+ self.boxShadow = Math.round(window.pageYOffset) / 10;
1729
+ if (self.boxShadow > 10) self.boxShadow = 10;
1730
+ };
1731
+ this._scrollListener();
1732
+ window.addEventListener('scroll', this._scrollListener);
1733
+ }
1734
+ },
1735
+ mounted: function() {
1736
+ if (this.leftMenuAndMobile && this.leftMenuAndMobile.length > 0) {
1737
+ this.injectScrollbarStyles();
1738
+ this.scrollToActiveMenu();
1739
+ }
1740
+ },
1741
+ beforeDestroy: function() {
1742
+ window.removeEventListener('scroll', this._scrollListener);
1743
+ }
1744
+ });
1745
+
1746
+ //===========================================================================================
1747
+ Vue.component('f-chart', {
1748
+ props: ["data", "type", "options", "reverseData"],
1749
+ template: `<canvas ref="myChart"></canvas>`,
1750
+ data: function () {
1751
+ return {
1752
+ myChart: null,
1753
+ config: {
1754
+ type: this.type,
1755
+ data: this.data,
1756
+ options: this.options
1757
+ },
1758
+ chart_color: [
1759
+ "#1e90ff",
1760
+ "#ffa64d",
1761
+ "#6fdc6f",
1762
+ "#f14aff",
1763
+ "#9966ff",
1764
+ "#02e8e8",
1765
+ "#402b58",
1766
+ "#3b8d74",
1767
+ "#095fa4",
1768
+ "#b63639",
1769
+ "#c84f7a",
1770
+ "#a8b236",
1771
+ "#90692f",
1772
+ "#b42e89",
1773
+ "#ff9966",
1774
+ "#4db8ff",
1775
+ "#1c8540",
1776
+ "#ff6699",
1777
+ "#5666ad",
1778
+ "#b2b266",
1779
+ "#e2976b",
1780
+ "#84c887",
1781
+ "#4d9dd0",
1782
+ "#a187f7",
1783
+ "#d9745b",
1784
+ "#862929",
1785
+ "#ea80fc",
1786
+ "#8c4391",
1787
+ "#ff8c00",
1788
+ "#53915b",
1789
+ "#6dcbab",
1790
+ "#7b7353",
1791
+ "#d4e157",
1792
+ "#cc242f",
1793
+ "#c69e31",
1794
+ "#fdd835",
1795
+ // Tableau20
1796
+ "#4E79A7",
1797
+ "#A0CBE8",
1798
+ "#F28E2B",
1799
+ "#FFBE7D",
1800
+ "#59A14F",
1801
+ "#8CD17D",
1802
+ "#B6992D",
1803
+ "#F1CE63",
1804
+ "#499894",
1805
+ "#86BCB6",
1806
+ "#E15759",
1807
+ "#FF9D9A",
1808
+ "#79706E",
1809
+ "#BAB0AC",
1810
+ "#D37295",
1811
+ "#FABFD2",
1812
+ "#B07AA1",
1813
+ "#D4A6C8",
1814
+ "#9D7660",
1815
+ "#D7B5A6",
1816
+ ]
1817
+ }
1818
+ },
1819
+ mounted() {
1820
+ this.buildChart(this);
1821
+ },
1822
+ watch: {
1823
+ data: {
1824
+ deep: true,
1825
+ handler(newValue) {
1826
+ this.myChart.destroy();
1827
+ this.config.data = newValue;
1828
+ this.buildChart(this);
1829
+ }
1830
+ }
1831
+ },
1832
+ methods: {
1833
+
1834
+ buildChart(self) {
1835
+ if (self.reverseData && self.config.data.labels) self.config.data.labels = _.reverse(self.config.data.labels);
1836
+ let n = parseInt(Math.random() * 45);
1837
+ _.forEach(self.config.data.datasets, function (v, k) {
1838
+ if (self.reverseData && v.data) v.data = _.reverse(v.data);
1839
+ if (!v.type) v.type = self.config.type;
1840
+
1841
+ if (v.type == 'pie' || v.type == 'doughnut' || v.type == 'polarArea') {
1842
+ if (!v.backgroundColor) v.backgroundColor = self.chart_color;
1843
+ } else {
1844
+ if (!v.borderColor) v.borderColor = self.chart_color[k + n % self.chart_color.length];
1845
+ if (!v.backgroundColor) v.backgroundColor = transparentize(v.borderColor, 0.6);
1846
+ if (v.type && v.type != 'line') if (!v.borderWidth) v.borderWidth = 1;
1847
+ }
1848
+ });
1849
+
1850
+ self.myChart = new Chart(self.$refs.myChart, self.config);
1851
+ },
1852
+ }
1853
+ });
1854
+ //===========================================================================================
1855
+ Vue.component('FWindow', {
1856
+ props: ['winData'],
1857
+ template: `
1858
+ <v-dialog v-model="dialogOpen" :fullscreen="(prop.width || prop.height != '100%'?false:true)" :width="prop.width" :max-width="prop.width" :height="prop.height" :max-height="prop.height" scrollable persistent :transition="prop.transition">
1859
+ <v-card height="100%">
1860
+ <v-toolbar v-if="prop.headerbar" density="compact" :color="prop.toolbarcolor">
1861
+ <v-toolbar-title class="font-weight-medium" v-html="prop.title" style="max-height: 48px; font-size: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"></v-toolbar-title>
1862
+ <f-button :checkvalid=false v-for="(item,i) in prop.button" :key="i" text v-bind="item"></f-button>
1863
+ <v-btn icon @click="closeDialog" class="ml-4"><v-icon>mdi-close</v-icon></v-btn>
1864
+ </v-toolbar>
1865
+ <iframe :src="url" v-bind="$attrs" :id="prop.id" name='frameName' height="100%" width="100%" style="border:none;" allow="fullscreen"></iframe>
1866
+ </v-card>
1867
+ </v-dialog>
1868
+ `,
1869
+ data: function () {
1870
+ var propInit = _.cloneDeep(this.winData)
1871
+
1872
+ if (!propInit.id) propInit.id = "Window" + generateID();
1873
+ if (typeof propInit.height == 'undefined') propInit.height = '100%';
1874
+ if (_.isInteger(propInit.height)) propInit.height = propInit.height + 'px';
1875
+ if (typeof propInit.toolbarcolor == 'undefined' && !(propInit.height != '100%' || propInit.width)) propInit.toolbarcolor = 'primary';
1876
+
1877
+ if (typeof propInit.headerbar == 'undefined') propInit.headerbar = true;
1878
+ if (typeof propInit.transition == 'undefined') propInit.transition = 'scale-transition';
1879
+
1880
+ if (propInit.button && _.isPlainObject(propInit.button)) propInit.button = [propInit.button];
1881
+
1882
+ return {
1883
+ prop: propInit,
1884
+ url: null,
1885
+ dialogOpen: true,
1886
+ }
1887
+ },
1888
+ mounted: function () {
1889
+ var self = this;
1890
+ setTimeout(function () {
1891
+ // console.log(self.prop.url);
1892
+ self.url = self.prop.url;
1893
+ }, 250);
1894
+ $('html').css({
1895
+ 'cssText': 'overflow: hidden !important'
1896
+ });
1897
+ },
1898
+ methods: {
1899
+ closeDialog() {
1900
+ var selfProp = this.prop;
1901
+ $('html').css({ "overflow": "" });
1902
+
1903
+ setTimeout(() => {
1904
+ if (selfProp.onclose) runAction(selfProp.onclose, selfProp);
1905
+ _.remove(app.config.globalProperties.v_OpenWindowList, function (n) {
1906
+ return n.uuid == selfProp.uuid;
1907
+ });
1908
+ }, 10);
1909
+ this.dialogOpen = false;
1910
+ },
1911
+ },
1912
+ destroyed() {
1913
+ console.log("window destroyed : ", this.$attrs.uuid)
1914
+ }
1915
+ });
1916
+
1917
+ //===========================================================================================
1918
+ Vue.component('FEditor', {
1919
+ props: {
1920
+ modelValue: {
1921
+ type: String,
1922
+ default: null,
1923
+ },
1924
+ imageapi: {
1925
+ type: String,
1926
+ default: 'https://fap.vn',
1927
+ },
1928
+ height: {
1929
+ type: [Number, String],
1930
+ default: null,
1931
+ },
1932
+ toolbar: {
1933
+ type: Array,
1934
+ default: null,
1935
+ },
1936
+ },
1937
+ template: `<div ref="elID" v-bind="$attrs"></div>`,
1938
+ data: function () {
1939
+ return {
1940
+ emitData: true,
1941
+ Editor: null
1942
+ }
1943
+ },
1944
+ watch: {
1945
+ modelValue: function (newVal, oldVal) {
1946
+ this.$nextTick(() => {
1947
+ if (!this.emitData && newVal && this.Editor) this.Editor.setData(newVal);
1948
+ this.emitData = false;
1949
+ });
1950
+ }
1951
+ },
1952
+ mounted: function () {
1953
+ setTimeout(() => {
1954
+ ClassicEditor.create(this.$refs.elID, {
1955
+ toolbar: {
1956
+ shouldNotGroupWhenFull: true,
1957
+ items: (this.toolbar ? this.toolbar : [
1958
+ 'undo',
1959
+ 'redo',
1960
+ 'removeFormat',
1961
+ 'heading',
1962
+ 'fontFamily',
1963
+ 'fontSize',
1964
+ '|',
1965
+ 'fontColor',
1966
+ 'fontBackgroundColor',
1967
+ 'highlight',
1968
+ '|',
1969
+ 'horizontalLine',
1970
+ 'subscript',
1971
+ 'superscript',
1972
+ 'blockQuote',
1973
+ '|',
1974
+ 'MathType',
1975
+ 'ChemType',
1976
+ 'specialCharacters',
1977
+ '|',
1978
+ 'bulletedList',
1979
+ 'numberedList',
1980
+ 'outdent',
1981
+ 'indent',
1982
+ '|',
1983
+ 'alignment',
1984
+ 'bold',
1985
+ 'italic',
1986
+ 'underline',
1987
+ 'strikethrough',
1988
+ '|',
1989
+ 'imageUpload',
1990
+ 'link',
1991
+ 'insertTable',
1992
+ 'mediaEmbed',
1993
+ ])
1994
+ },
1995
+ language: 'en',
1996
+ mediaEmbed: {
1997
+ toolbar: ['mediaEmbed']
1998
+ },
1999
+ image: {
2000
+ toolbar: [
2001
+ 'imageStyle:alignLeft', 'imageStyle:alignCenter', 'imageStyle:alignRight',
2002
+ '|',
2003
+ 'resizeImage',
2004
+ '|',
2005
+ 'imageTextAlternative',
2006
+ 'linkImage'
2007
+ ],
2008
+ styles: [
2009
+ 'alignLeft', 'alignCenter', 'alignRight'
2010
+ ],
2011
+ },
2012
+ table: {
2013
+ contentToolbar: [
2014
+ 'tableColumn',
2015
+ 'tableRow',
2016
+ 'mergeTableCells',
2017
+ 'tableCellProperties',
2018
+ 'tableProperties'
2019
+ ]
2020
+ },
2021
+ licenseKey: '',
2022
+ simpleUpload: {
2023
+ // The URL that the images are uploaded to.
2024
+ uploadUrl: this.imageapi,
2025
+ // Enable the XMLHttpRequest.withCredentials property.
2026
+ withCredentials: false,
2027
+ // Headers sent along with the XMLHttpRequest to the upload server.
2028
+ headers: {
2029
+ //'X-CSRF-TOKEN': 'CSRF-Token',
2030
+ Authorization: 'Bearer ' + getCookie('awt')
2031
+ }
2032
+ },
2033
+
2034
+ }).then(editor => {
2035
+ this.Editor = markRaw(editor);
2036
+
2037
+ if (this.modelValue) editor.setData(this.modelValue);
2038
+
2039
+ editor.editing.view.change(writer => {
2040
+ if (!this.height) this.height = 200;
2041
+ writer.setStyle({ 'height': this.height + 'px' }, editor.editing.view.document.getRoot());
2042
+ });
2043
+
2044
+ editor.model.document.on('change:data', () => {
2045
+ if (this.modelValue != editor.getData()) {
2046
+ this.emitData = true;
2047
+ this.$emit('update:modelValue', editor.getData());
2048
+ }
2049
+ });
2050
+
2051
+ this.$emit('update:modelValue', editor.getData());
2052
+ }).catch(error => {
2053
+ console.log(error);
2054
+ });
2055
+ }, 100);
2056
+ },
2057
+ methods: {}
2058
+ });
2059
+ //===========================================================================================
2060
+ Vue.component('FEditorDialog', {
2061
+ props: {
2062
+ modelValue: {
2063
+ type: Object,
2064
+ default: null,
2065
+ },
2066
+ imageapi: {
2067
+ type: String,
2068
+ default: 'https://fap.vn',
2069
+ },
2070
+ label: {
2071
+ type: String,
2072
+ default: null,
2073
+ },
2074
+ },
2075
+ template: `
2076
+ <v-overlay v-model="modelValue.open" persistent class="align-center justify-center">
2077
+ <v-card light elevation="10" width="800" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); -webkit-transform: translate(-50%, -50%); ">
2078
+ <v-toolbar flat>
2079
+ <v-toolbar-title>{{label}}</v-toolbar-title>
2080
+ <v-spacer></v-spacer>
2081
+ <v-btn text color="orange darken-2" @click="closeDialog(0)" class="mr-6">
2082
+ <v-icon left>mdi-close</v-icon>Cancel
2083
+ </v-btn>
2084
+ <v-btn text color="primary" @click="closeDialog(1)">
2085
+ <v-icon left>mdi-check</v-icon>OK
2086
+ </v-btn>
2087
+ </v-toolbar>
2088
+ <v-container text-left fluid class="pt-0">
2089
+ <v-layout>
2090
+ <v-col class="pa-0" :style="{height: windowHeight + 'px'}">
2091
+ <f-editor v-model="textData" :imageapi="imageapi" :height="windowHeight - 80"></f-editor>
2092
+ </v-col>
2093
+ </v-layout>
2094
+
2095
+ </v-container>
2096
+ </v-card>
2097
+ </v-overlay>
2098
+ `,
2099
+ data: function () {
2100
+ return {
2101
+ textData: this.getData(this.modelValue),
2102
+ windowHeight: $(window).height() * 0.9 - 70,
2103
+ }
2104
+ },
2105
+ watch: {
2106
+ 'modelValue.text': {
2107
+ handler(newVal, oldVal) {
2108
+ this.textData = this.getData(this.modelValue);
2109
+ }
2110
+ },
2111
+ 'modelValue.object': {
2112
+ deep: true,
2113
+ handler(newVal, oldVal) {
2114
+ this.textData = this.getData(this.modelValue);
2115
+ }
2116
+ },
2117
+ },
2118
+ methods: {
2119
+ getData(d) {
2120
+ if (!d) return "";
2121
+ if (d.object) return d.object[d.text];
2122
+ else return d.text;
2123
+ },
2124
+ closeDialog(e) {
2125
+ if (e == 1) {
2126
+ if (this.modelValue.object) this.modelValue.object[this.modelValue.text] = this.textData;
2127
+ else this.modelValue.text = this.textData;
2128
+
2129
+ this.modelValue.ok = true;
2130
+ } else this.modelValue.ok = false;
2131
+ this.modelValue.open = false;
2132
+ },
2133
+ },
2134
+ });
2135
+ //===========================================================================================
2136
+ Vue.component('FDialog', {
2137
+ props: {
2138
+ modelValue: {
2139
+ type: Boolean,
2140
+ default: false
2141
+ },
2142
+ data: {
2143
+ type: Object,
2144
+ default: {}
2145
+ },
2146
+ title: {
2147
+ type: String,
2148
+ default: ''
2149
+ },
2150
+ controls: {
2151
+ type: Array,
2152
+ default: []
2153
+ },
2154
+ watch: {
2155
+ type: Object,
2156
+ default: {}
2157
+ },
2158
+ button: {
2159
+ default: []
2160
+ },
2161
+ disableValidate: {
2162
+ type: Boolean,
2163
+ default: false
2164
+ },
2165
+ width: {
2166
+ type: Number,
2167
+ default: 800
2168
+ },
2169
+ dialogModel: {
2170
+ type: String,
2171
+ default: ''
2172
+ },
2173
+ openAction: {
2174
+ type: Object,
2175
+ default: null
2176
+ },
2177
+ },
2178
+ template: `
2179
+ <v-dialog v-model="modelValue" :width="width" persistent>
2180
+ <v-card>
2181
+ <v-toolbar color="white" density="compact"><v-toolbar-title class="ml-4">{{ title }}</v-toolbar-title><v-spacer></v-spacer><v-btn @click="$emit('update:modelValue', false)" icon="mdi-close"></v-btn></v-toolbar>
2182
+ <FDynamicComponent :componentData="dynData" @update:dataOut="updateDataOut"/>
2183
+ </v-card>
2184
+ </v-dialog>
2185
+ `,
2186
+ data: function () {
2187
+ var controlsForm = [{
2188
+ prop: {
2189
+ class: "grid-list-lg py-2"
2190
+ },
2191
+ rows: []
2192
+ }];
2193
+
2194
+ var row = {
2195
+ prop: "",
2196
+ cols: []
2197
+ };
2198
+
2199
+ var updateUI = bindData(this.controls);
2200
+
2201
+ if (_.isArray(updateUI) && updateUI.length > 0) {
2202
+ if (updateUI[0].rows) controlsForm = updateUI;
2203
+ else {
2204
+ _.forEach(updateUI, function (c) {
2205
+ if (!c.w) c.w = 12
2206
+ row.cols.push(c);
2207
+ if (c.br) {
2208
+ controlsForm[0].rows.push(_.cloneDeep(row));
2209
+ row = {
2210
+ prop: "",
2211
+ cols: []
2212
+ };
2213
+ }
2214
+ });
2215
+ if (row.cols.length > 0) controlsForm[0].rows.push(_.cloneDeep(row));
2216
+ }
2217
+ };
2218
+
2219
+ var comData = {};
2220
+ var controlsFormDOM = buildModuleUI(controlsForm, comData);
2221
+ //------------------------------------------------------------------------------------------------------------
2222
+ return {
2223
+ dynData: {
2224
+ template: `<v-form ref="form" @submit.prevent>${controlsFormDOM[0].outerHTML}<v-card-actions><v-spacer></v-spacer>${this.buildButton(this.button)}</v-card-actions></v-form>`,
2225
+ props: {
2226
+ innerDataOut: {
2227
+ type: Object,
2228
+ default: {}
2229
+ },
2230
+ },
2231
+ data: () => { return { vueData, ...comData, ...this.data, ...this.$props, v_outData: {} } },
2232
+ mounted: function () {
2233
+ _.forEach(Object.keys(this.data), (k) => {
2234
+ this.v_outData[k] = computed(() => this[k]);
2235
+ });
2236
+
2237
+ _.forEach(Object.keys(comData), (k) => {
2238
+ if (!this.v_outData[k]) this.v_outData[k] = computed(() => this[k]);
2239
+ });
2240
+
2241
+ this.watchDialog(this.watch);
2242
+ if (this.openAction) runAction(this.openAction, this.v_outData);
2243
+ console.log("Dialog Data:", this.v_outData)
2244
+ },
2245
+ methods: {
2246
+ async formValidate() {
2247
+ if (this.disableValidate) return true;
2248
+ const { valid } = await this.$refs.form.validate()
2249
+ return valid
2250
+ },
2251
+ watchDialog(obj) {
2252
+ if (_.isPlainObject(obj)) {
2253
+ _.forEach(obj, (value, key) => {
2254
+ console.log("ADD dialog watch : " + key);
2255
+ if (typeof (this[key]) == "undefined") this[key] = undefined;
2256
+ this.$watch(key, (newValue, oldValue) => {
2257
+ runAction(value, this.v_outData);
2258
+ });
2259
+ });
2260
+ }
2261
+ },
2262
+ buttonclick: _.debounce(async function (e) {
2263
+ if (e.type == 'submit' || e.checkvalid) {
2264
+ if (!await this.formValidate()) {
2265
+ showMessage({ icon: 'mdi mdi-alert-outline', type: "red", title: "Hãy điền đầy đủ dữ liệu bắt buộc !" })
2266
+ return;
2267
+ }
2268
+ }
2269
+
2270
+ console.log("Dialog Action", e.action, this.v_outData)
2271
+
2272
+ if (e.type == 'submit') {
2273
+ this.$emit('update:innerDataOut', this.v_outData);
2274
+ }
2275
+
2276
+ if (_.isFunction(e.action)) e.action();
2277
+ else
2278
+ if (e.action) {
2279
+ runAction(e.action, this.v_outData);
2280
+ }
2281
+
2282
+ }, 500, { leading: true, trailing: false }),
2283
+ },
2284
+ },
2285
+ }
2286
+ },
2287
+ methods: {
2288
+ updateDataOut(newValue) {
2289
+ this.$emit('update:data', _.cloneDeep(newValue));
2290
+ },
2291
+ buildButton(buttonArray) {
2292
+ var buttonContainer = $("<div>");
2293
+ _.forEach(buttonArray, function (C, index) {
2294
+ C = _.assignIn({
2295
+ elevation: 0,
2296
+ color: "primary",
2297
+ text: true,
2298
+ }, C);
2299
+
2300
+ delete C.action;
2301
+
2302
+ var btn = $("<v-btn>").attr(C);
2303
+ btn.attr({ "v-on:click": "buttonclick(button[" + index + "])" });
2304
+
2305
+ if (C['icon-text']) btn.append("<v-icon left>" + C['icon-text'] + "</v-icon>" + C['label']);
2306
+ else btn.append(C['label']);
2307
+
2308
+ buttonContainer.append(btn);
2309
+ });
2310
+
2311
+ return buttonContainer[0].innerHTML;
2312
+ },
2313
+ }
2314
+ });
2315
+
2316
+ //===========================================================================================
2317
+ Vue.component('FPdfmake', {
2318
+ props: ['data'],
2319
+ template: `
2320
+ <v-card elevation="0">
2321
+ <v-overlay v-model="overlay" persistent class="align-center justify-center"><v-progress-circular indeterminate color="white"></v-progress-circular></v-overlay>
2322
+ <iframe id="pdfmakeViewer" v-bind="$attrs" style="border: thin solid rgba(0,0,0,.12); margin:0;"></iframe>
2323
+ </v-card>
2324
+ `,
2325
+ data: function () {
2326
+ return {
2327
+ overlay: true
2328
+ }
2329
+ },
2330
+ mounted: function () {
2331
+ var obj = this;
2332
+ printPDF({
2333
+ data: _.cloneDeep(obj.data),
2334
+ viewer: '#pdfmakeViewer',
2335
+ callBack: function () {
2336
+ obj.overlay = false;
2337
+ }
2338
+ });
2339
+ },
2340
+ watch: {
2341
+ data: {
2342
+ deep: true,
2343
+ handler(newVal) {
2344
+ printPDF({ data: _.cloneDeep(newVal), viewer: '#pdfmakeViewer' });
2345
+ }
2346
+ }
2347
+ },
2348
+
2349
+ });
2350
+
2351
+ //===========================================================================================
2352
+ Vue.component('FExcelReader', {
2353
+ emits: ['update:modelValue', 'load', 'error', 'action-error'],
2354
+ props: {
2355
+ modelValue: { type: Array, default: () => [] },
2356
+
2357
+ header: { type: [String, Number, Array], default: null },
2358
+ raw: { type: [Boolean, String], default: false },
2359
+ dateFormat: { type: String, default: 'dd/mm/yyyy' },
2360
+ defval: { type: [String, Number], default: '' },
2361
+
2362
+ sheet: { type: [String, Number], default: 0 },
2363
+ skipRows: { type: [String, Number], default: 0 },
2364
+
2365
+ label: { type: String, default: 'Load Excel' },
2366
+ iconText: { type: String, default: 'mdi-grid' },
2367
+ accept: { type: String, default: '.xlsx,.xlsb,.xlsm,.xls,.csv,.txt,.ods,.dbf,.prn,.html,.htm' },
2368
+ maxSize: { type: [String, Number], default: 20 },
2369
+ loadTimeout: { type: [String, Number], default: 15000 },
2370
+
2371
+ action: { type: Object, default: null }
2372
+ },
2373
+ template: `
2374
+ <div>
2375
+ <v-btn :text="label" v-bind="$attrs" :prepend-icon="iconText"
2376
+ :disabled="parsing" :loading="busy" @click="onButtonClick"></v-btn>
2377
+ <input ref="uploader" style="display:none" type="file" :multiple="false"
2378
+ :accept="accept" @change="onchange" onclick="this.value=null;">
2379
+ </div>
2380
+ `,
2381
+ data() {
2382
+ return { busy: false, parsing: false, reqId: 0 };
2383
+ },
2384
+ computed: {
2385
+ optHeader() {
2386
+ const h = this.header;
2387
+ if (h === null || h === undefined || h === '') return null;
2388
+ if (Array.isArray(h)) return h;
2389
+ const s = String(h).trim();
2390
+ if (s === '1') return 1;
2391
+ if (s.toUpperCase() === 'A') return 'A';
2392
+ return h;
2393
+ },
2394
+ optRaw() {
2395
+ if (typeof this.raw === 'boolean') return this.raw;
2396
+ return ['true', '1', 'yes'].includes(String(this.raw).trim().toLowerCase());
2397
+ },
2398
+ optRange() {
2399
+ const n = parseInt(this.skipRows, 10);
2400
+ return n > 0 ? n : undefined;
2401
+ },
2402
+ limitMB() {
2403
+ const mb = parseFloat(this.maxSize);
2404
+ return isNaN(mb) ? 20 : mb; // NaN -> default, KHÔNG bỏ giới hạn
2405
+ }
2406
+ },
2407
+ methods: {
2408
+ onButtonClick() {
2409
+ if (this.parsing) return; // (2) chống click chồng
2410
+ this.busy = true;
2411
+ window.addEventListener('focus', () => {
2412
+ setTimeout(() => { if (!this.parsing) this.busy = false; }, 300);
2413
+ }, { once: true });
2414
+ this.$refs.uploader.click();
2415
+ },
2416
+
2417
+ async onchange(evt) {
2418
+ const file = evt.target.files && evt.target.files[0];
2419
+ evt.target.value = '';
2420
+
2421
+ if (this.parsing) return; // (2)
2422
+ if (!file) { this.busy = false; return; }
2423
+
2424
+ const id = ++this.reqId; // (1) vé của lượt này
2425
+ this.parsing = true;
2426
+ this.busy = true;
2427
+
2428
+ let result;
2429
+ try {
2430
+ if (this.limitMB > 0 && file.size > this.limitMB * 1024 * 1024)
2431
+ throw new Error('File vượt quá ' + this.limitMB + 'MB');
2432
+
2433
+ await this.ensureXLSX();
2434
+ result = await this.parseFile(file);
2435
+
2436
+ if (id !== this.reqId) return; // (1) kết quả cũ -> vứt
2437
+ } catch (err) {
2438
+ if (id === this.reqId) this.fail(err, file);
2439
+ else console.warn('[FExcelReader] bỏ qua lỗi của lượt cũ:', err);
2440
+ return;
2441
+ } finally {
2442
+ if (id === this.reqId) { this.parsing = false; this.busy = false; }
2443
+ }
2444
+
2445
+ // ===== Từ đây trở xuống: Excel ĐÃ đọc thành công, không thể thành error =====
2446
+ this.$emit('update:modelValue', result.rows);
2447
+ this.$emit('load', result.meta);
2448
+
2449
+ await this.$nextTick();
2450
+ this.runAction(file); // (3) tách hẳn khỏi try
2451
+ },
2452
+
2453
+ ensureXLSX() {
2454
+ return new Promise((resolve, reject) => {
2455
+ if (typeof XLSX !== 'undefined') return resolve();
2456
+ if (typeof Loader === 'undefined')
2457
+ return reject(new Error('Không tìm thấy Loader để nạp thư viện XLSX'));
2458
+
2459
+ let settled = false;
2460
+ const ms = parseInt(this.loadTimeout, 10) || 15000;
2461
+ const timer = setTimeout(() => {
2462
+ if (settled) return;
2463
+ settled = true;
2464
+ reject(new Error('Quá thời gian nạp thư viện XLSX'));
2465
+ }, ms);
2466
+
2467
+ new Loader().require(['/include/excel/xlsx.full.min.js'], () => {
2468
+ if (settled) return;
2469
+ settled = true;
2470
+ clearTimeout(timer);
2471
+ typeof XLSX !== 'undefined'
2472
+ ? resolve()
2473
+ : reject(new Error('Nạp thư viện XLSX thất bại'));
2474
+ });
2475
+ });
2476
+ },
2477
+
2478
+ readBuffer(file) {
2479
+ return new Promise((resolve, reject) => {
2480
+ const reader = new FileReader();
2481
+ reader.onload = e => resolve(e.target.result);
2482
+ reader.onerror = () => reject(new Error('Không đọc được file: ' + file.name));
2483
+ reader.onabort = () => reject(new Error('Việc đọc file bị hủy'));
2484
+ reader.readAsArrayBuffer(file);
2485
+ });
2486
+ },
2487
+
2488
+ async parseFile(file) {
2489
+ const buf = await this.readBuffer(file);
2490
+ const wb = XLSX.read(new Uint8Array(buf), { type: 'array', cellDates: true });
2491
+
2492
+ const isIndex = typeof this.sheet === 'number' || /^\d+$/.test(String(this.sheet));
2493
+ const wsname = isIndex
2494
+ ? wb.SheetNames[parseInt(this.sheet, 10)]
2495
+ : (wb.SheetNames.includes(this.sheet) ? this.sheet : wb.SheetNames[0]);
2496
+
2497
+ const ws = wb.Sheets[wsname];
2498
+ if (!ws) throw new Error('Không tìm thấy sheet: ' + this.sheet);
2499
+
2500
+ const rows = XLSX.utils.sheet_to_json(ws, {
2501
+ header: this.optHeader,
2502
+ raw: this.optRaw,
2503
+ dateNF: this.dateFormat,
2504
+ defval: this.defval,
2505
+ range: this.optRange,
2506
+ blankrows: false
2507
+ });
2508
+
2509
+ return {
2510
+ rows,
2511
+ meta: {
2512
+ rows,
2513
+ rowCount: rows.length,
2514
+ headers: this.optHeader === 1 && rows.length ? rows[0] : Object.keys(rows[0] || {}),
2515
+ sheetName: wsname,
2516
+ sheetNames: wb.SheetNames,
2517
+ fileName: file.name
2518
+ }
2519
+ };
2520
+ },
2521
+
2522
+ runAction(file) {
2523
+ if (!this.action || typeof CALL !== 'function') return;
2524
+ const onErr = (err) => {
2525
+ console.error('[FExcelReader] action lỗi (dữ liệu Excel vẫn hợp lệ):', err);
2526
+ this.$emit('action-error', {
2527
+ message: (err && err.message) || String(err),
2528
+ fileName: file.name
2529
+ });
2530
+ };
2531
+ try {
2532
+ const r = CALL(this.action);
2533
+ if (r && typeof r.then === 'function') r.then(null, onErr); // action async
2534
+ } catch (err) {
2535
+ onErr(err);
2536
+ }
2537
+ },
2538
+
2539
+ fail(err, file) {
2540
+ console.error('[FExcelReader]', err);
2541
+ this.parsing = false;
2542
+ this.busy = false;
2543
+ this.$emit('error', {
2544
+ message: (err && err.message) || String(err),
2545
+ fileName: file ? file.name : null
2546
+ });
2547
+ }
2548
+ }
2549
+ });