mulmoclaude 0.9.0 → 0.9.2
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/README.md +95 -38
- package/bin/mulmoclaude.js +1 -2
- package/client/assets/JsonEditor-CPJfn76E.css +1 -0
- package/client/assets/JsonEditor-CoWGJG3Y.js +10 -0
- package/client/assets/abnfDiagram-VRR7QNED-6nNByj6v-By7GJ3xW.js +1 -0
- package/client/assets/abnfDiagram-VRR7QNED-BrQlVixL.js +1 -0
- package/client/assets/arc-BXutyUAX.js +1 -0
- package/client/assets/arc-zT0wd74V-DulGJNnb.js +1 -0
- package/client/assets/architecture-TIHT7OUA-CYMWc3UT-C7HoFzWN.js +1 -0
- package/client/assets/architecture-TIHT7OUA-aml8u-G9.js +1 -0
- package/client/assets/architectureDiagram-ZJ3FMSHR-Bgnyaj_i-CCpEsCHN.js +36 -0
- package/client/assets/architectureDiagram-ZJ3FMSHR-DJTFpPjB.js +36 -0
- package/client/assets/array-BifhSqXX.js +1 -0
- package/client/assets/array-CApNbSU9-BifhSqXX.js +1 -0
- package/client/assets/blockDiagram-677ZJIJ3-DQ35o5E4-BXS11D3J.js +132 -0
- package/client/assets/blockDiagram-677ZJIJ3-aYvCODxv.js +132 -0
- package/client/assets/c4Diagram-LMCZKHZV-Bau6RMBb.js +10 -0
- package/client/assets/c4Diagram-LMCZKHZV-ClWZeiWo-DXGnOOUf.js +10 -0
- package/client/assets/channel-D9VSi_QV.js +1 -0
- package/client/assets/channel-Di5rtkx0-C3gZcytF.js +1 -0
- package/client/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/client/assets/chunk-2Q5K7J3B-DfKpwyl9-DTxwf5E9.js +1 -0
- package/client/assets/chunk-32BRIVSS-CCt9wtYd-Cwa_IP35.js +1 -0
- package/client/assets/chunk-32BRIVSS-CxzHeys5.js +1 -0
- package/client/assets/chunk-52WLFC77-BtNjbTdw.js +10 -0
- package/client/assets/chunk-52WLFC77-FbBbR4uI-wsNC5d-f.js +10 -0
- package/client/assets/chunk-5VM5RSS4-BYnnUiN6-C4rOnu9A.js +15 -0
- package/client/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/client/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- package/client/assets/chunk-7BUUIJ7U-hh8aCuGX-mGEpPzQy.js +1 -0
- package/client/assets/chunk-C7G6YPKG-BE8ehsnc.js +1 -0
- package/client/assets/chunk-C7G6YPKG-C87hlS9c-BitbJpTa.js +1 -0
- package/client/assets/{chunk-D8eiyYIV-BY16KEZc.js → chunk-D8eiyYIV-BeeEsA1B.js} +1 -1
- package/client/assets/chunk-EX3LRPZG-Bhgskzyy.js +231 -0
- package/client/assets/chunk-EX3LRPZG-BqGqMXLN-Cl2Eo1_u.js +231 -0
- package/client/assets/chunk-FWX5IMBZ-BkbSAAuW-C2r4AuAO.js +2 -0
- package/client/assets/chunk-FWX5IMBZ-D9sskZb_.js +2 -0
- package/client/assets/chunk-HOUHSVGY-CBCXk7rb.js +1 -0
- package/client/assets/chunk-HOUHSVGY-Cxu0eDlh-owg_DcY0.js +1 -0
- package/client/assets/chunk-ICXQ74PX-DTqPpDQ9.js +2 -0
- package/client/assets/chunk-ICXQ74PX-hiraF_Xj-CdaEwVin.js +2 -0
- package/client/assets/chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js +1 -0
- package/client/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/client/assets/chunk-KEIR6QF5-DNzq6p3w.js +161 -0
- package/client/assets/chunk-MOJQB5TN-CqxshQHA-Cn_EpDyZ.js +88 -0
- package/client/assets/chunk-MOJQB5TN-D1s_zUGz.js +88 -0
- package/client/assets/chunk-OGEWGWER-CjCr7ceX-Dfk5-70y.js +1 -0
- package/client/assets/chunk-OGEWGWER-DzfhxoQd.js +1 -0
- package/client/assets/chunk-PUDLZKDR-BZlebNhn.js +156 -0
- package/client/assets/chunk-PUDLZKDR-Dx6M-vz1-CLyVurOz.js +156 -0
- package/client/assets/chunk-Q4XR5HBZ-BcJb7mwA.js +70 -0
- package/client/assets/chunk-Q4XR5HBZ-CZd-9lTB-qPEYoRNc.js +11 -0
- package/client/assets/chunk-RYQCIY6F-B_IgMG7T.js +1 -0
- package/client/assets/chunk-RYQCIY6F-YkbemkCt-CX4fncTQ.js +1 -0
- package/client/assets/chunk-V7JOEXUC-Buce04o6.js +206 -0
- package/client/assets/chunk-V7JOEXUC-rI0xlC_O-C607vJXy.js +206 -0
- package/client/assets/chunk-VAUOI2AC-CxhkzXN0.js +1 -0
- package/client/assets/chunk-VAUOI2AC-DrcykVNK-CSROJWdE.js +1 -0
- package/client/assets/chunk-VR4S4FIN-D5afNrsS.js +1 -0
- package/client/assets/chunk-VR4S4FIN-O6iF8Yvf-CLIwrgqU.js +1 -0
- package/client/assets/chunk-WYO6CB5R-B83L_z6I-Cs8mg9If.js +127 -0
- package/client/assets/chunk-WYO6CB5R-BXTIdTMw.js +125 -0
- package/client/assets/chunk-XXDRQBXY-DFXH_lWn-B5HjCp2c.js +1 -0
- package/client/assets/chunk-XXDRQBXY-DXMD6ofA.js +1 -0
- package/client/assets/chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js +1 -0
- package/client/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/client/assets/chunk-ZGVPDNZ5-BpFv9JSP-CHYySMjV.js +62 -0
- package/client/assets/chunk-ZGVPDNZ5-CYusI0J_.js +62 -0
- package/client/assets/chunk-ZIRB5QZD-Biy6ZNFG-D1maAiXb.js +32 -0
- package/client/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
- package/client/assets/classDiagram-OUVF2IWQ-BgAZMSbT-BgxEE1D3.js +1 -0
- package/client/assets/classDiagram-OUVF2IWQ-CBD3Eidl.js +1 -0
- package/client/assets/classDiagram-v2-EOCWNBFH-CBD3Eidl.js +1 -0
- package/client/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-BgxEE1D3.js +1 -0
- package/client/assets/cose-bilkent-JH36ORCC-CuSL2tA8-aMIqISfj.js +1 -0
- package/client/assets/cose-bilkent-JH36ORCC-WAJDBXv1.js +1 -0
- package/client/assets/cynefin-VYW2F7L2-DH2qkJKw.js +1 -0
- package/client/assets/cynefin-VYW2F7L2-DqA3n9nY-BNWhIK_n.js +1 -0
- package/client/assets/cynefinDiagram-TSTJHNR4-CUvPV9MV.js +62 -0
- package/client/assets/cynefinDiagram-TSTJHNR4-x0-0sQ15-Cq1gFcFu.js +62 -0
- package/client/assets/cytoscape.esm-CzdwbRaj-Djp6vQyU.js +321 -0
- package/client/assets/cytoscape.esm-Djp6vQyU.js +321 -0
- package/client/assets/dagre-CXRCoUWR.js +1 -0
- package/client/assets/dagre-VKFMJZFB-CQdfl-bx-CzZZuace.js +4 -0
- package/client/assets/dagre-VKFMJZFB-C_9IS7mB.js +4 -0
- package/client/assets/dagre-wczQIDso-CfevN9RO.js +1 -0
- package/client/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/client/assets/defaultLocale-DUNguUWs-BbDo_yJX.js +1 -0
- package/client/assets/diagram-FQU43EPY-BOSB6VUb-isPdQ8WX.js +3 -0
- package/client/assets/diagram-FQU43EPY-BP8N00-b.js +3 -0
- package/client/assets/diagram-G47NLZAW-DLXrcXsN-BIqj7RKy.js +24 -0
- package/client/assets/diagram-G47NLZAW-ulE1JlWG.js +24 -0
- package/client/assets/diagram-NH7WQ7WH-BMQp1rkF-D6-fOq_v.js +24 -0
- package/client/assets/diagram-NH7WQ7WH-D_IXrL3i.js +24 -0
- package/client/assets/diagram-OA4YK3LP-C-pC6Eyu.js +30 -0
- package/client/assets/diagram-OA4YK3LP-D1wQ0vUj-BNi6QLCK.js +30 -0
- package/client/assets/diagram-WEI45ONY-Di9m35i-.js +41 -0
- package/client/assets/diagram-WEI45ONY-RR0DpF8R-BS3nrb3w.js +41 -0
- package/client/assets/dist-BQBs5pjy-BmtZ7Oc2.js +1 -0
- package/client/assets/dist-CQ3HaWOk.js +1 -0
- package/client/assets/ebnfDiagram-CCIWWBDH-Cbwnim2x.js +1 -0
- package/client/assets/ebnfDiagram-CCIWWBDH-M123uVJ8-9-dq55nQ.js +1 -0
- package/client/assets/erDiagram-Q63AITRT-BWx_-PXG-CWGfG4z5.js +85 -0
- package/client/assets/erDiagram-Q63AITRT-CMbtO3Sm.js +85 -0
- package/client/assets/eventmodeling-45OFAUF4--32SIpkL.js +1 -0
- package/client/assets/eventmodeling-45OFAUF4-_BVSjAXf-DQdL0Icr.js +1 -0
- package/client/assets/flowDiagram-23GEKE2U-BeOc_anm-CuVzKmmU.js +1 -0
- package/client/assets/flowDiagram-23GEKE2U-Dofa_qxG.js +1 -0
- package/client/assets/ganttDiagram-NO4QXBWP-BOoJ1eTw-TWoNmSvq.js +292 -0
- package/client/assets/ganttDiagram-NO4QXBWP-BgoAVKuc.js +292 -0
- package/client/assets/gitGraph-TEB2WS4Q-CH12KLTN-Bz4frAhV.js +1 -0
- package/client/assets/gitGraph-TEB2WS4Q-DIMvNvqt.js +1 -0
- package/client/assets/gitGraphDiagram-IHSO6WYX-B2CJhk_G-ClOKkjxw.js +106 -0
- package/client/assets/gitGraphDiagram-IHSO6WYX-Dmb6KnPz.js +106 -0
- package/client/assets/graphlib-B8gBHxth.js +1 -0
- package/client/assets/graphlib-hY-1btwe-DQjxxcnr.js +1 -0
- package/client/assets/{html2canvas-CDGcmOD3-CKJ6vKPo.js → html2canvas-CDGcmOD3-DRL9pFVl.js} +2 -2
- package/client/assets/{index-9lmYSaus.js → index-Dc0R-HW5.js} +129 -190
- package/client/assets/index-Dxo1Zdd-.css +2 -0
- package/client/assets/{index.es-DqtpmBm8-DFXjJgCa.js → index.es-DqtpmBm8-EQk3NgR8.js} +1 -1
- package/client/assets/info-DKCQHKI2-Cbw3mbiK-GsDZz9IO.js +1 -0
- package/client/assets/info-DKCQHKI2-ViCqobGo.js +1 -0
- package/client/assets/infoDiagram-FWYZ7A6U-HKV7LIG-.js +2 -0
- package/client/assets/infoDiagram-FWYZ7A6U-Mp1X3pBP-ATJv87Ur.js +2 -0
- package/client/assets/init-D6jRqBbL.js +1 -0
- package/client/assets/init-DEsX3bhM-D6jRqBbL.js +1 -0
- package/client/assets/ishikawaDiagram-FXEZZL3T-BNG7tkJu-CGCeOIlJ.js +70 -0
- package/client/assets/ishikawaDiagram-FXEZZL3T-CVDUj46f.js +70 -0
- package/client/assets/journeyDiagram-5HDEW3XC-BA-ESGLP.js +139 -0
- package/client/assets/journeyDiagram-5HDEW3XC-Dbp_hY9X-Bx-IoCe9.js +139 -0
- package/client/assets/kanban-definition-HUTT4EX6-Bqaet01L.js +89 -0
- package/client/assets/kanban-definition-HUTT4EX6-DSTc5u3q-C0JGckt1.js +89 -0
- package/client/assets/katex-CddkPoXu.js +257 -0
- package/client/assets/katex-M0IxphGf-CddkPoXu.js +257 -0
- package/client/assets/lib-4Tgx7rAy.js +114 -0
- package/client/assets/line-B1wBwzrY-f5wxpqoF.js +1 -0
- package/client/assets/line-CkyHfW7d.js +1 -0
- package/client/assets/linear-B9fuEF4c.js +1 -0
- package/client/assets/linear-lrGinF5_-CqNBync7.js +1 -0
- package/client/assets/map-CgrwEyH7-DWQFomlZ.js +1 -0
- package/client/assets/map-DsCK-0Cs.js +1 -0
- package/client/assets/marked.esm-CIU5FDdN.js +64 -0
- package/client/assets/marp-CSq0PPfK.js +3448 -0
- package/client/assets/material-symbols-outlined-DdRFxZLh.woff2 +0 -0
- package/client/assets/mermaid-parser.core-DC7NPJ_M-Ca6XzwfM.js +166 -0
- package/client/assets/mermaid-parser.core-SAwSf4_o.js +7 -0
- package/client/assets/mermaid.core-BBEQfkdJ.js +11 -0
- package/client/assets/mermaid.core-DZM3Ha-E-BxOo-RYG.js +11 -0
- package/client/assets/mindmap-definition-LN4V7U3C-B4BN0efL.js +96 -0
- package/client/assets/mindmap-definition-LN4V7U3C-DYtgcMsY-DynyLTNv.js +96 -0
- package/client/assets/ordinal-B9_Llu10-CU85fhaB.js +1 -0
- package/client/assets/ordinal-CopWnP7w.js +1 -0
- package/client/assets/packet-7NZHBO7P-CY4XRk9N.js +1 -0
- package/client/assets/packet-7NZHBO7P-lwb58iYx--vqSg65J.js +1 -0
- package/client/assets/path-BWPyau1x.js +1 -0
- package/client/assets/path-COivZ1Gw-C5riojK2.js +1 -0
- package/client/assets/pegDiagram-2B236MQR-BOKErjUu.js +1 -0
- package/client/assets/pegDiagram-2B236MQR-C43eIpKM-Bi_5Sxjt.js +1 -0
- package/client/assets/pie-RZYD4A2V-6BHB7bAx.js +1 -0
- package/client/assets/pie-RZYD4A2V-B1UWb4Gu-Dmn2hLWH.js +1 -0
- package/client/assets/pieDiagram-ENE6RG2P-BkTqgJyR-Bcdm19kR.js +39 -0
- package/client/assets/pieDiagram-ENE6RG2P-DtNjvz-U.js +39 -0
- package/client/assets/preload-helper-CZgWQFsJ.js +1 -0
- package/client/assets/purify.es-DY32g7DN.js +3 -0
- package/client/assets/quadrantDiagram-ABIIQ3AL-BQmAJL2v.js +7 -0
- package/client/assets/quadrantDiagram-ABIIQ3AL-Bm1Zjm45-B3orqx_f.js +7 -0
- package/client/assets/radar-I7S5WNFK-7CKcb_l--CqKdnR0v.js +1 -0
- package/client/assets/radar-I7S5WNFK-R7Jhjfdt.js +1 -0
- package/client/assets/railroad-3IZDKUUU-BepSPyHI.js +1 -0
- package/client/assets/railroad-3IZDKUUU-gCySKdnW-CKU26HTP.js +1 -0
- package/client/assets/railroad-abnf-AHOZXSZD-BlsHsp3c.js +1 -0
- package/client/assets/railroad-abnf-AHOZXSZD-BophH4r--CGkGHq54.js +1 -0
- package/client/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-B7bn_PF3.js +1 -0
- package/client/assets/railroad-ebnf-EBAXGLYW-B4fMcGEH.js +1 -0
- package/client/assets/railroad-peg-LSFZ7HO6-B1ZrjJIu.js +1 -0
- package/client/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-CP_B-N5g.js +1 -0
- package/client/assets/railroadDiagram-RFXS5EU6-8cz0Iby3.js +1 -0
- package/client/assets/railroadDiagram-RFXS5EU6-BkfbdeAs-Di6jH5I6.js +1 -0
- package/client/assets/requirementDiagram-TGXJPOKE-CrGTTjYg-DmIC7Shp.js +84 -0
- package/client/assets/requirementDiagram-TGXJPOKE-Dlqoe9TY.js +84 -0
- package/client/assets/rough.esm-CSKSodPl.js +1 -0
- package/client/assets/rough.esm-wVmwlXu7-CSKSodPl.js +1 -0
- package/client/assets/runtime-protocol-vue-WG3JLsjz.js +1 -0
- package/client/assets/runtime-vue-lHsY5zMj.js +1 -0
- package/client/assets/sankeyDiagram-HTMAVEWB-BkTKgu8Q.js +40 -0
- package/client/assets/sankeyDiagram-HTMAVEWB-rWXPf03Z-CjV9pCSJ.js +40 -0
- package/client/assets/{schemas-D_RbFtuQ.js → schemas-DGvl73AE.js} +3 -3
- package/client/assets/sequenceDiagram-DBY2YBRQ-BpuXUcFy.js +162 -0
- package/client/assets/sequenceDiagram-DBY2YBRQ-nkJYWO2m-BqS_sTCH.js +162 -0
- package/client/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- package/client/assets/sizeCapture-X5ZJPWSS-F5SadAuT-VHIb4_NU.js +1 -0
- package/client/assets/src-Brzfja-q-DNc4Or4z.js +1 -0
- package/client/assets/src-Cqa3Jy1j.js +1 -0
- package/client/assets/stateDiagram-2N3HPSRC-CdlGXuo7.js +1 -0
- package/client/assets/stateDiagram-2N3HPSRC-T4-clK8b-DQRiB4va.js +1 -0
- package/client/assets/stateDiagram-v2-6OUMAXLB-BN8Jr3ZM.js +1 -0
- package/client/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-DbrOhUn_.js +1 -0
- package/client/assets/swimlanes-5IMT3BWC-HmQNEntu-CNBgZdur.js +2 -0
- package/client/assets/swimlanes-5IMT3BWC-iEJg5gki.js +2 -0
- package/client/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-DpqgeNRA.js +8 -0
- package/client/assets/swimlanesDiagram-G3AALYLV-o-sS6aEP.js +8 -0
- package/client/assets/timeline-definition-FHXFAJF6-Bjh-7QmL.js +120 -0
- package/client/assets/timeline-definition-FHXFAJF6-CNc9jSTP-BKNSymgY.js +120 -0
- package/client/assets/treeView-QDETBFTQ-D3cqRl6k.js +1 -0
- package/client/assets/treeView-QDETBFTQ-nIQcG1h9-O_ri9HxD.js +1 -0
- package/client/assets/treemap-6X3UGDF4-BnsvC8yL-Cfsty1e3.js +1 -0
- package/client/assets/treemap-6X3UGDF4-DGkZcvDG.js +1 -0
- package/client/assets/v4-B29ayslu.js +1 -0
- package/client/assets/vennDiagram-L72KCM5P-Cz3dbe7b.js +34 -0
- package/client/assets/vennDiagram-L72KCM5P-Dr3pTJ_0-C38xwcVo.js +34 -0
- package/client/assets/vue-C6d2VveO.js +1 -0
- package/client/assets/vue-i18n-CL-ejuNu.js +3 -0
- package/client/assets/vue.runtime.esm-bundler-BeoTfMNc.js +4 -0
- package/client/assets/wardley-OPB4EBWU-8Odxkx6V-CCDIVuBs.js +1 -0
- package/client/assets/wardley-OPB4EBWU-CZsCDvKk.js +1 -0
- package/client/assets/wardleyDiagram-EHGQE667-DSFc7ZZa-mlWXRiWx.js +78 -0
- package/client/assets/wardleyDiagram-EHGQE667-DiyoyO_X.js +78 -0
- package/client/assets/xychartDiagram-FW5EYKEG-BP0Nn4Pp-DRRiStGZ.js +7 -0
- package/client/assets/xychartDiagram-FW5EYKEG-DhY1_UGM.js +7 -0
- package/client/index.html +16 -12
- package/package.json +17 -19
- package/server/agent/backend/types.ts +1 -1
- package/server/agent/backgroundSessions.ts +43 -0
- package/server/agent/config.ts +3 -2
- package/server/agent/mcp-tools/manageCollection.ts +4 -4
- package/server/agent/sandboxMounts.ts +1 -1
- package/server/agent/stream.ts +1 -3
- package/server/api/auth/viewToken.ts +1 -1
- package/server/api/bridge/sessionRole.ts +47 -0
- package/server/api/routes/agent.ts +79 -2
- package/server/api/routes/collections.ts +60 -7
- package/server/api/routes/collectionsRegistry.ts +113 -0
- package/server/api/routes/dashboard.ts +47 -0
- package/server/api/routes/feeds.ts +2 -2
- package/server/api/routes/files.ts +1 -2
- package/server/api/routes/pdf.ts +48 -9
- package/server/api/routes/plugins.ts +2 -2
- package/server/api/routes/remoteHost.ts +55 -0
- package/server/api/routes/sessions.ts +20 -11
- package/server/api/routes/share.ts +105 -0
- package/server/api/routes/transcribe.ts +1 -1
- package/server/api/routes/wiki/history.ts +2 -2
- package/server/api/routes/wiki.ts +29 -177
- package/server/api/sandboxStatus.ts +1 -1
- package/server/build/dispatcher.mjs +25 -14928
- package/server/events/collection-change.ts +4 -4
- package/server/events/file-change.ts +2 -2
- package/server/events/scheduler-adapter.ts +3 -3
- package/server/events/task-manager/index.ts +3 -3
- package/server/index.ts +53 -17
- package/server/notifier/engine.ts +3 -3
- package/server/notifier/types.ts +2 -2
- package/server/plugins/html-builtin.ts +15 -1
- package/server/prompts/system/system.md +4 -0
- package/server/remoteHost/auth.ts +29 -0
- package/server/remoteHost/commandChannel.ts +56 -0
- package/server/remoteHost/firebase.ts +16 -0
- package/server/remoteHost/handlers/collectionPage.ts +29 -0
- package/server/remoteHost/handlers/getCollection.ts +32 -0
- package/server/remoteHost/handlers/getFeed.ts +35 -0
- package/server/remoteHost/handlers/index.ts +19 -0
- package/server/remoteHost/handlers/listCollections.ts +29 -0
- package/server/remoteHost/handlers/listFeeds.ts +37 -0
- package/server/remoteHost/handlers/listShortcuts.ts +24 -0
- package/server/remoteHost/handlers/startChat.ts +78 -0
- package/server/remoteHost/hostRunner.ts +122 -0
- package/server/remoteHost/index.ts +117 -0
- package/server/system/config.ts +1 -1
- package/server/system/credentials.ts +2 -3
- package/server/system/docker.ts +9 -8
- package/server/system/env.ts +8 -0
- package/server/system/whisper/index.ts +4 -4
- package/server/utils/claudeConfigPath.ts +57 -0
- package/server/utils/files/dashboard-io.ts +87 -0
- package/server/utils/files/session-io.ts +11 -0
- package/server/utils/markdown/frontmatter.ts +8 -4
- package/server/utils/regex.ts +4 -3
- package/server/utils/share/packHtml.ts +108 -0
- package/server/utils/share/rewriteAssets.ts +178 -0
- package/server/workspace/chat-index/paths.ts +8 -2
- package/server/workspace/chat-index/summarizer.ts +23 -15
- package/server/workspace/collections/configure.ts +5 -3
- package/server/workspace/collections/index.ts +8 -6
- package/server/workspace/collections/notifications.ts +3 -3
- package/server/workspace/collections/types.ts +2 -2
- package/server/workspace/collections/watcher.ts +2 -2
- package/server/workspace/feeds/configure.ts +24 -0
- package/server/workspace/hooks/handlers/skillBridge.ts +2 -2
- package/server/workspace/hooks/handlers/wikiSnapshot.ts +1 -1
- package/server/workspace/paths.ts +8 -1
- package/server/workspace/skills/catalog.ts +1 -3
- package/server/workspace/skills/external/catalog.ts +1 -3
- package/server/workspace/skills/paths.ts +4 -3
- package/server/workspace/skills/writer.ts +1 -4
- package/server/workspace/skills-preset.ts +3 -3
- package/server/workspace/wiki-pages/io.ts +2 -2
- package/server/workspace/workspace.ts +2 -2
- package/src/App.vue +41 -18
- package/src/components/ChatInput.vue +1 -1
- package/src/components/DashboardView.vue +372 -0
- package/src/components/FileContentRenderer.vue +32 -7
- package/src/components/JsonEditor.vue +1 -1
- package/src/components/PluginLauncher.vue +122 -79
- package/src/components/RemoteHostControl.vue +159 -0
- package/src/components/SessionCountBadges.vue +32 -0
- package/src/components/SessionHeaderControls.vue +1 -8
- package/src/components/SessionHistoryPanel.vue +2 -0
- package/src/components/SessionHistoryToggleButton.vue +1 -15
- package/src/components/SidebarHeader.vue +2 -0
- package/src/components/collectionTypes.ts +2 -2
- package/src/composables/accountingHost.ts +24 -0
- package/src/composables/collections/uiHost.ts +17 -2
- package/src/composables/useDashboard.ts +191 -0
- package/src/composables/useMarkdownZip.ts +51 -0
- package/src/composables/useNotifications.ts +1 -4
- package/src/composables/useSharePack.ts +60 -0
- package/src/composables/useTranslatedQueries.ts +15 -95
- package/src/composables/useTranslatedStrings.ts +68 -0
- package/src/composables/useVoiceInput.ts +3 -3
- package/src/config/apiRoutes.ts +57 -9
- package/src/config/createFilePolicy.ts +1 -1
- package/src/config/firebase.ts +12 -0
- package/src/config/firebaseConfig.ts +18 -0
- package/src/config/historyFilters.ts +7 -0
- package/src/config/pubsubChannels.ts +7 -27
- package/src/config/roles.ts +12 -10
- package/src/config/workspacePaths.ts +10 -0
- package/src/index.css +1 -1
- package/src/lang/de.ts +39 -220
- package/src/lang/en.ts +39 -191
- package/src/lang/es.ts +39 -218
- package/src/lang/fr.ts +39 -220
- package/src/lang/ja.ts +39 -216
- package/src/lang/ko.ts +39 -216
- package/src/lang/pt-BR.ts +39 -217
- package/src/lang/zh.ts +39 -215
- package/src/main.ts +3 -0
- package/src/plugins/accounting/definition.ts +12 -8
- package/src/plugins/accounting/index.ts +7 -4
- package/src/plugins/accounting/meta.ts +23 -60
- package/src/plugins/manageSkills/View.vue +9 -1
- package/src/plugins/photoLocations/View.vue +2 -2
- package/src/plugins/presentCollection/definition.ts +2 -2
- package/src/plugins/presentCollection/plugin.ts +2 -2
- package/src/plugins/presentCollection/types.ts +2 -2
- package/src/plugins/presentHtml/definition.ts +1 -1
- package/src/plugins/presentMulmoScript/View.vue +2 -2
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/presentSVG/View.vue +2 -2
- package/src/plugins/scheduler/TasksTab.vue +1 -2
- package/src/plugins/skill/View.vue +7 -3
- package/src/plugins/spreadsheet/View.vue +3 -3
- package/src/plugins/spreadsheet/engine/functions/logical.ts +8 -1
- package/src/plugins/textResponse/View.vue +43 -16
- package/src/plugins/wiki/View.vue +29 -2
- package/src/plugins/wiki/components/WikiGraphView.vue +1 -1
- package/src/plugins/wiki/components/WikiPageBody.vue +4 -1
- package/src/plugins/wiki/helpers.ts +13 -81
- package/src/plugins/wiki/index.ts +1 -1
- package/src/router/guards.ts +1 -1
- package/src/router/index.ts +12 -0
- package/src/router/pageRoutes.ts +2 -0
- package/src/types/dashboard.ts +40 -0
- package/src/types/session.ts +3 -0
- package/src/utils/blobDownload.ts +19 -0
- package/src/utils/collections/presentSeed.ts +1 -1
- package/src/utils/html/customViewSrcdoc.ts +31 -3
- package/src/utils/html/previewCsp.ts +1 -1
- package/src/utils/id.ts +1 -1
- package/src/utils/image/htmlSrcAttrs.ts +1 -1
- package/src/utils/markdown/frontmatter.ts +12 -7
- package/src/utils/markdown/highlight.ts +22 -0
- package/src/utils/markdown/marpCustomSize.ts +2 -2
- package/src/utils/markdown/mermaidExtension.ts +56 -0
- package/src/utils/markdown/mermaidRender.ts +147 -0
- package/src/utils/markdown/setup.ts +17 -4
- package/src/utils/markdown/useMermaid.ts +33 -0
- package/src/utils/session/longRunning.ts +33 -0
- package/src/utils/session/mergeSessions.ts +1 -1
- package/client/assets/JsonEditor-Di5xGeZY.css +0 -1
- package/client/assets/JsonEditor-o5--tPQH.js +0 -10
- package/client/assets/index-tOu5ArRZ.css +0 -2
- package/client/assets/lib-D6Xy0IFc.js +0 -114
- package/client/assets/marp-D6GXA-EB.js +0 -3452
- package/client/assets/material-symbols-outlined-DtIK7AQn.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-pU0Mw7Zm.js +0 -1
- package/client/assets/runtime-vue-fFYhnNg3.js +0 -1
- package/client/assets/vue-UDIWDtr8.js +0 -1
- package/client/assets/vue-i18n-CQbxVmNs.js +0 -3
- package/client/assets/vue.runtime.esm-bundler-BTyIdNAI.js +0 -4
- package/server/accounting/accountNormalize.ts +0 -32
- package/server/accounting/defaultAccounts.ts +0 -87
- package/server/accounting/eventPublisher.ts +0 -52
- package/server/accounting/journal.ts +0 -252
- package/server/accounting/openingBalances.ts +0 -114
- package/server/accounting/report.ts +0 -237
- package/server/accounting/service.ts +0 -718
- package/server/accounting/snapshotCache.ts +0 -334
- package/server/accounting/timeSeries.ts +0 -265
- package/server/accounting/types.ts +0 -148
- package/server/api/routes/accounting.ts +0 -373
- package/server/api/routes/wiki/frontmatter.ts +0 -34
- package/server/api/routes/wiki/pageIndex.ts +0 -53
- package/server/system/logs/aaa +0 -737
- package/server/system/logs/bb +0 -446
- package/server/utils/files/accounting-io.ts +0 -294
- package/server/workspace/feeds/engine.ts +0 -143
- package/server/workspace/feeds/fetch/httpClient.ts +0 -127
- package/server/workspace/feeds/fetch/rssParser.ts +0 -117
- package/server/workspace/feeds/index.ts +0 -8
- package/server/workspace/feeds/ingestTypes.ts +0 -62
- package/server/workspace/feeds/pathResolver.ts +0 -74
- package/server/workspace/feeds/paths.ts +0 -30
- package/server/workspace/feeds/projectItem.ts +0 -92
- package/server/workspace/feeds/registry.ts +0 -43
- package/server/workspace/feeds/retrievers/httpJson.ts +0 -19
- package/server/workspace/feeds/retrievers/index.ts +0 -27
- package/server/workspace/feeds/retrievers/registerAll.ts +0 -5
- package/server/workspace/feeds/retrievers/rss.ts +0 -24
- package/server/workspace/feeds/state.ts +0 -55
- package/src/composables/useAccountingChannel.ts +0 -58
- package/src/lib/wiki-page/graph.ts +0 -108
- package/src/lib/wiki-page/index-parse.ts +0 -221
- package/src/lib/wiki-page/link.ts +0 -62
- package/src/lib/wiki-page/lint.ts +0 -105
- package/src/lib/wiki-page/paths.ts +0 -35
- package/src/lib/wiki-page/slug.ts +0 -54
- package/src/plugins/accounting/Preview.vue +0 -103
- package/src/plugins/accounting/View.vue +0 -633
- package/src/plugins/accounting/actions.ts +0 -34
- package/src/plugins/accounting/api.ts +0 -301
- package/src/plugins/accounting/components/AccountEditor.vue +0 -250
- package/src/plugins/accounting/components/AccountRow.vue +0 -50
- package/src/plugins/accounting/components/AccountsList.vue +0 -102
- package/src/plugins/accounting/components/AccountsModal.vue +0 -301
- package/src/plugins/accounting/components/BalanceSheet.vue +0 -186
- package/src/plugins/accounting/components/BookSettings.vue +0 -284
- package/src/plugins/accounting/components/BookSwitcher.vue +0 -78
- package/src/plugins/accounting/components/DateRangePicker.vue +0 -140
- package/src/plugins/accounting/components/JournalEntryForm.vue +0 -505
- package/src/plugins/accounting/components/JournalList.vue +0 -554
- package/src/plugins/accounting/components/Ledger.vue +0 -206
- package/src/plugins/accounting/components/NewBookForm.vue +0 -211
- package/src/plugins/accounting/components/OpeningBalancesForm.vue +0 -272
- package/src/plugins/accounting/components/ProfitLoss.vue +0 -160
- package/src/plugins/accounting/components/accountDraft.ts +0 -13
- package/src/plugins/accounting/components/accountNumbering.ts +0 -103
- package/src/plugins/accounting/components/accountValidation.ts +0 -75
- package/src/plugins/accounting/components/useLatestRequest.ts +0 -44
- package/src/plugins/accounting/countries.ts +0 -158
- package/src/plugins/accounting/currencies.ts +0 -77
- package/src/plugins/accounting/dates.ts +0 -51
- package/src/plugins/accounting/fiscalYear.ts +0 -136
- package/src/plugins/accounting/timeSeriesEnums.ts +0 -16
- package/src/plugins/wiki/route.ts +0 -137
- /package/client/assets/{_plugin-vue_export-helper-B67ILkmu.js → _plugin-vue_export-helper-BDNMzG2s.js} +0 -0
- /package/client/assets/{purify.es-B27wDFIb-51iYcXuK.js → purify.es-B27wDFIb-Bu4Grnl0.js} +0 -0
- /package/client/assets/{typeof-DBp4T-Ny-z2wCIsir.js → typeof-DBp4T-Ny-B5XbjTb1.js} +0 -0
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<form class="flex flex-col gap-3" data-testid="accounting-opening-form" @submit.prevent="onSubmit">
|
|
3
|
-
<div class="flex items-center justify-between gap-2">
|
|
4
|
-
<h3 class="text-base font-semibold">{{ t("pluginAccounting.openingForm.title") }}</h3>
|
|
5
|
-
<button
|
|
6
|
-
type="button"
|
|
7
|
-
class="h-8 px-2.5 flex items-center gap-1 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50"
|
|
8
|
-
data-testid="accounting-opening-manage-accounts"
|
|
9
|
-
@click="showAccountsModal = true"
|
|
10
|
-
>
|
|
11
|
-
<span class="material-icons text-base">tune</span>
|
|
12
|
-
<span>{{ t("pluginAccounting.accounts.manageButton") }}</span>
|
|
13
|
-
</button>
|
|
14
|
-
</div>
|
|
15
|
-
<p class="text-xs text-gray-500">{{ t("pluginAccounting.openingForm.explainer") }}</p>
|
|
16
|
-
<p class="text-xs text-blue-600" data-testid="accounting-opening-empty-hint">{{ t("pluginAccounting.openingForm.emptyHint") }}</p>
|
|
17
|
-
<div v-if="existing" class="text-xs text-gray-500" data-testid="accounting-opening-existing">
|
|
18
|
-
{{ t("pluginAccounting.openingForm.setBy", { date: existing.date }) }}
|
|
19
|
-
<span v-if="existing" class="text-amber-600 ml-2">{{ t("pluginAccounting.openingForm.replaceWarning") }}</span>
|
|
20
|
-
</div>
|
|
21
|
-
<p v-else class="text-xs text-gray-400" data-testid="accounting-opening-none">{{ t("pluginAccounting.openingForm.none") }}</p>
|
|
22
|
-
<label class="text-xs text-gray-500 flex flex-col gap-1 w-fit">
|
|
23
|
-
{{ t("pluginAccounting.openingForm.asOfLabel") }}
|
|
24
|
-
<input v-model="asOfDate" type="date" required class="h-8 px-2 rounded border border-gray-300 text-sm" data-testid="accounting-opening-asof" />
|
|
25
|
-
</label>
|
|
26
|
-
<table class="w-full text-sm">
|
|
27
|
-
<thead>
|
|
28
|
-
<tr class="text-xs text-gray-500 border-b border-gray-200">
|
|
29
|
-
<th class="text-left py-1 px-2">{{ t("pluginAccounting.entryForm.accountLabel") }}</th>
|
|
30
|
-
<th class="text-right py-1 px-2 w-32">{{ t("pluginAccounting.entryForm.debitLabel") }}</th>
|
|
31
|
-
<th class="text-right py-1 px-2 w-32">{{ t("pluginAccounting.entryForm.creditLabel") }}</th>
|
|
32
|
-
</tr>
|
|
33
|
-
</thead>
|
|
34
|
-
<tbody>
|
|
35
|
-
<tr v-for="account in bsAccounts" :key="account.code" class="border-b border-gray-100">
|
|
36
|
-
<td class="py-1 px-2">
|
|
37
|
-
<span class="font-mono text-[10px] text-gray-400 mr-2">{{ account.code }}</span>
|
|
38
|
-
<span>{{ account.name }}</span>
|
|
39
|
-
<span class="ml-2 text-xs text-gray-400">{{ account.type }}</span>
|
|
40
|
-
</td>
|
|
41
|
-
<td class="py-1 px-2">
|
|
42
|
-
<input
|
|
43
|
-
v-model.number="rows[account.code].debit"
|
|
44
|
-
type="number"
|
|
45
|
-
:step="step"
|
|
46
|
-
min="0"
|
|
47
|
-
class="h-8 px-2 w-full rounded border border-gray-300 text-sm text-right"
|
|
48
|
-
:data-testid="`accounting-opening-debit-${account.code}`"
|
|
49
|
-
@input="onDebitInput(account.code)"
|
|
50
|
-
/>
|
|
51
|
-
</td>
|
|
52
|
-
<td class="py-1 px-2">
|
|
53
|
-
<input
|
|
54
|
-
v-model.number="rows[account.code].credit"
|
|
55
|
-
type="number"
|
|
56
|
-
:step="step"
|
|
57
|
-
min="0"
|
|
58
|
-
class="h-8 px-2 w-full rounded border border-gray-300 text-sm text-right"
|
|
59
|
-
:data-testid="`accounting-opening-credit-${account.code}`"
|
|
60
|
-
@input="onCreditInput(account.code)"
|
|
61
|
-
/>
|
|
62
|
-
</td>
|
|
63
|
-
</tr>
|
|
64
|
-
</tbody>
|
|
65
|
-
</table>
|
|
66
|
-
<div class="flex items-center justify-between">
|
|
67
|
-
<span class="text-xs text-gray-400">{{ t("pluginAccounting.openingForm.explainer2") }}</span>
|
|
68
|
-
<span :class="balanced ? 'text-green-600' : 'text-red-500'" class="text-xs" data-testid="accounting-opening-balance">
|
|
69
|
-
{{ balanced ? t("pluginAccounting.entryForm.balanced") : t("pluginAccounting.entryForm.imbalance", { amount: imbalanceText }) }}
|
|
70
|
-
</span>
|
|
71
|
-
</div>
|
|
72
|
-
<p v-if="error" class="text-xs text-red-500" data-testid="accounting-opening-error">{{ error }}</p>
|
|
73
|
-
<p v-if="successMessage" class="text-xs text-green-600" data-testid="accounting-opening-success">{{ successMessage }}</p>
|
|
74
|
-
<div class="flex justify-end">
|
|
75
|
-
<button
|
|
76
|
-
type="submit"
|
|
77
|
-
class="h-8 px-3 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm disabled:opacity-50"
|
|
78
|
-
:disabled="!balanced || submitting"
|
|
79
|
-
data-testid="accounting-opening-submit"
|
|
80
|
-
>
|
|
81
|
-
{{ submitting ? t("pluginAccounting.entryForm.submitting") : t("pluginAccounting.openingForm.submit") }}
|
|
82
|
-
</button>
|
|
83
|
-
</div>
|
|
84
|
-
</form>
|
|
85
|
-
<!-- Sibling of the parent <form> on purpose: the modal renders
|
|
86
|
-
its own <form @submit.prevent> for the inline editor, and
|
|
87
|
-
nesting <form>s is invalid HTML that breaks Enter-key submit
|
|
88
|
-
routing in some browsers. Vue 3 multi-root templates let us
|
|
89
|
-
keep the markup flat with no wrapper div. -->
|
|
90
|
-
<AccountsModal v-if="showAccountsModal" :book-id="bookId" :accounts="accounts" @close="showAccountsModal = false" />
|
|
91
|
-
</template>
|
|
92
|
-
|
|
93
|
-
<script setup lang="ts">
|
|
94
|
-
import { computed, ref, watch } from "vue";
|
|
95
|
-
import { useI18n } from "vue-i18n";
|
|
96
|
-
import { getOpeningBalances, setOpeningBalances, type Account, type JournalEntry, type JournalLine } from "../api";
|
|
97
|
-
import { formatAmount, inputStepFor } from "../currencies";
|
|
98
|
-
import { localDateString } from "../dates";
|
|
99
|
-
import { useLatestRequest } from "./useLatestRequest";
|
|
100
|
-
import AccountsModal from "./AccountsModal.vue";
|
|
101
|
-
import { errorMessage } from "../../../utils/errors";
|
|
102
|
-
|
|
103
|
-
const { t } = useI18n();
|
|
104
|
-
|
|
105
|
-
const props = defineProps<{ bookId: string; accounts: Account[]; currency: string; version: number }>();
|
|
106
|
-
const emit = defineEmits<{ submitted: [] }>();
|
|
107
|
-
|
|
108
|
-
const showAccountsModal = ref(false);
|
|
109
|
-
|
|
110
|
-
interface OpeningRow {
|
|
111
|
-
debit: number | null;
|
|
112
|
-
credit: number | null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const asOfDate = ref(localDateString());
|
|
116
|
-
const rows = ref<Record<string, OpeningRow>>({});
|
|
117
|
-
const existing = ref<JournalEntry | null>(null);
|
|
118
|
-
const submitting = ref(false);
|
|
119
|
-
const error = ref<string | null>(null);
|
|
120
|
-
const successMessage = ref<string | null>(null);
|
|
121
|
-
const { begin: beginLoad, isCurrent: isCurrentLoad } = useLatestRequest();
|
|
122
|
-
|
|
123
|
-
const bsAccounts = computed(() =>
|
|
124
|
-
props.accounts.filter((account) => (account.type === "asset" || account.type === "liability" || account.type === "equity") && account.active !== false),
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
function ensureRows(): void {
|
|
128
|
-
for (const account of bsAccounts.value) {
|
|
129
|
-
if (!rows.value[account.code]) rows.value[account.code] = { debit: null, credit: null };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function onDebitInput(code: string): void {
|
|
134
|
-
const row = rows.value[code];
|
|
135
|
-
if (row.debit !== null && row.debit !== 0) row.credit = null;
|
|
136
|
-
}
|
|
137
|
-
function onCreditInput(code: string): void {
|
|
138
|
-
const row = rows.value[code];
|
|
139
|
-
if (row.credit !== null && row.credit !== 0) row.debit = null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const imbalance = computed<number>(() => {
|
|
143
|
-
// Iterate the live bsAccounts (already active-filtered) rather
|
|
144
|
-
// than rows.value keys so a row for a now-inactive account
|
|
145
|
-
// doesn't tilt `balanced` against what `toApiLines` will
|
|
146
|
-
// actually post.
|
|
147
|
-
let sum = 0;
|
|
148
|
-
for (const account of bsAccounts.value) {
|
|
149
|
-
const row = rows.value[account.code];
|
|
150
|
-
if (!row) continue;
|
|
151
|
-
if (typeof row.debit === "number") sum += row.debit;
|
|
152
|
-
if (typeof row.credit === "number") sum -= row.credit;
|
|
153
|
-
}
|
|
154
|
-
return sum;
|
|
155
|
-
});
|
|
156
|
-
// An all-empty form is valid: it submits as a zero-line opening
|
|
157
|
-
// marker so the user can unlock the rest of the UI without
|
|
158
|
-
// committing to specific balances on day one.
|
|
159
|
-
const balanced = computed(() => Math.abs(imbalance.value) <= 0.005);
|
|
160
|
-
const imbalanceText = computed(() => formatAmount(imbalance.value, props.currency));
|
|
161
|
-
const step = computed(() => inputStepFor(props.currency));
|
|
162
|
-
|
|
163
|
-
function isPositiveAmount(value: unknown): value is number {
|
|
164
|
-
// Robust against the empty string `v-model.number` produces when
|
|
165
|
-
// the user clears a previously-typed field — without this, the
|
|
166
|
-
// skip condition `value === 0` was false for `""` and the form
|
|
167
|
-
// emitted ghost lines like `{accountCode: "3000"}` with no
|
|
168
|
-
// amount on either side.
|
|
169
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function toApiLines(): JournalLine[] {
|
|
173
|
-
const out: JournalLine[] = [];
|
|
174
|
-
// Iterate the visible bsAccounts list (which already filters
|
|
175
|
-
// out inactive accounts) rather than `rows.value` keys. A row
|
|
176
|
-
// for an account that was active when the user typed amounts
|
|
177
|
-
// and then got deactivated mid-edit would otherwise still post —
|
|
178
|
-
// the row stays in the map even after the v-for stops rendering
|
|
179
|
-
// it, so iterating keys would silently land entries on a
|
|
180
|
-
// soft-deleted account.
|
|
181
|
-
for (const account of bsAccounts.value) {
|
|
182
|
-
const row = rows.value[account.code];
|
|
183
|
-
if (!row) continue;
|
|
184
|
-
const debitOk = isPositiveAmount(row.debit);
|
|
185
|
-
const creditOk = isPositiveAmount(row.credit);
|
|
186
|
-
if (!debitOk && !creditOk) continue;
|
|
187
|
-
const line: JournalLine = { accountCode: account.code };
|
|
188
|
-
if (debitOk) line.debit = row.debit as number;
|
|
189
|
-
if (creditOk) line.credit = row.credit as number;
|
|
190
|
-
out.push(line);
|
|
191
|
-
}
|
|
192
|
-
return out;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function freshRows(): Record<string, OpeningRow> {
|
|
196
|
-
const out: Record<string, OpeningRow> = {};
|
|
197
|
-
for (const account of bsAccounts.value) out[account.code] = { debit: null, credit: null };
|
|
198
|
-
return out;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function loadExisting(): Promise<void> {
|
|
202
|
-
// Always start from a fresh row map so a book without an
|
|
203
|
-
// opening doesn't inherit the previous book's draft values.
|
|
204
|
-
const token = beginLoad();
|
|
205
|
-
const next = freshRows();
|
|
206
|
-
const result = await getOpeningBalances(props.bookId);
|
|
207
|
-
// Drop the result if the user has switched books since this
|
|
208
|
-
// call started — otherwise stale rows would land on the new
|
|
209
|
-
// book's form.
|
|
210
|
-
if (!isCurrentLoad(token)) return;
|
|
211
|
-
if (!result.ok) {
|
|
212
|
-
existing.value = null;
|
|
213
|
-
rows.value = next;
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
existing.value = result.data.opening;
|
|
217
|
-
if (result.data.opening) {
|
|
218
|
-
asOfDate.value = result.data.opening.date;
|
|
219
|
-
for (const line of result.data.opening.lines) {
|
|
220
|
-
next[line.accountCode] = { debit: line.debit ?? null, credit: line.credit ?? null };
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
asOfDate.value = localDateString();
|
|
224
|
-
}
|
|
225
|
-
rows.value = next;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function onSubmit(): Promise<void> {
|
|
229
|
-
if (submitting.value || !balanced.value) return;
|
|
230
|
-
submitting.value = true;
|
|
231
|
-
error.value = null;
|
|
232
|
-
successMessage.value = null;
|
|
233
|
-
try {
|
|
234
|
-
const result = await setOpeningBalances({ bookId: props.bookId, asOfDate: asOfDate.value, lines: toApiLines() });
|
|
235
|
-
if (!result.ok) {
|
|
236
|
-
error.value = result.error;
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
successMessage.value = t("pluginAccounting.openingForm.success");
|
|
240
|
-
emit("submitted");
|
|
241
|
-
} catch (err) {
|
|
242
|
-
error.value = errorMessage(err);
|
|
243
|
-
} finally {
|
|
244
|
-
submitting.value = false;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
watch(
|
|
249
|
-
() => [props.bookId, props.version, props.accounts.length],
|
|
250
|
-
() => {
|
|
251
|
-
ensureRows();
|
|
252
|
-
void loadExisting();
|
|
253
|
-
},
|
|
254
|
-
{ immediate: true },
|
|
255
|
-
);
|
|
256
|
-
</script>
|
|
257
|
-
|
|
258
|
-
<style scoped>
|
|
259
|
-
/* Hide the WebKit / Firefox spin buttons on amount inputs. The
|
|
260
|
-
step attribute still controls validation; this is purely UI.
|
|
261
|
-
Accounting amount fields don't benefit from a spinner — users
|
|
262
|
-
type the number and the up/down arrows just clutter the row. */
|
|
263
|
-
input[type="number"]::-webkit-outer-spin-button,
|
|
264
|
-
input[type="number"]::-webkit-inner-spin-button {
|
|
265
|
-
-webkit-appearance: none;
|
|
266
|
-
margin: 0;
|
|
267
|
-
}
|
|
268
|
-
input[type="number"] {
|
|
269
|
-
-moz-appearance: textfield;
|
|
270
|
-
appearance: textfield;
|
|
271
|
-
}
|
|
272
|
-
</style>
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="flex flex-col gap-3" data-testid="accounting-profit-loss">
|
|
3
|
-
<div class="flex flex-wrap items-end gap-3">
|
|
4
|
-
<DateRangePicker v-model="range" :fiscal-year-end="resolvedFiscalYearEnd" :opening-date="openingDate" />
|
|
5
|
-
<button class="h-8 px-2.5 rounded border border-gray-300 text-sm text-gray-600 hover:bg-gray-50" @click="refresh">
|
|
6
|
-
<span class="material-icons text-base align-middle">refresh</span>
|
|
7
|
-
</button>
|
|
8
|
-
</div>
|
|
9
|
-
<p v-if="loading" class="text-xs text-gray-400">{{ t("pluginAccounting.common.loading") }}</p>
|
|
10
|
-
<p v-else-if="error" class="text-xs text-red-500">{{ t("pluginAccounting.common.error", { error }) }}</p>
|
|
11
|
-
<template v-else-if="profitLoss">
|
|
12
|
-
<section class="border border-gray-200 rounded p-3">
|
|
13
|
-
<h4 class="text-sm font-semibold mb-2">{{ t("pluginAccounting.profitLoss.income") }}</h4>
|
|
14
|
-
<table class="w-full text-sm">
|
|
15
|
-
<tbody>
|
|
16
|
-
<tr
|
|
17
|
-
v-for="row in profitLoss.income.rows"
|
|
18
|
-
:key="row.accountCode"
|
|
19
|
-
class="border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
|
|
20
|
-
tabindex="0"
|
|
21
|
-
role="button"
|
|
22
|
-
:aria-label="t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })"
|
|
23
|
-
:data-testid="`accounting-pl-row-${row.accountCode}`"
|
|
24
|
-
@click="onRowClick(row.accountCode)"
|
|
25
|
-
@keydown.enter.prevent.self="onKeyActivate($event, row.accountCode)"
|
|
26
|
-
@keydown.space.prevent.self="onKeyActivate($event, row.accountCode)"
|
|
27
|
-
>
|
|
28
|
-
<td class="py-1 px-1">
|
|
29
|
-
<span class="font-mono text-[10px] text-gray-400 mr-2">{{ row.accountCode }}</span
|
|
30
|
-
>{{ row.accountName }}
|
|
31
|
-
</td>
|
|
32
|
-
<td class="py-1 px-1 text-right font-mono">{{ formatAmount(row.amount) }}</td>
|
|
33
|
-
</tr>
|
|
34
|
-
</tbody>
|
|
35
|
-
<tfoot>
|
|
36
|
-
<tr class="font-semibold border-t border-gray-300">
|
|
37
|
-
<td class="py-1 px-1">{{ t("pluginAccounting.balanceSheet.total") }}</td>
|
|
38
|
-
<td class="py-1 px-1 text-right">{{ formatAmount(profitLoss.income.total) }}</td>
|
|
39
|
-
</tr>
|
|
40
|
-
</tfoot>
|
|
41
|
-
</table>
|
|
42
|
-
</section>
|
|
43
|
-
<section class="border border-gray-200 rounded p-3">
|
|
44
|
-
<h4 class="text-sm font-semibold mb-2">{{ t("pluginAccounting.profitLoss.expense") }}</h4>
|
|
45
|
-
<table class="w-full text-sm">
|
|
46
|
-
<tbody>
|
|
47
|
-
<tr
|
|
48
|
-
v-for="row in profitLoss.expense.rows"
|
|
49
|
-
:key="row.accountCode"
|
|
50
|
-
class="border-b border-gray-100 hover:bg-blue-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
|
|
51
|
-
tabindex="0"
|
|
52
|
-
role="button"
|
|
53
|
-
:aria-label="t('pluginAccounting.accounts.openLedgerAria', { code: row.accountCode, name: row.accountName })"
|
|
54
|
-
:data-testid="`accounting-pl-row-${row.accountCode}`"
|
|
55
|
-
@click="onRowClick(row.accountCode)"
|
|
56
|
-
@keydown.enter.prevent.self="onKeyActivate($event, row.accountCode)"
|
|
57
|
-
@keydown.space.prevent.self="onKeyActivate($event, row.accountCode)"
|
|
58
|
-
>
|
|
59
|
-
<td class="py-1 px-1">
|
|
60
|
-
<span class="font-mono text-[10px] text-gray-400 mr-2">{{ row.accountCode }}</span
|
|
61
|
-
>{{ row.accountName }}
|
|
62
|
-
</td>
|
|
63
|
-
<td class="py-1 px-1 text-right font-mono">{{ formatAmount(row.amount) }}</td>
|
|
64
|
-
</tr>
|
|
65
|
-
</tbody>
|
|
66
|
-
<tfoot>
|
|
67
|
-
<tr class="font-semibold border-t border-gray-300">
|
|
68
|
-
<td class="py-1 px-1">{{ t("pluginAccounting.balanceSheet.total") }}</td>
|
|
69
|
-
<td class="py-1 px-1 text-right">{{ formatAmount(profitLoss.expense.total) }}</td>
|
|
70
|
-
</tr>
|
|
71
|
-
</tfoot>
|
|
72
|
-
</table>
|
|
73
|
-
</section>
|
|
74
|
-
<div class="flex justify-end items-center gap-2 text-sm font-semibold" data-testid="accounting-pl-net">
|
|
75
|
-
<span>{{ t("pluginAccounting.profitLoss.netIncome") }}</span>
|
|
76
|
-
<span :class="profitLoss.netIncome >= 0 ? 'text-green-600' : 'text-red-500'">{{ formatAmount(profitLoss.netIncome) }}</span>
|
|
77
|
-
</div>
|
|
78
|
-
</template>
|
|
79
|
-
</div>
|
|
80
|
-
</template>
|
|
81
|
-
|
|
82
|
-
<script setup lang="ts">
|
|
83
|
-
import { computed, ref, watch } from "vue";
|
|
84
|
-
import { useI18n } from "vue-i18n";
|
|
85
|
-
import { getProfitLoss, type ProfitLoss } from "../api";
|
|
86
|
-
import { formatAmount as formatAmountWithCurrency } from "../currencies";
|
|
87
|
-
import { currentFiscalYearRange, resolveFiscalYearEnd, type DateRange, type FiscalYearEnd } from "../fiscalYear";
|
|
88
|
-
import { useLatestRequest } from "./useLatestRequest";
|
|
89
|
-
import DateRangePicker from "./DateRangePicker.vue";
|
|
90
|
-
|
|
91
|
-
const { t } = useI18n();
|
|
92
|
-
|
|
93
|
-
const props = defineProps<{
|
|
94
|
-
bookId: string;
|
|
95
|
-
currency: string;
|
|
96
|
-
version: number;
|
|
97
|
-
fiscalYearEnd?: FiscalYearEnd;
|
|
98
|
-
/** Opening-balance date for the active book — drives the "Lifetime"
|
|
99
|
-
* shortcut in the date picker (from = openingDate, to = today).
|
|
100
|
-
* When absent, the picker hides Lifetime; "All" still works. */
|
|
101
|
-
openingDate?: string;
|
|
102
|
-
}>();
|
|
103
|
-
|
|
104
|
-
const emit = defineEmits<{ selectAccount: [code: string] }>();
|
|
105
|
-
|
|
106
|
-
const resolvedFiscalYearEnd = computed<FiscalYearEnd>(() => resolveFiscalYearEnd(props.fiscalYearEnd));
|
|
107
|
-
|
|
108
|
-
function onRowClick(code: string): void {
|
|
109
|
-
emit("selectAccount", code);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function onKeyActivate(event: KeyboardEvent, code: string): void {
|
|
113
|
-
if (event.repeat) return;
|
|
114
|
-
emit("selectAccount", code);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Default = current fiscal year. Reset by the bookId/fiscalYearEnd
|
|
118
|
-
// watcher below so switching books or changing the FY-end in
|
|
119
|
-
// settings drops a stale custom range from the prior book.
|
|
120
|
-
const range = ref<DateRange>(currentFiscalYearRange(resolvedFiscalYearEnd.value));
|
|
121
|
-
const profitLoss = ref<ProfitLoss | null>(null);
|
|
122
|
-
const loading = ref(false);
|
|
123
|
-
const error = ref<string | null>(null);
|
|
124
|
-
const { begin: beginRequest, isCurrent } = useLatestRequest();
|
|
125
|
-
|
|
126
|
-
function formatAmount(value: number): string {
|
|
127
|
-
return formatAmountWithCurrency(value, props.currency);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function refresh(): Promise<void> {
|
|
131
|
-
const token = beginRequest();
|
|
132
|
-
loading.value = true;
|
|
133
|
-
error.value = null;
|
|
134
|
-
try {
|
|
135
|
-
// P&L always sends a range. Empty-side gets a sentinel so "All"
|
|
136
|
-
// (both empty) means "every entry" rather than an empty window.
|
|
137
|
-
const fromBound = range.value.from || "0000-01-01";
|
|
138
|
-
const toBound = range.value.to || "9999-12-31";
|
|
139
|
-
const result = await getProfitLoss({ kind: "range", from: fromBound, to: toBound }, props.bookId);
|
|
140
|
-
if (!isCurrent(token)) return;
|
|
141
|
-
if (!result.ok) {
|
|
142
|
-
error.value = result.error;
|
|
143
|
-
profitLoss.value = null;
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
profitLoss.value = result.data.profitLoss;
|
|
147
|
-
} finally {
|
|
148
|
-
if (isCurrent(token)) loading.value = false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
watch(
|
|
153
|
-
() => [props.bookId, resolvedFiscalYearEnd.value],
|
|
154
|
-
() => {
|
|
155
|
-
range.value = currentFiscalYearRange(resolvedFiscalYearEnd.value);
|
|
156
|
-
},
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
watch(() => [props.bookId, props.version, range.value.from, range.value.to], refresh, { immediate: true });
|
|
160
|
-
</script>
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// Shared draft shape for the AccountsModal editor row. Lives in
|
|
2
|
-
// its own module so AccountsModal.vue and AccountEditor.vue can
|
|
3
|
-
// both type the prop / emit without one importing the other —
|
|
4
|
-
// they form a parent / child pair, not a re-export chain.
|
|
5
|
-
|
|
6
|
-
import type { AccountType } from "../api";
|
|
7
|
-
|
|
8
|
-
export interface AccountDraft {
|
|
9
|
-
code: string;
|
|
10
|
-
name: string;
|
|
11
|
-
type: AccountType;
|
|
12
|
-
note: string;
|
|
13
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// Account-code numbering convention. The chart of accounts uses
|
|
2
|
-
// 4-digit codes whose leading digit identifies the type:
|
|
3
|
-
//
|
|
4
|
-
// 1xxx → asset
|
|
5
|
-
// 2xxx → liability
|
|
6
|
-
// 3xxx → equity
|
|
7
|
-
// 4xxx → income
|
|
8
|
-
// 5xxx → expense
|
|
9
|
-
//
|
|
10
|
-
// Within those bands, the second digit `4` is reserved for tax-
|
|
11
|
-
// related accounts on both sides of the balance sheet:
|
|
12
|
-
//
|
|
13
|
-
// 14xx → tax-related current assets
|
|
14
|
-
// (1400 Input Tax Receivable / 仮払消費税, plus future
|
|
15
|
-
// withholding-tax-receivable / etc. siblings)
|
|
16
|
-
// 24xx → tax-related current liabilities
|
|
17
|
-
// (2400 Sales Tax Payable / 仮受消費税, plus future
|
|
18
|
-
// withholding-tax-payable / etc. siblings)
|
|
19
|
-
//
|
|
20
|
-
// Special-case UI (Ledger T-number column, JournalEntryForm
|
|
21
|
-
// per-line tax-registration ID input) is **input-tax-only** — it
|
|
22
|
-
// keys off `isTaxAccountCode`, which matches 14xx (purchase side)
|
|
23
|
-
// only. Output-tax / sales-side lines (24xx) intentionally don't
|
|
24
|
-
// surface a counterparty registration field: the seller's
|
|
25
|
-
// obligation is to put their *own* registration number on the
|
|
26
|
-
// invoice they issue, not to capture the customer's. So a custom
|
|
27
|
-
// suspense account added in the 14xx band participates without
|
|
28
|
-
// any opt-in step; 24xx accounts book the liability without the
|
|
29
|
-
// extra column.
|
|
30
|
-
//
|
|
31
|
-
// Lives in its own module so AccountsModal, AccountEditor, and the
|
|
32
|
-
// validation helper can share the same constants without circular
|
|
33
|
-
// imports between Vue components.
|
|
34
|
-
|
|
35
|
-
import type { Account, AccountType } from "../api";
|
|
36
|
-
|
|
37
|
-
export const ACCOUNT_TYPE_PREFIX: Record<AccountType, number> = {
|
|
38
|
-
asset: 1,
|
|
39
|
-
liability: 2,
|
|
40
|
-
equity: 3,
|
|
41
|
-
income: 4,
|
|
42
|
-
expense: 5,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const TAX_ACCOUNT_PREFIXES: readonly string[] = ["14"];
|
|
46
|
-
|
|
47
|
-
/** Returns `true` for codes whose first two digits identify a
|
|
48
|
-
* tax-related current asset (`14xx`) — i.e. the input-tax /
|
|
49
|
-
* purchase side of consumption / sales / VAT bookkeeping. Drives
|
|
50
|
-
* Ledger column visibility and the JournalEntryForm per-line
|
|
51
|
-
* tax-registration ID input. Output-tax (24xx) is intentionally
|
|
52
|
-
* excluded: the counterparty's registration ID is only
|
|
53
|
-
* load-bearing for input-tax-credit eligibility on purchases. */
|
|
54
|
-
export function isTaxAccountCode(code: string): boolean {
|
|
55
|
-
return TAX_ACCOUNT_PREFIXES.some((prefix) => code.startsWith(prefix));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const ACCOUNT_CODE_RE = /^\d{4}$/;
|
|
59
|
-
const SUGGESTED_GAP = 10;
|
|
60
|
-
|
|
61
|
-
export function isValidAccountCode(code: string): boolean {
|
|
62
|
-
return ACCOUNT_CODE_RE.test(code);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function typeForCode(code: string): AccountType | null {
|
|
66
|
-
if (!isValidAccountCode(code)) return null;
|
|
67
|
-
const leading = Number.parseInt(code[0], 10);
|
|
68
|
-
for (const [type, prefix] of Object.entries(ACCOUNT_TYPE_PREFIX) as [AccountType, number][]) {
|
|
69
|
-
if (prefix === leading) return type;
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function codeMatchesType(code: string, type: AccountType): boolean {
|
|
75
|
-
return typeForCode(code) === type;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Suggest the next free 4-digit code for `type`. Picks max-in-range
|
|
79
|
-
* + SUGGESTED_GAP so users keep room to insert sibling accounts
|
|
80
|
-
* later (the standard accounting convention). Falls back to the
|
|
81
|
-
* prefix base when the range is empty, and to max+1 when +gap would
|
|
82
|
-
* spill out of the 4-digit prefix window. */
|
|
83
|
-
export function suggestNextCode(type: AccountType, accounts: readonly Account[]): string {
|
|
84
|
-
const prefix = ACCOUNT_TYPE_PREFIX[type];
|
|
85
|
-
const inRange: number[] = [];
|
|
86
|
-
for (const account of accounts) {
|
|
87
|
-
if (!isValidAccountCode(account.code)) continue;
|
|
88
|
-
const value = Number.parseInt(account.code, 10);
|
|
89
|
-
if (Math.floor(value / 1000) !== prefix) continue;
|
|
90
|
-
inRange.push(value);
|
|
91
|
-
}
|
|
92
|
-
if (inRange.length === 0) return `${prefix}000`;
|
|
93
|
-
const max = Math.max(...inRange);
|
|
94
|
-
const candidate = max + SUGGESTED_GAP;
|
|
95
|
-
if (Math.floor(candidate / 1000) === prefix && candidate <= 9999) return String(candidate);
|
|
96
|
-
// Range is dense at the top — fall back to a unit step. If even
|
|
97
|
-
// that overflows the prefix window the chart is essentially full
|
|
98
|
-
// for that type; surface the overflow rather than silently
|
|
99
|
-
// suggesting a code in the next type's range.
|
|
100
|
-
const fallback = max + 1;
|
|
101
|
-
if (Math.floor(fallback / 1000) === prefix && fallback <= 9999) return String(fallback);
|
|
102
|
-
return `${prefix}999`;
|
|
103
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// Pure validation for the AccountsModal editor draft. Lives in its
|
|
2
|
-
// own module so unit tests can exercise the boundary cases (reserved
|
|
3
|
-
// `_` prefix, duplicate code, empty fields) without spinning up Vue
|
|
4
|
-
// or i18n. The component maps the returned error code to a
|
|
5
|
-
// localized message.
|
|
6
|
-
//
|
|
7
|
-
// The `_`-prefix rule mirrors the server's check in
|
|
8
|
-
// server/accounting/service.ts:upsertAccount — codes starting with
|
|
9
|
-
// `_` are reserved for synthetic report rows. Catching it client-
|
|
10
|
-
// side avoids a round-trip and surfaces the localized message
|
|
11
|
-
// instead of the raw server error.
|
|
12
|
-
|
|
13
|
-
import type { Account } from "../api";
|
|
14
|
-
import type { AccountDraft } from "./accountDraft";
|
|
15
|
-
import { codeMatchesType, isValidAccountCode } from "./accountNumbering";
|
|
16
|
-
|
|
17
|
-
export const RESERVED_PREFIX = "_";
|
|
18
|
-
|
|
19
|
-
export type CodeValidationError = "emptyCode" | "reservedCode" | "invalidCodeFormat" | "codeTypeMismatch" | "duplicateCode";
|
|
20
|
-
export type NameValidationError = "emptyName" | "duplicateName";
|
|
21
|
-
export type AccountValidationError = CodeValidationError | NameValidationError;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Validate just the code field. Split out from the full draft
|
|
25
|
-
* validator so AccountEditor can paint a per-field red border in
|
|
26
|
-
* realtime without re-running the name check on every keystroke.
|
|
27
|
-
*/
|
|
28
|
-
export function validateCodeField(draft: AccountDraft, existing: readonly Account[], isNew: boolean): CodeValidationError | null {
|
|
29
|
-
const trimmedCode = draft.code.trim();
|
|
30
|
-
if (trimmedCode.length === 0) return "emptyCode";
|
|
31
|
-
if (trimmedCode.startsWith(RESERVED_PREFIX)) return "reservedCode";
|
|
32
|
-
// 4-digit numbering is enforced for new accounts only: pre-existing
|
|
33
|
-
// books may already hold legacy codes the user added before the
|
|
34
|
-
// rule landed, and changing the code would orphan their journal
|
|
35
|
-
// lines (codes are immutable once created — see codeReadOnlyHint).
|
|
36
|
-
if (isNew && !isValidAccountCode(trimmedCode)) return "invalidCodeFormat";
|
|
37
|
-
if (isNew && !codeMatchesType(trimmedCode, draft.type)) return "codeTypeMismatch";
|
|
38
|
-
if (isNew && existing.some((account) => account.code === trimmedCode)) return "duplicateCode";
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Validate just the name field. Empty + duplicate (case-insensitive,
|
|
44
|
-
* trimmed) against other accounts. On edit, the account being edited
|
|
45
|
-
* is excluded from the duplicate check via `draft.code` — otherwise
|
|
46
|
-
* every save would flag the user's own row as a collision.
|
|
47
|
-
*/
|
|
48
|
-
export function validateNameField(draft: AccountDraft, existing: readonly Account[], isNew: boolean): NameValidationError | null {
|
|
49
|
-
const trimmedName = draft.name.trim();
|
|
50
|
-
if (trimmedName.length === 0) return "emptyName";
|
|
51
|
-
const folded = trimmedName.toLowerCase();
|
|
52
|
-
const collides = existing.some((account) => {
|
|
53
|
-
if (!isNew && account.code === draft.code.trim()) return false;
|
|
54
|
-
return account.name.trim().toLowerCase() === folded;
|
|
55
|
-
});
|
|
56
|
-
if (collides) return "duplicateName";
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Validate a draft about to be sent to `upsertAccount`. Returns
|
|
62
|
-
* `null` on success or an error code on failure. Caller maps the
|
|
63
|
-
* code to a localized message.
|
|
64
|
-
*
|
|
65
|
-
* `existing` is the current chart of accounts — used to detect a
|
|
66
|
-
* duplicate code on a brand-new entry (otherwise the server would
|
|
67
|
-
* silently overwrite the existing account, which is rarely what
|
|
68
|
-
* the user typing into the "Add account" form intended).
|
|
69
|
-
*
|
|
70
|
-
* Code errors take precedence over name errors so the user fixes
|
|
71
|
-
* one stable issue at a time as they type.
|
|
72
|
-
*/
|
|
73
|
-
export function validateAccountDraft(draft: AccountDraft, existing: readonly Account[], isNew: boolean): AccountValidationError | null {
|
|
74
|
-
return validateCodeField(draft, existing, isNew) ?? validateNameField(draft, existing, isNew);
|
|
75
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Stale-response guard for watcher-driven async fetches.
|
|
2
|
-
//
|
|
3
|
-
// Pattern: a watcher fires on bookId / filter / version changes
|
|
4
|
-
// and kicks off `apiPost(...)`. Without coordination, a slower
|
|
5
|
-
// earlier request can resolve after a newer one and overwrite the
|
|
6
|
-
// fresh state with stale data. This composable hands out a
|
|
7
|
-
// monotonic token before each await; the caller checks that the
|
|
8
|
-
// token is still current after the await before mutating state.
|
|
9
|
-
//
|
|
10
|
-
// Usage:
|
|
11
|
-
//
|
|
12
|
-
// const { begin, isCurrent } = useLatestRequest();
|
|
13
|
-
// async function refresh() {
|
|
14
|
-
// const token = begin();
|
|
15
|
-
// const result = await api.fetch(...);
|
|
16
|
-
// if (!isCurrent(token)) return; // a newer refresh started
|
|
17
|
-
// applyState(result);
|
|
18
|
-
// }
|
|
19
|
-
//
|
|
20
|
-
// Cheap and dependency-free. Each component holds its own
|
|
21
|
-
// `useLatestRequest()` instance — there's no shared state across
|
|
22
|
-
// components.
|
|
23
|
-
|
|
24
|
-
export interface LatestRequestApi {
|
|
25
|
-
/** Returns the token of the new request. Increments the
|
|
26
|
-
* internal counter; older outstanding requests will fail
|
|
27
|
-
* `isCurrent`. */
|
|
28
|
-
begin: () => number;
|
|
29
|
-
/** True if `token` is still the most recently issued one. */
|
|
30
|
-
isCurrent: (token: number) => boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function useLatestRequest(): LatestRequestApi {
|
|
34
|
-
let counter = 0;
|
|
35
|
-
return {
|
|
36
|
-
begin(): number {
|
|
37
|
-
counter += 1;
|
|
38
|
-
return counter;
|
|
39
|
-
},
|
|
40
|
-
isCurrent(token: number): boolean {
|
|
41
|
-
return token === counter;
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|