@ngockhoale/ukit 1.1.6
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.
- package/CHANGELOG.md +179 -0
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/bin/ukit +30 -0
- package/manifests/platform.full.yaml +1194 -0
- package/package.json +71 -0
- package/scripts/bug/triage.mjs +37 -0
- package/scripts/index/build-index.mjs +35 -0
- package/scripts/index/query-index.mjs +92 -0
- package/scripts/index/refresh-index.mjs +85 -0
- package/scripts/release/verify-release.mjs +56 -0
- package/src/bug/triageBug.js +123 -0
- package/src/cli/adapters.js +148 -0
- package/src/cli/commands/diff.js +51 -0
- package/src/cli/commands/doctor.js +125 -0
- package/src/cli/commands/indexArgs.js +73 -0
- package/src/cli/commands/indexTools.js +509 -0
- package/src/cli/commands/install.js +293 -0
- package/src/cli/commands/memory.js +126 -0
- package/src/cli/commands/status.js +8 -0
- package/src/cli/commands/uninstall.js +51 -0
- package/src/cli/index.js +109 -0
- package/src/context/detectProjectContext.js +49 -0
- package/src/context/detectProviders.js +12 -0
- package/src/core/applyPlan.js +89 -0
- package/src/core/buildPlan.js +228 -0
- package/src/core/compact/index.js +294 -0
- package/src/core/compact/threshold.js +936 -0
- package/src/core/diffPlan.js +73 -0
- package/src/core/ensureGitignore.js +117 -0
- package/src/core/fileOps.js +188 -0
- package/src/core/memory/hygiene.js +160 -0
- package/src/core/memory/index.js +2 -0
- package/src/core/memory/retrieval.js +476 -0
- package/src/core/memory/store.js +202 -0
- package/src/core/metadata.js +132 -0
- package/src/core/migrateLegacy.js +139 -0
- package/src/core/output/index.js +1309 -0
- package/src/core/paths.js +13 -0
- package/src/core/report.js +17 -0
- package/src/core/router/advisor.js +42 -0
- package/src/core/router/index.js +2 -0
- package/src/core/router/router.js +164 -0
- package/src/core/runInstallPipeline.js +365 -0
- package/src/core/runtimeConfig.js +190 -0
- package/src/core/runtimePaths.js +24 -0
- package/src/core/status.js +186 -0
- package/src/core/token/index.js +328 -0
- package/src/core/uninstall.js +246 -0
- package/src/core/validation/confidence.js +89 -0
- package/src/core/validation/index.js +2 -0
- package/src/core/validation/validator.js +165 -0
- package/src/index/buildIndex.js +1392 -0
- package/src/index/gitHooks.js +109 -0
- package/src/index/importResolution.js +377 -0
- package/src/index/languageTools.js +127 -0
- package/src/index/paths.js +27 -0
- package/src/index/queryIndex.js +637 -0
- package/src/index/relatedTests.js +237 -0
- package/src/index/resolveContext.js +345 -0
- package/src/index/routeCatalog.js +258 -0
- package/src/index/taskRouting.js +677 -0
- package/src/index/verificationPlan.js +437 -0
- package/src/manifest/loadManifest.js +22 -0
- package/src/manifest/selectItems.js +78 -0
- package/src/manifest/validateManifest.js +115 -0
- package/src/render/buildVariables.js +39 -0
- package/src/render/renderTemplate.js +44 -0
- package/src/stack/detectStack.js +213 -0
- package/templates/.claude/agents/bug-debugger.md +57 -0
- package/templates/.claude/agents/feature-implementer.md +55 -0
- package/templates/.claude/config/providers.md +25 -0
- package/templates/.claude/hooks/auto-allow-bash.sh +155 -0
- package/templates/.claude/hooks/auto-prune-bash.sh +75 -0
- package/templates/.claude/hooks/block-dangerous.sh +54 -0
- package/templates/.claude/hooks/compress-output.sh +17 -0
- package/templates/.claude/hooks/protect-files.sh +37 -0
- package/templates/.claude/hooks/reinject-context.sh +28 -0
- package/templates/.claude/hooks/session-start.md +13 -0
- package/templates/.claude/hooks/skill-router.sh +1681 -0
- package/templates/.claude/hooks/verification-guard.sh +271 -0
- package/templates/.claude/settings.json +144 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/mce/mc.xsd +75 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/pack.py +159 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/unpack.py +29 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validate.py +69 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validation/__init__.py +15 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validation/base.py +951 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validation/docx.py +274 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validation/pptx.py +315 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validation/redlining.py +279 -0
- package/templates/.claude/skills/backend-api/SKILL.md +26 -0
- package/templates/.claude/skills/canvas-design/LICENSE.txt +202 -0
- package/templates/.claude/skills/canvas-design/SKILL.md +130 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
- package/templates/.claude/skills/code-review/SKILL.md +97 -0
- package/templates/.claude/skills/debugging-toolkit/SKILL.md +156 -0
- package/templates/.claude/skills/delivery/SKILL.md +92 -0
- package/templates/.claude/skills/discover-security/SKILL.md +86 -0
- package/templates/.claude/skills/docker-packaging/SKILL.md +60 -0
- package/templates/.claude/skills/docs-manager/SKILL.md +465 -0
- package/templates/.claude/skills/docs-manager/init-project-docs.sh +70 -0
- package/templates/.claude/skills/docs-manager/templates/README.md.template +50 -0
- package/templates/.claude/skills/docs-manager/templates/agent-roles.md.template +24 -0
- package/templates/.claude/skills/docs-manager/templates/coding-conventions.md.template +28 -0
- package/templates/.claude/skills/docs-manager/templates/memory.md.template +30 -0
- package/templates/.claude/skills/docs-manager/templates/onboarding.md.template +20 -0
- package/templates/.claude/skills/docs-manager/templates/project.md.template +26 -0
- package/templates/.claude/skills/docs-quality/SKILL.md +148 -0
- package/templates/.claude/skills/docx/LICENSE.txt +30 -0
- package/templates/.claude/skills/docx/SKILL.md +197 -0
- package/templates/.claude/skills/docx/docx-js.md +350 -0
- package/templates/.claude/skills/docx/ooxml.md +610 -0
- package/templates/.claude/skills/docx/scripts/__init__.py +1 -0
- package/templates/.claude/skills/docx/scripts/document.py +1276 -0
- package/templates/.claude/skills/docx/scripts/templates/comments.xml +3 -0
- package/templates/.claude/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/templates/.claude/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/templates/.claude/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/templates/.claude/skills/docx/scripts/templates/people.xml +3 -0
- package/templates/.claude/skills/docx/scripts/utilities.py +374 -0
- package/templates/.claude/skills/duraone/SKILL.md +204 -0
- package/templates/.claude/skills/duraone/references/backend.md +636 -0
- package/templates/.claude/skills/duraone/references/frontend.md +1506 -0
- package/templates/.claude/skills/duraone/references/sql.md +631 -0
- package/templates/.claude/skills/duraone/references/workflow.md +520 -0
- package/templates/.claude/skills/executing-plans/SKILL.md +76 -0
- package/templates/.claude/skills/file-organizer/SKILL.md +433 -0
- package/templates/.claude/skills/frontend/SKILL.md +26 -0
- package/templates/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/templates/.claude/skills/frontend-design/SKILL.md +42 -0
- package/templates/.claude/skills/frontend-vue/SKILL.md +127 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Box.vue +137 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Button.vue +93 -0
- package/templates/.claude/skills/frontend-vue/components/Control/ButtonBar.vue +29 -0
- package/templates/.claude/skills/frontend-vue/components/Control/ButtonFloat.vue +62 -0
- package/templates/.claude/skills/frontend-vue/components/Control/CheckButton.vue +75 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Checkbox.vue +58 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Datetime.vue +148 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Dropdownlist.vue +156 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Input.vue +106 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Label.vue +38 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Master/BoxColumn.vue +24 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Popup/Confirm.vue +33 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Popup/Info.vue +32 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Popup/ModalInfo.vue +39 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Popup/Reject.vue +64 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Tag.vue +82 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Upload.vue +61 -0
- package/templates/.claude/skills/frontend-vue/components/ControlMobile/Dropdownlist.vue +103 -0
- package/templates/.claude/skills/frontend-vue/components/ControlMobile/PagingBar.vue +108 -0
- package/templates/.claude/skills/frontend-vue/components/ControlMobile/UploadImage.vue +137 -0
- package/templates/.claude/skills/frontend-vue/components/Grid/AG.vue +806 -0
- package/templates/.claude/skills/frontend-vue/components/Grid/AntTable.vue +253 -0
- package/templates/.claude/skills/frontend-vue/components/Grid/CustomDropdownEditor.vue +43 -0
- package/templates/.claude/skills/frontend-vue/components/Grid/CustomDropdownEditorEnable.vue +55 -0
- package/templates/.claude/skills/frontend-vue/components/Grid/HtmlTable.vue +40 -0
- package/templates/.claude/skills/frontend-vue/components/PDFViewer.vue +25 -0
- package/templates/.claude/skills/frontend-vue/components/Panel/FormView.vue +309 -0
- package/templates/.claude/skills/frontend-vue/components/Partial/Footer.vue +23 -0
- package/templates/.claude/skills/frontend-vue/components/Partial/Header.vue +265 -0
- package/templates/.claude/skills/frontend-vue/components/Partial/Sidebar.vue +122 -0
- package/templates/.claude/skills/frontend-vue/components/Template.vue +16 -0
- package/templates/.claude/skills/frontend-vue/components/View/Form.vue +89 -0
- package/templates/.claude/skills/frontend-vue/composables/indexDBStore.js +140 -0
- package/templates/.claude/skills/frontend-vue/composables/masterApi.js +362 -0
- package/templates/.claude/skills/frontend-vue/composables/state.js +578 -0
- package/templates/.claude/skills/frontend-vue/composables/useRequest.js +221 -0
- package/templates/.claude/skills/frontend-vue/composables/useSession.js +179 -0
- package/templates/.claude/skills/frontend-vue/composables/useTranslation.js +54 -0
- package/templates/.claude/skills/frontend-vue/composables/useWebSocket.js +257 -0
- package/templates/.claude/skills/frontend-vue/composables/userObj.js +111 -0
- package/templates/.claude/skills/frontend-vue/composables/utils.js +322 -0
- package/templates/.claude/skills/frontend-vue/reference/composables-example.vue +320 -0
- package/templates/.claude/skills/frontend-vue/reference/form-example.vue +183 -0
- package/templates/.claude/skills/frontend-vue/reference/grid-example.vue +147 -0
- package/templates/.claude/skills/frontend-vue/reference/masterdata-example/[id].vue +106 -0
- package/templates/.claude/skills/frontend-vue/reference/masterdata-example/index.vue +58 -0
- package/templates/.claude/skills/frontend-vue/reference/popup-example.vue +159 -0
- package/templates/.claude/skills/pdf/LICENSE.txt +30 -0
- package/templates/.claude/skills/pdf/SKILL.md +294 -0
- package/templates/.claude/skills/pdf/forms.md +205 -0
- package/templates/.claude/skills/pdf/reference.md +612 -0
- package/templates/.claude/skills/pdf/scripts/check_bounding_boxes.py +70 -0
- package/templates/.claude/skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
- package/templates/.claude/skills/pdf/scripts/check_fillable_fields.py +12 -0
- package/templates/.claude/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/templates/.claude/skills/pdf/scripts/create_validation_image.py +41 -0
- package/templates/.claude/skills/pdf/scripts/extract_form_field_info.py +152 -0
- package/templates/.claude/skills/pdf/scripts/fill_fillable_fields.py +114 -0
- package/templates/.claude/skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/templates/.claude/skills/pdf-processing/SKILL.md +107 -0
- package/templates/.claude/skills/pdf-processing-pro/FORMS.md +610 -0
- package/templates/.claude/skills/pdf-processing-pro/OCR.md +137 -0
- package/templates/.claude/skills/pdf-processing-pro/SKILL.md +296 -0
- package/templates/.claude/skills/pdf-processing-pro/TABLES.md +626 -0
- package/templates/.claude/skills/pdf-processing-pro/scripts/analyze_form.py +307 -0
- package/templates/.claude/skills/postgres/SKILL.md +69 -0
- package/templates/.claude/skills/postgres/reference/fn_get_examples.sql +208 -0
- package/templates/.claude/skills/postgres/reference/fn_rpt_examples.sql +239 -0
- package/templates/.claude/skills/postgres/reference/utility_functions.sql +94 -0
- package/templates/.claude/skills/pptx/LICENSE.txt +30 -0
- package/templates/.claude/skills/pptx/SKILL.md +484 -0
- package/templates/.claude/skills/pptx/html2pptx.md +625 -0
- package/templates/.claude/skills/pptx/ooxml.md +427 -0
- package/templates/.claude/skills/pptx/scripts/html2pptx.js +979 -0
- package/templates/.claude/skills/pptx/scripts/inventory.py +1020 -0
- package/templates/.claude/skills/pptx/scripts/rearrange.py +231 -0
- package/templates/.claude/skills/pptx/scripts/replace.py +385 -0
- package/templates/.claude/skills/pptx/scripts/thumbnail.py +450 -0
- package/templates/.claude/skills/repo-maintenance/SKILL.md +97 -0
- package/templates/.claude/skills/research/EXAMPLES.md +434 -0
- package/templates/.claude/skills/research/REFERENCE.md +399 -0
- package/templates/.claude/skills/research/SKILL.md +136 -0
- package/templates/.claude/skills/root-cause-tracing/SKILL.md +174 -0
- package/templates/.claude/skills/root-cause-tracing/find-polluter.sh +63 -0
- package/templates/.claude/skills/sharing-skills/SKILL.md +194 -0
- package/templates/.claude/skills/sql-optimization-patterns/SKILL.md +493 -0
- package/templates/.claude/skills/subagent-driven-development/SKILL.md +189 -0
- package/templates/.claude/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/templates/.claude/skills/systematic-debugging/SKILL.md +295 -0
- package/templates/.claude/skills/systematic-debugging/test-academic.md +14 -0
- package/templates/.claude/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/templates/.claude/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/templates/.claude/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/templates/.claude/skills/test-driven-development/SKILL.md +364 -0
- package/templates/.claude/skills/testing-anti-patterns/SKILL.md +302 -0
- package/templates/.claude/skills/testing-quality/SKILL.md +97 -0
- package/templates/.claude/skills/verification-before-completion/SKILL.md +139 -0
- package/templates/.claude/skills/webapp-testing/LICENSE.txt +202 -0
- package/templates/.claude/skills/webapp-testing/SKILL.md +96 -0
- package/templates/.claude/skills/webapp-testing/examples/console_logging.py +35 -0
- package/templates/.claude/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/templates/.claude/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/templates/.claude/skills/webapp-testing/scripts/with_server.py +106 -0
- package/templates/.claude/ukit/index/build-index.mjs +28 -0
- package/templates/.claude/ukit/index/cache-utils.mjs +140 -0
- package/templates/.claude/ukit/index/lib/index-core.mjs +2800 -0
- package/templates/.claude/ukit/index/query-index.mjs +150 -0
- package/templates/.claude/ukit/index/refresh-index.mjs +57 -0
- package/templates/.claude/ukit/index/reset-auto-permissions.mjs +76 -0
- package/templates/.claude/ukit/index/resolve-context.mjs +279 -0
- package/templates/.claude/ukit/index/route-catalog.mjs +258 -0
- package/templates/.claude/ukit/index/route-task.mjs +1994 -0
- package/templates/.claude/ukit/index/triage.mjs +133 -0
- package/templates/.claude/ukit/index/verify-context.mjs +689 -0
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +1013 -0
- package/templates/.claude/ukit/runtime/output-compression.mjs +1340 -0
- package/templates/.claude/ukit/runtime/reinject-context.mjs +874 -0
- package/templates/.claude/ukit/runtime/token-utils.mjs +500 -0
- package/templates/.codex/README.md +83 -0
- package/templates/.codex/settings.json +187 -0
- package/templates/.gitignore +75 -0
- package/templates/AGENTS.md +116 -0
- package/templates/CLAUDE.md +93 -0
- package/templates/adapter-presets/antigravity/README.md +22 -0
- package/templates/adapter-presets/antigravity/rules.md +49 -0
- package/templates/adapter-presets/claude/settings.local.json +42 -0
- package/templates/adapter-presets/codex/settings.local.json +6 -0
- package/templates/adapter-presets/opencode/opencode.template.json +1 -0
- package/templates/docs/BUGFIX.md +20 -0
- package/templates/docs/BUG_INDEX.md +12 -0
- package/templates/docs/BUG_METRICS.md +7 -0
- package/templates/docs/BUG_TEMPLATE.md +13 -0
- package/templates/docs/CODE_MAP.md +35 -0
- package/templates/docs/INSTALL.md +113 -0
- package/templates/docs/MEMORY.md +49 -0
- package/templates/docs/PROJECT.md +50 -0
- package/templates/docs/UKIT_USAGE_GUIDE.md +147 -0
- package/templates/docs/WORKLOG.md +10 -0
- package/templates/ukit/README.md +14 -0
- package/templates/ukit/storage/cache/compact-history.json +3 -0
- package/templates/ukit/storage/cache/compact-pressure.json +1 -0
- package/templates/ukit/storage/cache/output-history.json +3 -0
- package/templates/ukit/storage/cache/prompt-cache.json +3 -0
- package/templates/ukit/storage/config.json +37 -0
- package/templates/ukit/storage/memory/projects/.gitkeep +2 -0
- package/templates/ukit/storage/memory/sessions/.gitkeep +0 -0
- package/templates/ukit/storage/memory/user.json +5 -0
|
@@ -0,0 +1,1506 @@
|
|
|
1
|
+
# DuraOne Frontend Reference
|
|
2
|
+
## Vue 3 / Nuxt 3 — Options API Style
|
|
3
|
+
|
|
4
|
+
> **NGUYÊN TẮC CỐT LÕI**: Tận dụng tối đa những gì đã có sẵn trong `components/` và `composables/`.
|
|
5
|
+
> KHÔNG tự tạo input thô — LUÔN dùng Control components. KHÔNG import composables — Nuxt auto-import.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## MỤC LỤC
|
|
10
|
+
1. [Cấu trúc thư mục FE](#1-cấu-trúc-thư-mục-fe)
|
|
11
|
+
2. [Cấu trúc Vue Component (Options API)](#2-cấu-trúc-vue-component)
|
|
12
|
+
3. [Naming Conventions](#3-naming-conventions)
|
|
13
|
+
4. [CONTROL COMPONENTS — Dùng Tối Đa](#4-control-components)
|
|
14
|
+
- [ControlInput](#controlinput)
|
|
15
|
+
- [ControlDropdownlist](#controldropdownlist)
|
|
16
|
+
- [ControlDatetime](#controldatetime)
|
|
17
|
+
- [ControlCheckbox](#controlcheckbox)
|
|
18
|
+
- [ControlCheckButton](#controlcheckbutton)
|
|
19
|
+
- [ControlTag](#controltag)
|
|
20
|
+
- [ControlButton](#controlbutton)
|
|
21
|
+
- [ControlButtonBar](#controlbuttonbar)
|
|
22
|
+
- [ControlButtonFloat](#controlbuttonfloat)
|
|
23
|
+
- [ControlLabel](#controllabel)
|
|
24
|
+
- [ControlBox](#controlbox)
|
|
25
|
+
- [ControlUpload](#controlupload)
|
|
26
|
+
- [ControlMasterBoxColumn](#controlmasterboxcolumn)
|
|
27
|
+
5. [POPUP COMPONENTS](#5-popup-components)
|
|
28
|
+
- [ControlPopupConfirm](#controlpopupconfirm)
|
|
29
|
+
- [ControlPopupConfirmReject](#controlpopupconfirmreject)
|
|
30
|
+
- [ControlPopupReject](#controlpopupreject)
|
|
31
|
+
- [ControlPopupInfo](#controlpopupinfo)
|
|
32
|
+
- [ControlPopupModalInfo](#controlpopupmodalinfo)
|
|
33
|
+
6. [GRID COMPONENTS](#6-grid-components)
|
|
34
|
+
- [GridAG](#gridag)
|
|
35
|
+
- [GridAntTable](#gridanttable)
|
|
36
|
+
- [GridHtmlTable](#gridhtmltable)
|
|
37
|
+
7. [PANEL COMPONENTS](#7-panel-components)
|
|
38
|
+
8. [COMPOSABLES — Dùng Trực Tiếp (Auto-Import)](#8-composables)
|
|
39
|
+
- [dayjs — DATETIME STANDARD](#dayjs--datetime-standard)
|
|
40
|
+
- [Dropdown Loading Pattern](#dropdown-loading-pattern)
|
|
41
|
+
9. [API Call Patterns](#9-api-call-patterns)
|
|
42
|
+
10. [Pattern: Trang Danh Sách](#10-pattern-trang-danh-sách)
|
|
43
|
+
11. [Pattern: Trang Form](#11-pattern-trang-form)
|
|
44
|
+
12. [Routing & Navigation](#12-routing--navigation)
|
|
45
|
+
13. [Validation Pattern](#13-validation-pattern)
|
|
46
|
+
14. [Đề Xuất Components Mới](#14-đề-xuất-components-mới)
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 1. Cấu Trúc Thư Mục FE
|
|
51
|
+
|
|
52
|
+
### 1.1. Cấu Trúc Gốc Nuxt Project
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
FE_DuraOne/
|
|
56
|
+
├── assets/ # Tài nguyên tĩnh (SCSS, fonts, images) — được Nuxt xử lý qua build pipeline
|
|
57
|
+
├── components/ # Tất cả Vue components — Nuxt auto-import, KHÔNG cần import thủ công
|
|
58
|
+
├── composables/ # Logic dùng chung — Nuxt auto-import, KHÔNG cần import thủ công
|
|
59
|
+
├── dist/ # Output build production (do Nuxt generate) — KHÔNG chỉnh sửa thủ công
|
|
60
|
+
├── docs/ # Tài liệu dự án, hướng dẫn, ghi chú kỹ thuật
|
|
61
|
+
├── layouts/ # Layout wrappers (default, admin, blank...) — bọc ngoài mỗi page
|
|
62
|
+
├── middleware/ # Route middleware (auth check, permission guard) — chạy trước khi vào page
|
|
63
|
+
├── node_modules/ # Dependencies (do yarn install) — KHÔNG chỉnh sửa, KHÔNG commit
|
|
64
|
+
├── pages/ # Các trang Vue — cấu trúc thư mục = routing tự động của Nuxt
|
|
65
|
+
├── plugins/ # Nuxt plugins (Ant Design Vue, ECharts, dayjs, v.v.) — chạy khi app khởi tạo
|
|
66
|
+
├── public/ # File tĩnh phục vụ trực tiếp (favicon, robots.txt) — KHÔNG qua build pipeline
|
|
67
|
+
│
|
|
68
|
+
├── .gitignore # Danh sách file/folder git bỏ qua
|
|
69
|
+
├── AGENTS.md # Hướng dẫn cho AI agents làm việc với project
|
|
70
|
+
├── api.http # File test API thủ công (dùng với REST Client extension)
|
|
71
|
+
├── app.vue # Root component của Nuxt app — entry point, chứa <NuxtLayout> + <NuxtPage>
|
|
72
|
+
├── CLAUDE.md # Hướng dẫn riêng cho Claude AI khi làm việc với project
|
|
73
|
+
├── nuxt.config.ts # Cấu hình Nuxt (modules, runtime config, CSS, vite options)
|
|
74
|
+
├── package.json # Dependencies + scripts (dev, build, generate)
|
|
75
|
+
├── tsconfig.json # Cấu hình TypeScript (Nuxt tự extend)
|
|
76
|
+
└── yarn.lock # Lock file dependencies — KHÔNG chỉnh sửa thủ công
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 1.2. Chi Tiết Bên Trong `components/` và `composables/`
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
components/
|
|
83
|
+
│ ├── Control/ # Form controls — LUÔN dùng thay vì raw input
|
|
84
|
+
│ │ ├── Box.vue
|
|
85
|
+
│ │ ├── Button.vue
|
|
86
|
+
│ │ ├── ButtonBar.vue
|
|
87
|
+
│ │ ├── ButtonFloat.vue
|
|
88
|
+
│ │ ├── CheckButton.vue
|
|
89
|
+
│ │ ├── Checkbox.vue
|
|
90
|
+
│ │ ├── Datetime.vue
|
|
91
|
+
│ │ ├── Dropdownlist.vue
|
|
92
|
+
│ │ ├── Input.vue
|
|
93
|
+
│ │ ├── Label.vue
|
|
94
|
+
│ │ ├── Tag.vue
|
|
95
|
+
│ │ ├── Upload.vue
|
|
96
|
+
│ │ ├── Master/BoxColumn.vue
|
|
97
|
+
│ │ └── Popup/
|
|
98
|
+
│ │ ├── Confirm.vue
|
|
99
|
+
│ │ ├── ConfirmReject.vue
|
|
100
|
+
│ │ ├── Info.vue
|
|
101
|
+
│ │ ├── ModalInfo.vue
|
|
102
|
+
│ │ └── Reject.vue
|
|
103
|
+
│ ├── ControlMobile/ # Mobile variants
|
|
104
|
+
│ │ ├── PagingBar.vue
|
|
105
|
+
│ │ └── UploadImage.vue
|
|
106
|
+
│ ├── Grid/
|
|
107
|
+
│ │ ├── AG.vue # Main grid — dùng cho mọi danh sách
|
|
108
|
+
│ │ ├── AntTable.vue
|
|
109
|
+
│ │ └── HtmlTable.vue
|
|
110
|
+
│ ├── Panel/
|
|
111
|
+
│ │ └── FormView.vue # Auto CRUD form
|
|
112
|
+
│ ├── Partial/ # Layout partials (Header, Sidebar, Footer)
|
|
113
|
+
│ └── PDFViewer.vue
|
|
114
|
+
|
|
115
|
+
composables/ # Auto-imported bởi Nuxt — KHÔNG import thủ công
|
|
116
|
+
│ ├── masterApi.js # Domain API functions (get data cho nhiều trang)
|
|
117
|
+
│ ├── useRequest.js # HTTP wrapper + GZIP compression — MỌI API call phải qua đây
|
|
118
|
+
│ ├── useSession.js # localStorage session management
|
|
119
|
+
│ ├── useTranslation.js # i18n — hàm t() để đa ngôn ngữ
|
|
120
|
+
│ ├── useWebSocket.js # WebSocket connection
|
|
121
|
+
│ ├── state.js # Global reactive state (user, loading, language...)
|
|
122
|
+
│ ├── utils.js # Utility functions (format, convert, validate dùng chung)
|
|
123
|
+
│ ├── userObj.js # User-related helpers
|
|
124
|
+
│ └── indexDBStore.js # IndexedDB storage
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 2. Cấu Trúc Vue Component
|
|
130
|
+
|
|
131
|
+
**⚠️ LUÔN dùng Options API. KHÔNG dùng `<script setup>`. KHÔNG import composables.**
|
|
132
|
+
|
|
133
|
+
```vue
|
|
134
|
+
<template>
|
|
135
|
+
<!-- HTML -->
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<script>
|
|
139
|
+
// ⚠️ KHÔNG import — Nuxt 3 auto-import tất cả từ /composables/
|
|
140
|
+
// state, get_schema, request, formatDate, show_message, t(), get_*... đều sẵn dùng
|
|
141
|
+
|
|
142
|
+
export default {
|
|
143
|
+
props: {
|
|
144
|
+
propName: { type: String, default: "" },
|
|
145
|
+
},
|
|
146
|
+
data() {
|
|
147
|
+
return {
|
|
148
|
+
workingObj: {},
|
|
149
|
+
rows: [],
|
|
150
|
+
// dropdowns
|
|
151
|
+
branchList: [],
|
|
152
|
+
// ui flags
|
|
153
|
+
is_saving: false,
|
|
154
|
+
show_confirm_delete: false,
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
computed: {
|
|
158
|
+
is_locked() {
|
|
159
|
+
return !this.workingObj.can_edit;
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
watch: {
|
|
163
|
+
"workingObj.id_branch"(newVal) {
|
|
164
|
+
this.on_branch_change(newVal);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
async created() {
|
|
168
|
+
if (!authorize_menu(this.$route.fullPath)) {
|
|
169
|
+
this.$router.push(state.homepage);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
state.page_header = t("Page Title");
|
|
173
|
+
await this.init();
|
|
174
|
+
},
|
|
175
|
+
methods: {
|
|
176
|
+
async init() {
|
|
177
|
+
await Promise.all([this.get_data(), this.load_dropdowns()]);
|
|
178
|
+
},
|
|
179
|
+
async get_data() { /* ... */ },
|
|
180
|
+
async load_dropdowns() { /* ... */ },
|
|
181
|
+
async save() { /* ... */ },
|
|
182
|
+
validate() { /* ... */ },
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<style scoped></style>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 3. Naming Conventions
|
|
193
|
+
|
|
194
|
+
| Loại | Convention | Ví dụ |
|
|
195
|
+
|------|-----------|-------|
|
|
196
|
+
| Page files | PascalCase | `AgreementList.vue`, `UserForm.vue` |
|
|
197
|
+
| Page folders | PascalCase | `Agreement/`, `Master/Users/` |
|
|
198
|
+
| Component files | PascalCase | `ControlInput.vue`, `GridAG.vue` |
|
|
199
|
+
| Composables | camelCase | `masterApi.js`, `useRequest.js` |
|
|
200
|
+
| JS variables | camelCase | `workingObj`, `branchList` |
|
|
201
|
+
| DB field keys | snake_case | `id_agreement`, `created_at` |
|
|
202
|
+
| ID fields | `id_` prefix | `id_agreement`, `id_user` |
|
|
203
|
+
| Boolean fields | `can_`/`is_`/`has_`/`show_` | `can_edit`, `is_locked`, `show_confirm` |
|
|
204
|
+
| Data methods | `get_` / `load_` | `get_data()`, `load_dropdowns()` |
|
|
205
|
+
| Action methods | verb | `save()`, `approve()`, `delete_record()` |
|
|
206
|
+
| Event handlers | `on_` | `on_row_click()`, `on_branch_change()` |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 4. CONTROL COMPONENTS
|
|
211
|
+
|
|
212
|
+
> **KHÔNG tự tạo `<input>`, `<select>`, `<button>` raw** — luôn dùng những component dưới đây.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### ControlInput
|
|
217
|
+
|
|
218
|
+
Text, number, password, textarea — tất cả đều qua component này.
|
|
219
|
+
|
|
220
|
+
```vue
|
|
221
|
+
<!-- Text -->
|
|
222
|
+
<ControlInput
|
|
223
|
+
v-model="workingObj.agreement_number"
|
|
224
|
+
name="Agreement Number"
|
|
225
|
+
type="text"
|
|
226
|
+
:required="true"
|
|
227
|
+
:disabled="is_locked"
|
|
228
|
+
placeholder="Enter number..."
|
|
229
|
+
/>
|
|
230
|
+
|
|
231
|
+
<!-- Textarea -->
|
|
232
|
+
<ControlInput
|
|
233
|
+
v-model="workingObj.note"
|
|
234
|
+
name="Note"
|
|
235
|
+
type="textarea"
|
|
236
|
+
:rows="4"
|
|
237
|
+
:disabled="is_locked"
|
|
238
|
+
/>
|
|
239
|
+
|
|
240
|
+
<!-- Number -->
|
|
241
|
+
<ControlInput
|
|
242
|
+
v-model="workingObj.amount"
|
|
243
|
+
name="Amount"
|
|
244
|
+
type="number"
|
|
245
|
+
:required="true"
|
|
246
|
+
:disabled="is_locked"
|
|
247
|
+
/>
|
|
248
|
+
|
|
249
|
+
<!-- Password -->
|
|
250
|
+
<ControlInput
|
|
251
|
+
v-model="form.password"
|
|
252
|
+
name="Password"
|
|
253
|
+
type="password"
|
|
254
|
+
:required="true"
|
|
255
|
+
/>
|
|
256
|
+
|
|
257
|
+
<!-- Search (emit onSearch khi nhấn Enter/button) -->
|
|
258
|
+
<ControlInput
|
|
259
|
+
v-model="keyword"
|
|
260
|
+
name=""
|
|
261
|
+
type="search"
|
|
262
|
+
placeholder="Search..."
|
|
263
|
+
@onSearch="get_data"
|
|
264
|
+
/>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Props:** `modelValue`, `name` (label), `type` (text|number|password|textarea|search), `placeholder`, `required`, `disabled`, `rows` (cho textarea), `classes`, `label_classes`, `id`
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### ControlDropdownlist
|
|
272
|
+
|
|
273
|
+
Select đơn hoặc multiple — có search, có hỗ trợ show_key.
|
|
274
|
+
|
|
275
|
+
```vue
|
|
276
|
+
<!-- Dropdown đơn -->
|
|
277
|
+
<ControlDropdownlist
|
|
278
|
+
v-model="workingObj.id_branch"
|
|
279
|
+
name="Branch"
|
|
280
|
+
:list="branchList"
|
|
281
|
+
:required="true"
|
|
282
|
+
:disabled="is_locked"
|
|
283
|
+
placeholder="Select branch..."
|
|
284
|
+
/>
|
|
285
|
+
|
|
286
|
+
<!-- Multiple select -->
|
|
287
|
+
<ControlDropdownlist
|
|
288
|
+
v-model="workingObj.id_tags"
|
|
289
|
+
name="Tags"
|
|
290
|
+
:list="tagList"
|
|
291
|
+
:multiple="true"
|
|
292
|
+
:disabled="is_locked"
|
|
293
|
+
/>
|
|
294
|
+
|
|
295
|
+
<!-- Hiển thị value kèm label -->
|
|
296
|
+
<ControlDropdownlist
|
|
297
|
+
v-model="workingObj.status"
|
|
298
|
+
name="Status"
|
|
299
|
+
:list="statusList"
|
|
300
|
+
:show_key="true"
|
|
301
|
+
/>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Props:** `modelValue`, `name`, `list` (Array `[{value, label}]`), `required`, `disabled`, `multiple`, `placeholder`, `show_key`, `icon`, `classes`, `id`
|
|
305
|
+
|
|
306
|
+
**Lưu ý:** `list` phải có format `{ value: ..., label: ... }` — không đổi key.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
### ControlDatetime
|
|
311
|
+
|
|
312
|
+
Date picker, month picker, hoặc year picker.
|
|
313
|
+
|
|
314
|
+
```vue
|
|
315
|
+
<!-- Date -->
|
|
316
|
+
<ControlDatetime
|
|
317
|
+
v-model="workingObj.start_date"
|
|
318
|
+
name="Start Date"
|
|
319
|
+
type="date"
|
|
320
|
+
:required="true"
|
|
321
|
+
:disabled="is_locked"
|
|
322
|
+
:maxDate="workingObj.end_date"
|
|
323
|
+
/>
|
|
324
|
+
|
|
325
|
+
<!-- Date + Time -->
|
|
326
|
+
<ControlDatetime
|
|
327
|
+
v-model="workingObj.appointment_time"
|
|
328
|
+
name="Appointment"
|
|
329
|
+
type="date"
|
|
330
|
+
:show_time="true"
|
|
331
|
+
:required="true"
|
|
332
|
+
/>
|
|
333
|
+
|
|
334
|
+
<!-- Month picker -->
|
|
335
|
+
<ControlDatetime
|
|
336
|
+
v-model="filter_month"
|
|
337
|
+
name="Month"
|
|
338
|
+
type="month"
|
|
339
|
+
/>
|
|
340
|
+
|
|
341
|
+
<!-- Year picker -->
|
|
342
|
+
<ControlDatetime
|
|
343
|
+
v-model="filter_year"
|
|
344
|
+
name="Year"
|
|
345
|
+
type="year"
|
|
346
|
+
/>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Props:** `modelValue`, `name`, `type` (date|month|year), `show_time`, `required`, `disabled`, `minDate`, `maxDate`, `classes`, `id`
|
|
350
|
+
|
|
351
|
+
**Emit:** `update:modelValue` với format `YYYY-MM-DD`
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
### ControlCheckbox
|
|
356
|
+
|
|
357
|
+
Boolean checkbox với label.
|
|
358
|
+
|
|
359
|
+
```vue
|
|
360
|
+
<ControlCheckbox
|
|
361
|
+
v-model="workingObj.is_active"
|
|
362
|
+
label="Active"
|
|
363
|
+
:disabled="is_locked"
|
|
364
|
+
/>
|
|
365
|
+
|
|
366
|
+
<ControlCheckbox
|
|
367
|
+
v-model="workingObj.has_vat"
|
|
368
|
+
label="Include VAT"
|
|
369
|
+
/>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Props:** `modelValue`, `label`, `disabled`
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### ControlCheckButton
|
|
377
|
+
|
|
378
|
+
Checkbox styled như button — dùng khi cần toggle nổi bật hơn.
|
|
379
|
+
|
|
380
|
+
```vue
|
|
381
|
+
<ControlCheckButton
|
|
382
|
+
v-model="filter_is_urgent"
|
|
383
|
+
label="Urgent Only"
|
|
384
|
+
/>
|
|
385
|
+
|
|
386
|
+
<ControlCheckButton
|
|
387
|
+
v-model="workingObj.is_adhoc"
|
|
388
|
+
label="Ad-hoc"
|
|
389
|
+
:disabled="is_locked"
|
|
390
|
+
/>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Props:** `modelValue`, `label`, `disabled`, `id`
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
### ControlTag
|
|
398
|
+
|
|
399
|
+
Multi-tag input — nhập nhiều giá trị tự do.
|
|
400
|
+
|
|
401
|
+
```vue
|
|
402
|
+
<ControlTag
|
|
403
|
+
v-model="workingObj.keywords"
|
|
404
|
+
name="Keywords"
|
|
405
|
+
placeholder="Add keyword, press Enter..."
|
|
406
|
+
:disabled="is_locked"
|
|
407
|
+
/>
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Props:** `modelValue` (Array), `name`, `placeholder`, `required`, `disabled`, `classes`, `label_classes`, `id`
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### ControlButton
|
|
415
|
+
|
|
416
|
+
Button chính — có loading state tự động (1 giây sau click).
|
|
417
|
+
|
|
418
|
+
```vue
|
|
419
|
+
<!-- Primary -->
|
|
420
|
+
<ControlButton name="Save" type="primary" @onClick="save" />
|
|
421
|
+
|
|
422
|
+
<!-- Danger -->
|
|
423
|
+
<ControlButton name="Delete" type="danger" @onClick="show_confirm_delete = true" />
|
|
424
|
+
|
|
425
|
+
<!-- Secondary -->
|
|
426
|
+
<ControlButton name="Back" type="secondary" @onClick="go_back" />
|
|
427
|
+
|
|
428
|
+
<!-- Success -->
|
|
429
|
+
<ControlButton name="Approve" type="success" @onClick="approve" />
|
|
430
|
+
|
|
431
|
+
<!-- Custom color -->
|
|
432
|
+
<ControlButton name="Export" color="#6c5ce7" @onClick="export_data" />
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Props:** `name` (label), `type` (primary|secondary|danger|success|warning), `color` (custom hex), `classes`
|
|
436
|
+
|
|
437
|
+
**Emit:** `onClick`
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
### ControlButtonBar
|
|
442
|
+
|
|
443
|
+
Group nhiều button liền nhau.
|
|
444
|
+
|
|
445
|
+
```vue
|
|
446
|
+
<ControlButtonBar
|
|
447
|
+
:button_list="[
|
|
448
|
+
{ name: 'Close', type: 'secondary' },
|
|
449
|
+
{ name: 'Delete', type: 'danger' },
|
|
450
|
+
{ name: 'Save', type: 'success' },
|
|
451
|
+
]"
|
|
452
|
+
@clickClose="close"
|
|
453
|
+
@clickDelete="show_confirm_delete = true"
|
|
454
|
+
@clickSave="save"
|
|
455
|
+
/>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Props:** `button_list` (Array `[{name, type}]`)
|
|
459
|
+
|
|
460
|
+
**Emit:** `click{ButtonName}` — dynamic theo tên button (e.g. `clickSave`, `clickDelete`)
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
### ControlButtonFloat
|
|
465
|
+
|
|
466
|
+
Floating action button cố định góc dưới phải — dùng cho trang danh sách để "Add New".
|
|
467
|
+
|
|
468
|
+
```vue
|
|
469
|
+
<ControlButtonFloat name="Add New" @clickButtonFloat="go_to_form" />
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**Props:** `name`
|
|
473
|
+
|
|
474
|
+
**Emit:** `clickButtonFloat`
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
### ControlLabel
|
|
479
|
+
|
|
480
|
+
Label riêng — dùng khi cần label mà không kèm input.
|
|
481
|
+
|
|
482
|
+
```vue
|
|
483
|
+
<ControlLabel name="Section Title" :required="false" />
|
|
484
|
+
<ControlLabel name="Email" icon="fas fa-envelope" :required="true" />
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Props:** `name`, `required`, `icon` (FontAwesome class), `classes`
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
### ControlBox
|
|
492
|
+
|
|
493
|
+
Container có header + màu sắc theo loại — dùng để group fields.
|
|
494
|
+
|
|
495
|
+
```vue
|
|
496
|
+
<!-- Info box -->
|
|
497
|
+
<ControlBox title="Agreement Info" type="info" :col="12">
|
|
498
|
+
<div class="row">
|
|
499
|
+
<div class="col-md-6">
|
|
500
|
+
<ControlInput v-model="workingObj.agreement_number" name="Number" />
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</ControlBox>
|
|
504
|
+
|
|
505
|
+
<!-- Success status box -->
|
|
506
|
+
<ControlBox title="Approval" type="success" :col="6">
|
|
507
|
+
<ControlLabel :name="workingObj.approved_by" />
|
|
508
|
+
</ControlBox>
|
|
509
|
+
|
|
510
|
+
<!-- Warning box -->
|
|
511
|
+
<ControlBox title="Attention" type="warning" :col="12">
|
|
512
|
+
<p>Please review before submitting.</p>
|
|
513
|
+
</ControlBox>
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**Props:** `title`, `type` (info|success|danger|warning), `col` (3|4|6|12), `show_icon`
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
### ControlUpload
|
|
521
|
+
|
|
522
|
+
File upload với drag-and-drop, tối đa 3 file, 10MB/file.
|
|
523
|
+
|
|
524
|
+
```vue
|
|
525
|
+
<ControlUpload
|
|
526
|
+
v-model="fileList"
|
|
527
|
+
label="Click or drag file to upload"
|
|
528
|
+
hint="Max 3 files, 10MB each"
|
|
529
|
+
@uploadFinish="on_upload_finish"
|
|
530
|
+
/>
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
```javascript
|
|
534
|
+
methods: {
|
|
535
|
+
on_upload_finish(file) {
|
|
536
|
+
this.workingObj.file_url = file.url || file.name;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Props:** `modelValue` (Array), `label`, `hint`, `required`
|
|
542
|
+
|
|
543
|
+
**Emit:** `uploadFinish(file)`
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
### ControlMasterBoxColumn
|
|
548
|
+
|
|
549
|
+
Layout wrapper tự điều chỉnh col width theo số item — dùng trong PanelFormView.
|
|
550
|
+
|
|
551
|
+
```vue
|
|
552
|
+
<!-- item.position.length = 1 → col-md-12 -->
|
|
553
|
+
<!-- item.position.length = 2 → col-md-6 -->
|
|
554
|
+
<!-- item.position.length = 3 → col-md-4 -->
|
|
555
|
+
<!-- item.position.length = 4 → col-md-3 -->
|
|
556
|
+
<ControlMasterBoxColumn :item="field">
|
|
557
|
+
<ControlInput v-model="obj[field.key]" :name="field.title" />
|
|
558
|
+
</ControlMasterBoxColumn>
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## 5. POPUP COMPONENTS
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
### ControlPopupConfirm
|
|
568
|
+
|
|
569
|
+
Confirm dialog đơn giản — Yes/No.
|
|
570
|
+
|
|
571
|
+
```vue
|
|
572
|
+
<ControlPopupConfirm
|
|
573
|
+
:visible="show_confirm_delete"
|
|
574
|
+
title="Confirm Delete"
|
|
575
|
+
message="Are you sure you want to delete this record?"
|
|
576
|
+
@onConfirm="on_confirm_delete"
|
|
577
|
+
@onClose="show_confirm_delete = false"
|
|
578
|
+
/>
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
```javascript
|
|
582
|
+
data() {
|
|
583
|
+
return { show_confirm_delete: false };
|
|
584
|
+
},
|
|
585
|
+
methods: {
|
|
586
|
+
async on_confirm_delete() {
|
|
587
|
+
this.show_confirm_delete = false;
|
|
588
|
+
await request("/delete", { table: "agreement", id_agreement: this.workingObj.id_agreement });
|
|
589
|
+
show_message("success", t("Deleted successfully"));
|
|
590
|
+
this.$router.push("/Agreement/AgreementList");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Props:** `visible`, `title`, `message` (HTML supported)
|
|
596
|
+
|
|
597
|
+
**Emits:** `onConfirm`, `onClose`
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
### ControlPopupConfirmReject
|
|
602
|
+
|
|
603
|
+
Popup confirm có checkbox Adhoc/OnPlan + textarea lý do — dùng cho approval flow.
|
|
604
|
+
|
|
605
|
+
```vue
|
|
606
|
+
<ControlPopupConfirmReject
|
|
607
|
+
:visible="show_confirm_approve"
|
|
608
|
+
title="Classify Agreement"
|
|
609
|
+
message="Please classify this agreement type"
|
|
610
|
+
@onConfirm="on_confirm_approve"
|
|
611
|
+
@onClose="show_confirm_approve = false"
|
|
612
|
+
/>
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
```javascript
|
|
616
|
+
// onConfirm nhận (reason, is_adhoc, is_onplan)
|
|
617
|
+
async on_confirm_approve(reason, is_adhoc, is_onplan) {
|
|
618
|
+
await request("/save", {
|
|
619
|
+
table: "agreement",
|
|
620
|
+
key_array: ["id_agreement"],
|
|
621
|
+
id_agreement: this.workingObj.id_agreement,
|
|
622
|
+
status: "approved",
|
|
623
|
+
approve_reason: reason,
|
|
624
|
+
is_adhoc,
|
|
625
|
+
is_onplan,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**Props:** `visible`, `title`, `message`
|
|
631
|
+
|
|
632
|
+
**Emits:** `onConfirm(reason, is_adhoc, is_onplan)`, `onClose`
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
### ControlPopupReject
|
|
637
|
+
|
|
638
|
+
Popup từ chối có Adhoc/OnPlan checkbox + lý do — dùng cho rejection flow.
|
|
639
|
+
|
|
640
|
+
```vue
|
|
641
|
+
<ControlPopupReject
|
|
642
|
+
:visible="show_reject"
|
|
643
|
+
title="Reject Agreement"
|
|
644
|
+
message="Please provide rejection reason"
|
|
645
|
+
@onConfirm="on_reject"
|
|
646
|
+
@onClose="show_reject = false"
|
|
647
|
+
/>
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
```javascript
|
|
651
|
+
async on_reject(reason, is_adhoc, is_onplan) {
|
|
652
|
+
await request("/save", {
|
|
653
|
+
table: "agreement",
|
|
654
|
+
key_array: ["id_agreement"],
|
|
655
|
+
id_agreement: this.workingObj.id_agreement,
|
|
656
|
+
status: "rejected",
|
|
657
|
+
reject_reason: reason,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
**Props:** `visible`, `title`, `message`
|
|
663
|
+
|
|
664
|
+
**Emits:** `onConfirm(reason, is_adhoc, is_onplan)`, `onClose`
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
### ControlPopupInfo
|
|
669
|
+
|
|
670
|
+
Info modal hiển thị kết quả thao tác.
|
|
671
|
+
|
|
672
|
+
```vue
|
|
673
|
+
<ControlPopupInfo
|
|
674
|
+
:visible="show_result"
|
|
675
|
+
status="success"
|
|
676
|
+
title="Saved Successfully"
|
|
677
|
+
/>
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**Props:** `visible`, `status` (success|error|warning|info), `title`
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
### ControlPopupModalInfo
|
|
685
|
+
|
|
686
|
+
Modal chứa GridAG — hiển thị danh sách chi tiết trong popup.
|
|
687
|
+
|
|
688
|
+
```vue
|
|
689
|
+
<ControlPopupModalInfo
|
|
690
|
+
:visible="show_detail_popup"
|
|
691
|
+
title="Transaction History"
|
|
692
|
+
:rows="transaction_rows"
|
|
693
|
+
:columns="transaction_columns"
|
|
694
|
+
/>
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
**Props:** `visible`, `title`, `rows`, `columns`
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## 6. GRID COMPONENTS
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
### GridAG
|
|
706
|
+
|
|
707
|
+
**Component chính cho mọi danh sách dữ liệu** — AG Grid với đầy đủ tính năng.
|
|
708
|
+
|
|
709
|
+
```vue
|
|
710
|
+
<GridAG
|
|
711
|
+
:rows="rows"
|
|
712
|
+
:columns="columns"
|
|
713
|
+
:can_add="true"
|
|
714
|
+
:can_delete="false"
|
|
715
|
+
:can_export="true"
|
|
716
|
+
:can_search="true"
|
|
717
|
+
:can_refresh="true"
|
|
718
|
+
height="500px"
|
|
719
|
+
@onAdd="go_to_new_form"
|
|
720
|
+
@onClickDetail="go_to_edit_form"
|
|
721
|
+
@reloadData="get_data"
|
|
722
|
+
/>
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
**Column Definition:**
|
|
726
|
+
```javascript
|
|
727
|
+
columns: [
|
|
728
|
+
// Text
|
|
729
|
+
{ key: "request_no", title: t("Request No"), type: "text", width: 150 },
|
|
730
|
+
// Number (auto format)
|
|
731
|
+
{ key: "amount", title: t("Amount"), type: "number", width: 120 },
|
|
732
|
+
// Date (auto format DD/MM/YYYY)
|
|
733
|
+
{ key: "created_at", title: t("Date"), type: "date", width: 110 },
|
|
734
|
+
// Date + Time
|
|
735
|
+
{ key: "updated_at", title: t("Updated"), type: "datetime", width: 140 },
|
|
736
|
+
// HTML render
|
|
737
|
+
{ key: "status_html", title: t("Status"), type: "html", width: 100 },
|
|
738
|
+
// Checkbox (read-only)
|
|
739
|
+
{ key: "is_active", title: t("Active"), type: "checkbox", width: 80 },
|
|
740
|
+
// Editable dropdown
|
|
741
|
+
{
|
|
742
|
+
key: "status",
|
|
743
|
+
title: t("Status"),
|
|
744
|
+
type: "dropdown",
|
|
745
|
+
can_edit: true,
|
|
746
|
+
dropdown_list: [
|
|
747
|
+
{ value: "draft", label: "Draft" },
|
|
748
|
+
{ value: "approved", label: "Approved" },
|
|
749
|
+
],
|
|
750
|
+
width: 120,
|
|
751
|
+
},
|
|
752
|
+
// Sum aggregation
|
|
753
|
+
{ key: "amount", title: t("Amount"), type: "number", aggFunc: "sum", width: 120 },
|
|
754
|
+
],
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**Props đầy đủ:**
|
|
758
|
+
- `rows` (Array) — data
|
|
759
|
+
- `columns` (Array) — column defs
|
|
760
|
+
- `can_add` (Boolean, default: true)
|
|
761
|
+
- `can_search` (Boolean, default: true)
|
|
762
|
+
- `can_edit` (Boolean, default: true) — hiện nút Edit/Detail
|
|
763
|
+
- `can_delete` (Boolean, default: false)
|
|
764
|
+
- `can_export` (Boolean, default: true)
|
|
765
|
+
- `can_refresh` (Boolean, default: true)
|
|
766
|
+
- `can_sync` (Boolean, default: false) — sync từ Excel
|
|
767
|
+
- `can_upload` (Boolean, default: false)
|
|
768
|
+
- `have_checkbox` (Boolean, default: false)
|
|
769
|
+
- `show_total` (Boolean, default: false) — pinned bottom row
|
|
770
|
+
- `height` (String, default: "600px")
|
|
771
|
+
- `is_fix_height` (Boolean, default: false)
|
|
772
|
+
- `is_loading` (Boolean, default: false)
|
|
773
|
+
- `table` (String) — table name cho sync
|
|
774
|
+
- `table_schema` (String)
|
|
775
|
+
- `detail_button_list` (Array) — extra per-row buttons
|
|
776
|
+
- `extra_button` (Array) — extra toolbar buttons
|
|
777
|
+
|
|
778
|
+
**Emits:**
|
|
779
|
+
- `onAdd` — Add button
|
|
780
|
+
- `onClickDetail(row)` — Edit/Detail button
|
|
781
|
+
- `onExtraDetailBtnClick(key, row)` — extra row button
|
|
782
|
+
- `onCellValueChanged(event)` — inline edit
|
|
783
|
+
- `reloadData()` — refresh
|
|
784
|
+
- `getSelectedRows(key, rows)` — checkbox selection
|
|
785
|
+
- `onDeleteRowIndex(index)` — delete row
|
|
786
|
+
- `onExtraBtnClick(key)` — extra toolbar button
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
### GridAntTable
|
|
791
|
+
|
|
792
|
+
Table đơn giản hơn GridAG — dùng khi cần pagination nhẹ.
|
|
793
|
+
|
|
794
|
+
```vue
|
|
795
|
+
<GridAntTable
|
|
796
|
+
:rows="rows"
|
|
797
|
+
:columns="columns"
|
|
798
|
+
:is_checkbox="false"
|
|
799
|
+
:is_edit="true"
|
|
800
|
+
:is_export="true"
|
|
801
|
+
@onClickDetail="on_click_detail"
|
|
802
|
+
/>
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
**Props:** `rows`, `columns` (`[{title, dataIndex}]`), `is_checkbox`, `is_edit`, `is_export`
|
|
806
|
+
|
|
807
|
+
**Emits:** `onClickDetail(row)`, `onClickEdit(row)`
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
### GridHtmlTable
|
|
812
|
+
|
|
813
|
+
Table HTML thuần — dùng cho print hoặc display đơn giản nhất.
|
|
814
|
+
|
|
815
|
+
```vue
|
|
816
|
+
<GridHtmlTable
|
|
817
|
+
:rows="rows"
|
|
818
|
+
:columns="[
|
|
819
|
+
{ caption: 'Name', field: 'full_name' },
|
|
820
|
+
{ caption: 'Amount', field: 'amount' },
|
|
821
|
+
]"
|
|
822
|
+
/>
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
## 7. PANEL COMPONENTS
|
|
828
|
+
|
|
829
|
+
### PanelFormView
|
|
830
|
+
|
|
831
|
+
Form CRUD tự động — dùng cho Master Data (không cần business logic phức tạp).
|
|
832
|
+
|
|
833
|
+
```vue
|
|
834
|
+
<PanelFormView
|
|
835
|
+
:workingObj="currentRecord"
|
|
836
|
+
:rows="allRows"
|
|
837
|
+
:columns="formColumns"
|
|
838
|
+
table="branch"
|
|
839
|
+
table_schema="qas"
|
|
840
|
+
:allow_edit="true"
|
|
841
|
+
@reloadData="get_data"
|
|
842
|
+
/>
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**Column definition cho FormView:**
|
|
846
|
+
```javascript
|
|
847
|
+
formColumns: [
|
|
848
|
+
{
|
|
849
|
+
key: "id_branch",
|
|
850
|
+
title: "Branch ID",
|
|
851
|
+
type: "auto_generate",
|
|
852
|
+
primary: true, // PK — disable khi update
|
|
853
|
+
show_form: false, // ẩn khỏi form
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
key: "branch_name",
|
|
857
|
+
title: "Branch Name",
|
|
858
|
+
type: "input",
|
|
859
|
+
required: true,
|
|
860
|
+
show_form: true,
|
|
861
|
+
position: ["col1"], // position.length = 1 → col-12
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
key: "id_region",
|
|
865
|
+
title: "Region",
|
|
866
|
+
type: "dropdown",
|
|
867
|
+
required: true,
|
|
868
|
+
show_form: true,
|
|
869
|
+
position: ["col1", "col2"], // length = 2 → col-6
|
|
870
|
+
dropdown_info: {
|
|
871
|
+
schema: "qas",
|
|
872
|
+
table: "region",
|
|
873
|
+
field: { label: ["region_name"], value: ["id_region"] },
|
|
874
|
+
},
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
key: "id_country",
|
|
878
|
+
title: "Country",
|
|
879
|
+
type: "dropdown",
|
|
880
|
+
show_form: true,
|
|
881
|
+
position: ["col1", "col2"],
|
|
882
|
+
dropdown_info: {
|
|
883
|
+
schema: "qas",
|
|
884
|
+
table: "country",
|
|
885
|
+
field: { label: ["country_name"], value: ["id_country"] },
|
|
886
|
+
conditions: { id_region: "id_region" }, // dependent dropdown
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
],
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
---
|
|
893
|
+
|
|
894
|
+
## 8. COMPOSABLES
|
|
895
|
+
|
|
896
|
+
> ⚠️ **KHÔNG import** — Nuxt 3 auto-import tất cả từ `/composables/`. Dùng trực tiếp.
|
|
897
|
+
|
|
898
|
+
### useRequest.js
|
|
899
|
+
|
|
900
|
+
> 🚫🚫🚫 **CẤM TUYỆT ĐỐI GỌI API RAW** 🚫🚫🚫
|
|
901
|
+
>
|
|
902
|
+
> **KHÔNG BAO GIỜ** dùng `fetch()`, `axios()`, `axios.get()`, `axios.post()`, `axios.put()`, `axios.delete()` hay bất kỳ HTTP client nào khác.
|
|
903
|
+
>
|
|
904
|
+
> MỌI request đến server **BẮT BUỘC** phải đi qua 3 hàm duy nhất bên dưới (đã auto-import từ `composables/useRequest.js`):
|
|
905
|
+
> - `request(url, data, method)` — API chuẩn, có GZIP + loading state
|
|
906
|
+
> - `requestForm(url, formData)` — Upload file (multipart/form-data)
|
|
907
|
+
> - `request_origin(url, data)` — Custom endpoint (không qua `/api/select`)
|
|
908
|
+
>
|
|
909
|
+
> **Nếu code sinh ra chứa `fetch(`, `axios(`, `axios.get(`, `axios.post(` → CODE SAI, PHẢI SỬA LẠI NGAY.**
|
|
910
|
+
> Không có ngoại lệ — kể cả "chỉ test nhanh", "gọi API bên ngoài", hay "chỉ 1 lần".
|
|
911
|
+
|
|
912
|
+
```javascript
|
|
913
|
+
// GET data (qua generic /api/select)
|
|
914
|
+
const rows = await request("/select", {
|
|
915
|
+
schema: get_schema(),
|
|
916
|
+
table: "v_agreement",
|
|
917
|
+
conditions: JSON.stringify({ id_branch: 5, status: "active" }),
|
|
918
|
+
order_by: ["created_at desc"],
|
|
919
|
+
columns: "*", // hoặc ["col1", "col2"]
|
|
920
|
+
}, "get");
|
|
921
|
+
|
|
922
|
+
// POST save (insert id='' hoặc update id=uuid)
|
|
923
|
+
await request("/save", {
|
|
924
|
+
table: "agreement",
|
|
925
|
+
key_array: ["id_agreement"],
|
|
926
|
+
id_agreement: "", // '' = insert, uuid = update
|
|
927
|
+
agreement_number: "AGR-001",
|
|
928
|
+
amount: 50000,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// Custom endpoint
|
|
932
|
+
await request_origin("/api/agreement/approve", {
|
|
933
|
+
id_agreement: "uuid-here",
|
|
934
|
+
schema: get_schema(),
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
// Upload file
|
|
938
|
+
const formData = new FormData();
|
|
939
|
+
formData.append("file", fileObj);
|
|
940
|
+
formData.append("folder", "agreements");
|
|
941
|
+
await requestForm("/api/upload_to_cloud", formData);
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### Dropdown Loading Pattern
|
|
945
|
+
|
|
946
|
+
Dùng pattern này để load dropdown list. Gọi trong `load_dropdowns()` hoặc `init()`.
|
|
947
|
+
|
|
948
|
+
```javascript
|
|
949
|
+
// Pattern chuẩn:
|
|
950
|
+
// 1. Dùng separate `let data = {}` object (không inline)
|
|
951
|
+
// 2. Check resp.length > 0 trước khi gán
|
|
952
|
+
// 3. Dùng convertToDropdownValue() để chuyển sang {value, label}
|
|
953
|
+
|
|
954
|
+
async getAreaList() {
|
|
955
|
+
let data = {
|
|
956
|
+
schema: get_schema(),
|
|
957
|
+
table: "mkt_stock_area",
|
|
958
|
+
};
|
|
959
|
+
let resp = await request("/select", data, "get");
|
|
960
|
+
if (resp.length > 0) {
|
|
961
|
+
this.areaList = convertToDropdownValue(resp, "name", "name");
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
|
|
965
|
+
async getBranchList() {
|
|
966
|
+
let data = {
|
|
967
|
+
schema: get_schema(),
|
|
968
|
+
table: "branch",
|
|
969
|
+
conditions: JSON.stringify({ is_active: true }),
|
|
970
|
+
order_by: ["branch_name asc"],
|
|
971
|
+
};
|
|
972
|
+
let resp = await request("/select", data, "get");
|
|
973
|
+
if (resp.length > 0) {
|
|
974
|
+
this.branchList = convertToDropdownValue(resp, "id_branch", "branch_name");
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
**Gọi đồng thời trong `load_dropdowns()`:**
|
|
980
|
+
```javascript
|
|
981
|
+
async load_dropdowns() {
|
|
982
|
+
await Promise.all([
|
|
983
|
+
this.getAreaList(),
|
|
984
|
+
this.getBranchList(),
|
|
985
|
+
]);
|
|
986
|
+
},
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
**`convertToDropdownValue(arr, valueField, labelField)`** — chuyển array thô thành `[{value, label}]`:
|
|
990
|
+
```javascript
|
|
991
|
+
// Khi value và label dùng cùng field (enum-style)
|
|
992
|
+
convertToDropdownValue(resp, "name", "name") // → [{ value: "Zone A", label: "Zone A" }]
|
|
993
|
+
|
|
994
|
+
// Khi value và label khác field
|
|
995
|
+
convertToDropdownValue(resp, "id_branch", "branch_name") // → [{ value: "uuid...", label: "Main Branch" }]
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
---
|
|
999
|
+
|
|
1000
|
+
### masterApi.js — Pattern
|
|
1001
|
+
|
|
1002
|
+
```javascript
|
|
1003
|
+
// Get single record — trả về {} nếu không có
|
|
1004
|
+
export const get_agreement_by_id = async (id_agreement) => {
|
|
1005
|
+
const result = await request("/select", {
|
|
1006
|
+
schema: get_schema(),
|
|
1007
|
+
table: "v_agreement",
|
|
1008
|
+
conditions: JSON.stringify({ id_agreement }),
|
|
1009
|
+
}, "get");
|
|
1010
|
+
return result.length > 0 ? result[0] : {};
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
// Get dropdown list — trả về [{value, label}]
|
|
1014
|
+
export const get_branch_list_dropdown = async () => {
|
|
1015
|
+
return await request("/select", {
|
|
1016
|
+
schema: get_schema(),
|
|
1017
|
+
table: "branch",
|
|
1018
|
+
columns: ["id_branch as value", "branch_name as label"],
|
|
1019
|
+
conditions: JSON.stringify({ is_active: true }),
|
|
1020
|
+
order_by: ["branch_name asc"],
|
|
1021
|
+
}, "get");
|
|
1022
|
+
};
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
### state.js
|
|
1026
|
+
|
|
1027
|
+
```javascript
|
|
1028
|
+
state.page_header = t("Agreement Management"); // set page title
|
|
1029
|
+
state.username // current user
|
|
1030
|
+
state.usergroup // user group
|
|
1031
|
+
state.language // current language
|
|
1032
|
+
state.loading // boolean — auto-managed by request()
|
|
1033
|
+
state.loading_count // internal counter
|
|
1034
|
+
|
|
1035
|
+
get_schema() // → "qas" | "prd"
|
|
1036
|
+
baseurl() // → base URL
|
|
1037
|
+
authorize_menu(path) // → boolean
|
|
1038
|
+
menu() // → menu structure
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
### utils.js
|
|
1042
|
+
|
|
1043
|
+
```javascript
|
|
1044
|
+
formatDate(date, "YYYY-MM-DD") // "2024-01-15"
|
|
1045
|
+
formatDate(date, "DD/MM/YYYY") // "15/01/2024"
|
|
1046
|
+
formatDate(date, "DD/MM/YYYY HH:mm") // "15/01/2024 10:30"
|
|
1047
|
+
formatNumber(1234567.89, ".", 2) // "1.234.567,89"
|
|
1048
|
+
check_is_null_or_blank(value) // true nếu null/undefined/""/[]/{}
|
|
1049
|
+
// → dùng cho MỌI loại: string, array, object
|
|
1050
|
+
// check_is_null_or_blank("") → true
|
|
1051
|
+
// check_is_null_or_blank([]) → true ← array rỗng
|
|
1052
|
+
// check_is_null_or_blank({}) → true ← object rỗng
|
|
1053
|
+
// check_is_null_or_blank(null) → true
|
|
1054
|
+
// check_is_null_or_blank("abc") → false
|
|
1055
|
+
// check_is_null_or_blank([1]) → false
|
|
1056
|
+
check_is_zero(value) // true if 0/null/""
|
|
1057
|
+
convertToDropdownValue(arr, "id", "name") // [{value, label}]
|
|
1058
|
+
show_message("success", "Saved!")
|
|
1059
|
+
show_message("error", "Failed!")
|
|
1060
|
+
show_message("warning", "Check input!")
|
|
1061
|
+
go_to_page("/path", { id: "uuid" })
|
|
1062
|
+
go_back("/fallback")
|
|
1063
|
+
screen_height() // viewport height
|
|
1064
|
+
bodautiengviet(str) // remove Vietnamese diacritics
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
### useSession.js
|
|
1068
|
+
|
|
1069
|
+
```javascript
|
|
1070
|
+
setSession("user_data", response.data, 86400000) // expire 24h
|
|
1071
|
+
const userData = getSession("user_data")
|
|
1072
|
+
clearSession()
|
|
1073
|
+
refreshSession(86400000)
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
### useTranslation.js
|
|
1077
|
+
|
|
1078
|
+
```javascript
|
|
1079
|
+
t("Field Name") // → translated string
|
|
1080
|
+
t("Save") // → "บันทึก" | "Lưu" | "Save"
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
> **Lưu ý dùng `t()` trong `data()`:** OK vì Nuxt 3 auto-import composables trước khi `data()` chạy.
|
|
1084
|
+
> Dùng bình thường cho column definitions, label mặc định, v.v.
|
|
1085
|
+
|
|
1086
|
+
### dayjs — DATETIME STANDARD
|
|
1087
|
+
|
|
1088
|
+
> **QUY TẮC BẮT BUỘC**: Mọi biến liên quan đến datetime PHẢI dùng `dayjs()`.
|
|
1089
|
+
> KHÔNG dùng `new Date()`, `Date.now()`, `.getFullYear()`, `.getMonth()`, v.v.
|
|
1090
|
+
> `dayjs` có sẵn qua composables (auto-import) — dùng trực tiếp, không cần import.
|
|
1091
|
+
|
|
1092
|
+
```javascript
|
|
1093
|
+
// ── Tạo giá trị mặc định cho filter/date fields ──────────────────
|
|
1094
|
+
dayjs().format("YYYY-MM-DD") // "2024-03-26"
|
|
1095
|
+
dayjs().format("YYYY-MM-DD HH:mm:ss") // "2024-03-26 10:30:00"
|
|
1096
|
+
dayjs().startOf("month").format("YYYY-MM-DD") // "2024-03-01"
|
|
1097
|
+
dayjs().endOf("month").format("YYYY-MM-DD") // "2024-03-31"
|
|
1098
|
+
dayjs().subtract(1, "month").startOf("month").format("YYYY-MM-DD") // đầu tháng trước
|
|
1099
|
+
dayjs().subtract(1, "month").endOf("month").format("YYYY-MM-DD") // cuối tháng trước
|
|
1100
|
+
|
|
1101
|
+
// ── Lấy thành phần từ ngày ────────────────────────────────────────
|
|
1102
|
+
dayjs().year() // 2024 ← THAY new Date().getFullYear()
|
|
1103
|
+
dayjs().month() + 1 // 3 ← month() trả về 0-indexed
|
|
1104
|
+
dayjs().date() // 26
|
|
1105
|
+
|
|
1106
|
+
// ── Tính toán, so sánh ───────────────────────────────────────────
|
|
1107
|
+
dayjs("2024-01-15").isAfter(dayjs("2024-01-01")) // true
|
|
1108
|
+
dayjs("2024-01-15").isBefore(dayjs("2024-12-31")) // true
|
|
1109
|
+
dayjs("2024-01-15").diff(dayjs("2024-01-01"), "day") // 14
|
|
1110
|
+
dayjs().add(7, "day").format("YYYY-MM-DD") // 7 ngày sau
|
|
1111
|
+
|
|
1112
|
+
// ── Parse và format ──────────────────────────────────────────────
|
|
1113
|
+
dayjs("2024-01-15 10:30:00").format("DD/MM/YYYY") // "15/01/2024"
|
|
1114
|
+
dayjs("2024-01-15").isValid() // true — check hợp lệ trước dùng
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
**Pattern trong `data()` — dùng dayjs cho mọi default date:**
|
|
1118
|
+
```javascript
|
|
1119
|
+
data() {
|
|
1120
|
+
return {
|
|
1121
|
+
from_date: dayjs().startOf("month").format("YYYY-MM-DD"),
|
|
1122
|
+
to_date: dayjs().endOf("month").format("YYYY-MM-DD"),
|
|
1123
|
+
filter_year: dayjs().year(),
|
|
1124
|
+
filter_month: dayjs().format("YYYY-MM"),
|
|
1125
|
+
};
|
|
1126
|
+
},
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
---
|
|
1130
|
+
|
|
1131
|
+
## 9. API Call Patterns
|
|
1132
|
+
|
|
1133
|
+
### Đọc danh sách (dùng view)
|
|
1134
|
+
```javascript
|
|
1135
|
+
async get_data() {
|
|
1136
|
+
this.rows = await request("/select", {
|
|
1137
|
+
schema: get_schema(),
|
|
1138
|
+
table: "v_agreement", // LUÔN dùng view, không phải table thô
|
|
1139
|
+
conditions: JSON.stringify({
|
|
1140
|
+
created_at_from: this.from_date,
|
|
1141
|
+
created_at_to: this.to_date,
|
|
1142
|
+
status: this.filter_status || undefined,
|
|
1143
|
+
}),
|
|
1144
|
+
order_by: ["created_at desc"],
|
|
1145
|
+
}, "get");
|
|
1146
|
+
},
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### Lưu (insert/update)
|
|
1150
|
+
```javascript
|
|
1151
|
+
async save() {
|
|
1152
|
+
if (!this.validate()) return;
|
|
1153
|
+
this.is_saving = true;
|
|
1154
|
+
await request("/save", {
|
|
1155
|
+
table: "agreement",
|
|
1156
|
+
key_array: ["id_agreement"],
|
|
1157
|
+
...this.workingObj,
|
|
1158
|
+
});
|
|
1159
|
+
this.is_saving = false;
|
|
1160
|
+
show_message("success", t("Saved successfully"));
|
|
1161
|
+
},
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
### Xóa mềm (soft delete — preferred)
|
|
1165
|
+
```javascript
|
|
1166
|
+
async delete_record() {
|
|
1167
|
+
await request("/save", {
|
|
1168
|
+
table: "agreement",
|
|
1169
|
+
key_array: ["id_agreement"],
|
|
1170
|
+
id_agreement: this.workingObj.id_agreement,
|
|
1171
|
+
is_deleted: true,
|
|
1172
|
+
});
|
|
1173
|
+
show_message("success", t("Deleted"));
|
|
1174
|
+
this.$router.push("/Agreement/AgreementList");
|
|
1175
|
+
},
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
### Chạy Stored Procedure
|
|
1179
|
+
```javascript
|
|
1180
|
+
await request("/run_proc", {
|
|
1181
|
+
schema: get_schema(),
|
|
1182
|
+
proc_name: "sp_generate_running_no",
|
|
1183
|
+
params: { prefix: "AGR", year: dayjs().year() }, // ✅ dayjs, KHÔNG dùng new Date()
|
|
1184
|
+
});
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
---
|
|
1188
|
+
|
|
1189
|
+
## 10. Pattern: Trang Danh Sách
|
|
1190
|
+
|
|
1191
|
+
```vue
|
|
1192
|
+
<template>
|
|
1193
|
+
<div>
|
|
1194
|
+
<!-- Filter -->
|
|
1195
|
+
<div class="row mb-3">
|
|
1196
|
+
<div class="col-md-3">
|
|
1197
|
+
<ControlDatetime v-model="from_date" name="From" type="date" />
|
|
1198
|
+
</div>
|
|
1199
|
+
<div class="col-md-3">
|
|
1200
|
+
<ControlDatetime v-model="to_date" name="To" type="date" />
|
|
1201
|
+
</div>
|
|
1202
|
+
<div class="col-md-3">
|
|
1203
|
+
<ControlDropdownlist v-model="filter_status" name="Status" :list="statusList" placeholder="All" />
|
|
1204
|
+
</div>
|
|
1205
|
+
<div class="col-md-3 d-flex align-items-end gap-2">
|
|
1206
|
+
<ControlButton name="Search" type="primary" @onClick="get_data" />
|
|
1207
|
+
</div>
|
|
1208
|
+
</div>
|
|
1209
|
+
|
|
1210
|
+
<!-- Grid -->
|
|
1211
|
+
<GridAG
|
|
1212
|
+
:rows="rows"
|
|
1213
|
+
:columns="columns"
|
|
1214
|
+
:can_add="true"
|
|
1215
|
+
:can_export="true"
|
|
1216
|
+
@onAdd="() => $router.push('/Agreement/AgreementForm')"
|
|
1217
|
+
@onClickDetail="(row) => $router.push({ path: '/Agreement/AgreementForm', query: { id: row.id_agreement } })"
|
|
1218
|
+
@reloadData="get_data"
|
|
1219
|
+
/>
|
|
1220
|
+
|
|
1221
|
+
<!-- Floating add button (mobile/alternative) -->
|
|
1222
|
+
<ControlButtonFloat name="New" @clickButtonFloat="() => $router.push('/Agreement/AgreementForm')" />
|
|
1223
|
+
</div>
|
|
1224
|
+
</template>
|
|
1225
|
+
|
|
1226
|
+
<script>
|
|
1227
|
+
export default {
|
|
1228
|
+
data() {
|
|
1229
|
+
return {
|
|
1230
|
+
from_date: dayjs().subtract(1, "month").startOf("month").format("YYYY-MM-DD"),
|
|
1231
|
+
to_date: dayjs().endOf("month").format("YYYY-MM-DD"),
|
|
1232
|
+
filter_status: "",
|
|
1233
|
+
statusList: [
|
|
1234
|
+
{ value: "draft", label: "Draft" },
|
|
1235
|
+
{ value: "approved", label: "Approved" },
|
|
1236
|
+
{ value: "rejected", label: "Rejected" },
|
|
1237
|
+
],
|
|
1238
|
+
rows: [],
|
|
1239
|
+
columns: [
|
|
1240
|
+
{ key: "request_no", title: t("No."), type: "text", width: 150 },
|
|
1241
|
+
{ key: "created_at", title: t("Date"), type: "date", width: 110 },
|
|
1242
|
+
{ key: "branch_name",title: t("Branch"), type: "text", width: 150 },
|
|
1243
|
+
{ key: "amount", title: t("Amount"), type: "number", width: 120, aggFunc: "sum" },
|
|
1244
|
+
{ key: "status", title: t("Status"), type: "text", width: 100 },
|
|
1245
|
+
],
|
|
1246
|
+
};
|
|
1247
|
+
},
|
|
1248
|
+
async created() {
|
|
1249
|
+
if (!authorize_menu(this.$route.fullPath)) { this.$router.push(state.homepage); return; }
|
|
1250
|
+
state.page_header = t("Agreement List");
|
|
1251
|
+
await this.get_data();
|
|
1252
|
+
},
|
|
1253
|
+
methods: {
|
|
1254
|
+
async get_data() {
|
|
1255
|
+
this.rows = await request("/select", {
|
|
1256
|
+
schema: get_schema(),
|
|
1257
|
+
table: "v_agreement",
|
|
1258
|
+
conditions: JSON.stringify({
|
|
1259
|
+
from_date: this.from_date,
|
|
1260
|
+
to_date: this.to_date,
|
|
1261
|
+
status: this.filter_status || undefined,
|
|
1262
|
+
}),
|
|
1263
|
+
order_by: ["created_at desc"],
|
|
1264
|
+
}, "get");
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
};
|
|
1268
|
+
</script>
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
---
|
|
1272
|
+
|
|
1273
|
+
## 11. Pattern: Trang Form
|
|
1274
|
+
|
|
1275
|
+
```vue
|
|
1276
|
+
<template>
|
|
1277
|
+
<div>
|
|
1278
|
+
<ControlBox title="Agreement Information" type="info" :col="12">
|
|
1279
|
+
<div class="row">
|
|
1280
|
+
<div class="col-md-6">
|
|
1281
|
+
<ControlInput v-model="workingObj.agreement_number" name="Agreement Number"
|
|
1282
|
+
:required="true" :disabled="is_locked" />
|
|
1283
|
+
</div>
|
|
1284
|
+
<div class="col-md-6">
|
|
1285
|
+
<ControlDropdownlist v-model="workingObj.id_branch" name="Branch"
|
|
1286
|
+
:list="branchList" :required="true" :disabled="is_locked" />
|
|
1287
|
+
</div>
|
|
1288
|
+
<div class="col-md-4">
|
|
1289
|
+
<ControlDatetime v-model="workingObj.start_date" name="Start Date"
|
|
1290
|
+
type="date" :required="true" :disabled="is_locked" />
|
|
1291
|
+
</div>
|
|
1292
|
+
<div class="col-md-4">
|
|
1293
|
+
<ControlDatetime v-model="workingObj.end_date" name="End Date"
|
|
1294
|
+
type="date" :required="true" :disabled="is_locked"
|
|
1295
|
+
:minDate="workingObj.start_date" />
|
|
1296
|
+
</div>
|
|
1297
|
+
<div class="col-md-4">
|
|
1298
|
+
<ControlInput v-model="workingObj.amount" name="Amount"
|
|
1299
|
+
type="number" :required="true" :disabled="is_locked" />
|
|
1300
|
+
</div>
|
|
1301
|
+
<div class="col-md-12">
|
|
1302
|
+
<ControlInput v-model="workingObj.note" name="Note"
|
|
1303
|
+
type="textarea" :rows="3" :disabled="is_locked" />
|
|
1304
|
+
</div>
|
|
1305
|
+
</div>
|
|
1306
|
+
</ControlBox>
|
|
1307
|
+
|
|
1308
|
+
<!-- Action Buttons -->
|
|
1309
|
+
<div class="row mt-3">
|
|
1310
|
+
<div class="col-12">
|
|
1311
|
+
<ControlButton v-if="workingObj.can_edit" name="Save" type="primary" @onClick="save" />
|
|
1312
|
+
<ControlButton v-if="workingObj.can_submit" name="Submit" type="success" @onClick="submit" />
|
|
1313
|
+
<ControlButton v-if="workingObj.can_approve" name="Approve" type="success"
|
|
1314
|
+
@onClick="show_confirm_approve = true" />
|
|
1315
|
+
<ControlButton v-if="workingObj.can_reject" name="Reject" type="danger"
|
|
1316
|
+
@onClick="show_reject = true" />
|
|
1317
|
+
<ControlButton name="Back" type="secondary" @onClick="$router.go(-1)" />
|
|
1318
|
+
</div>
|
|
1319
|
+
</div>
|
|
1320
|
+
|
|
1321
|
+
<!-- Popups -->
|
|
1322
|
+
<ControlPopupConfirm :visible="show_confirm_delete" title="Delete?"
|
|
1323
|
+
message="Are you sure?" @onConfirm="delete_record" @onClose="show_confirm_delete = false" />
|
|
1324
|
+
<ControlPopupConfirmReject :visible="show_confirm_approve" title="Approve"
|
|
1325
|
+
@onConfirm="on_approve" @onClose="show_confirm_approve = false" />
|
|
1326
|
+
<ControlPopupReject :visible="show_reject" title="Reject"
|
|
1327
|
+
@onConfirm="on_reject" @onClose="show_reject = false" />
|
|
1328
|
+
</div>
|
|
1329
|
+
</template>
|
|
1330
|
+
|
|
1331
|
+
<script>
|
|
1332
|
+
export default {
|
|
1333
|
+
data() {
|
|
1334
|
+
return {
|
|
1335
|
+
workingObj: { id_agreement: "", agreement_number: "", id_branch: "",
|
|
1336
|
+
start_date: "", end_date: "", amount: 0, note: "",
|
|
1337
|
+
can_edit: true, can_submit: false, can_approve: false,
|
|
1338
|
+
can_reject: false, is_deleted: false },
|
|
1339
|
+
branchList: [],
|
|
1340
|
+
show_confirm_delete: false,
|
|
1341
|
+
show_confirm_approve: false,
|
|
1342
|
+
show_reject: false,
|
|
1343
|
+
};
|
|
1344
|
+
},
|
|
1345
|
+
computed: {
|
|
1346
|
+
is_locked() { return !this.workingObj.can_edit; },
|
|
1347
|
+
},
|
|
1348
|
+
async created() {
|
|
1349
|
+
if (!authorize_menu(this.$route.fullPath)) { this.$router.push(state.homepage); return; }
|
|
1350
|
+
state.page_header = t("Agreement Form");
|
|
1351
|
+
const id = this.$route.query.id;
|
|
1352
|
+
await Promise.all([
|
|
1353
|
+
id ? this.load_by_id(id) : Promise.resolve(),
|
|
1354
|
+
this.load_dropdowns(),
|
|
1355
|
+
]);
|
|
1356
|
+
},
|
|
1357
|
+
methods: {
|
|
1358
|
+
async load_by_id(id) {
|
|
1359
|
+
this.workingObj = await get_agreement_by_id(id);
|
|
1360
|
+
},
|
|
1361
|
+
async load_dropdowns() {
|
|
1362
|
+
this.branchList = await get_branch_list_dropdown();
|
|
1363
|
+
},
|
|
1364
|
+
async save() {
|
|
1365
|
+
if (!this.validate()) return;
|
|
1366
|
+
await request("/save", { table: "agreement", key_array: ["id_agreement"], ...this.workingObj });
|
|
1367
|
+
show_message("success", t("Saved successfully"));
|
|
1368
|
+
},
|
|
1369
|
+
async delete_record() {
|
|
1370
|
+
this.show_confirm_delete = false;
|
|
1371
|
+
await request("/save", { table: "agreement", key_array: ["id_agreement"],
|
|
1372
|
+
id_agreement: this.workingObj.id_agreement, is_deleted: true });
|
|
1373
|
+
this.$router.push("/Agreement/AgreementList");
|
|
1374
|
+
},
|
|
1375
|
+
async on_approve(reason, is_adhoc, is_onplan) {
|
|
1376
|
+
this.show_confirm_approve = false;
|
|
1377
|
+
await request_origin("/api/agreement/approve", {
|
|
1378
|
+
schema: get_schema(),
|
|
1379
|
+
id_agreement: this.workingObj.id_agreement,
|
|
1380
|
+
username: state.username, // ✅ luôn truyền username cho custom approve endpoints
|
|
1381
|
+
reason, is_adhoc, is_onplan,
|
|
1382
|
+
});
|
|
1383
|
+
await this.load_by_id(this.workingObj.id_agreement);
|
|
1384
|
+
},
|
|
1385
|
+
async on_reject(reason, is_adhoc, is_onplan) {
|
|
1386
|
+
this.show_reject = false;
|
|
1387
|
+
await request("/save", { table: "agreement", key_array: ["id_agreement"],
|
|
1388
|
+
id_agreement: this.workingObj.id_agreement, status: "rejected", reject_reason: reason });
|
|
1389
|
+
await this.load_by_id(this.workingObj.id_agreement);
|
|
1390
|
+
},
|
|
1391
|
+
validate() {
|
|
1392
|
+
const required = [
|
|
1393
|
+
{ key: "agreement_number", name: t("Agreement Number") },
|
|
1394
|
+
{ key: "id_branch", name: t("Branch") },
|
|
1395
|
+
{ key: "start_date", name: t("Start Date") },
|
|
1396
|
+
];
|
|
1397
|
+
for (const f of required) {
|
|
1398
|
+
if (check_is_null_or_blank(this.workingObj[f.key])) {
|
|
1399
|
+
show_message("error", `${f.name} ${t("is required")}`); return false;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return true;
|
|
1403
|
+
},
|
|
1404
|
+
},
|
|
1405
|
+
};
|
|
1406
|
+
</script>
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
---
|
|
1410
|
+
|
|
1411
|
+
## 12. Routing & Navigation
|
|
1412
|
+
|
|
1413
|
+
```javascript
|
|
1414
|
+
// Navigate with query
|
|
1415
|
+
this.$router.push({ path: "/Agreement/AgreementForm", query: { id: row.id_agreement } });
|
|
1416
|
+
|
|
1417
|
+
// Get params
|
|
1418
|
+
const id = this.$route.query.id;
|
|
1419
|
+
|
|
1420
|
+
// Back
|
|
1421
|
+
this.$router.go(-1);
|
|
1422
|
+
this.$router.push("/Agreement/AgreementList");
|
|
1423
|
+
|
|
1424
|
+
// Utility (từ utils.js — auto-imported)
|
|
1425
|
+
go_to_page("/Agreement/AgreementForm", { id: "uuid" });
|
|
1426
|
+
go_back("/Agreement/AgreementList");
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
## 13. Validation Pattern
|
|
1432
|
+
|
|
1433
|
+
```javascript
|
|
1434
|
+
validate() {
|
|
1435
|
+
// Required fields
|
|
1436
|
+
const required = [
|
|
1437
|
+
{ key: "agreement_number", name: t("Agreement Number") },
|
|
1438
|
+
{ key: "id_branch", name: t("Branch") },
|
|
1439
|
+
{ key: "start_date", name: t("Start Date") },
|
|
1440
|
+
];
|
|
1441
|
+
for (const f of required) {
|
|
1442
|
+
if (check_is_null_or_blank(this.workingObj[f.key])) {
|
|
1443
|
+
show_message("error", `${f.name} ${t("is required")}`);
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// Business rules
|
|
1449
|
+
if (this.workingObj.end_date && this.workingObj.start_date) {
|
|
1450
|
+
if (this.workingObj.end_date < this.workingObj.start_date) {
|
|
1451
|
+
show_message("error", t("End date must be after start date"));
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (check_is_zero(this.workingObj.amount)) {
|
|
1457
|
+
show_message("error", t("Amount must be greater than 0"));
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
return true;
|
|
1462
|
+
},
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
---
|
|
1466
|
+
|
|
1467
|
+
## 14. Đề Xuất Components Mới
|
|
1468
|
+
|
|
1469
|
+
> **Lưu ý:** Chỉ liệt kê những component thực sự chưa có.
|
|
1470
|
+
> - Number input → dùng `ControlInput type="number"`
|
|
1471
|
+
> - Search input → dùng `ControlInput type="search"`
|
|
1472
|
+
> - Radio group → dùng `ControlCheckbox` với layout ngang là đủ
|
|
1473
|
+
|
|
1474
|
+
---
|
|
1475
|
+
|
|
1476
|
+
### 🔮 Tạo khi thực sự cần dùng
|
|
1477
|
+
|
|
1478
|
+
#### `ControlStatusBadge` — Hiển thị status với màu sắc theo map
|
|
1479
|
+
```vue
|
|
1480
|
+
<ControlStatusBadge :status="row.status"
|
|
1481
|
+
:map="{
|
|
1482
|
+
draft: { label: 'Draft', color: 'gray' },
|
|
1483
|
+
approved: { label: 'Approved', color: 'green' },
|
|
1484
|
+
rejected: { label: 'Rejected', color: 'red' },
|
|
1485
|
+
}"
|
|
1486
|
+
/>
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
#### `ControlFileList` — Hiển thị + download danh sách files đã upload
|
|
1490
|
+
```vue
|
|
1491
|
+
<ControlFileList
|
|
1492
|
+
:files="workingObj.file_list"
|
|
1493
|
+
:can_delete="workingObj.can_edit"
|
|
1494
|
+
@onDelete="remove_file"
|
|
1495
|
+
/>
|
|
1496
|
+
```
|
|
1497
|
+
|
|
1498
|
+
#### `ControlSummaryCard` — KPI/summary card cho dashboard
|
|
1499
|
+
```vue
|
|
1500
|
+
<ControlSummaryCard title="Total Amount" :value="total_amount" format="number" color="primary" />
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
#### `GridCardList` — Hiển thị dạng card thay vì table (mobile / dashboard)
|
|
1504
|
+
```vue
|
|
1505
|
+
<GridCardList :rows="rows" :fields="['name', 'amount', 'status']" @onCardClick="go_to_detail" />
|
|
1506
|
+
```
|