Compare commits

...

48 Commits

Author SHA1 Message Date
zhengchen.tao d629dfe18c docs: README fork notice 改动清单翻译为英文(面向 GitHub mirror 受众)
FORK.md 仍保持中文。
2026-05-07 15:47:00 +08:00
zhengchen.tao 76a274e1cc docs: README 移除已回滚的「交易时间点击标题默认日期选择器」条目
与 FORK.md item #8 的回滚状态对齐(commit 78818470)。
2026-05-07 15:32:24 +08:00
zhengchen.tao 291bd86c94 LICENSE: fork 版权署名顺序订正 Tao Zhengchen → Zhengchen Tao 2026-05-05 18:57:09 +08:00
zhengchen.tao 8658e849e7 ci: 镜像路径 dev/ezbookkeeping → zhengchen.tao/ezbookkeeping
仓库已转移到 zhengchen.tao 命名空间,build-image.yml 里的 OCI source label /
image tags / build summary 三处硬编码路径同步更新,否则 push 会推到不存在的
dev 命名空间。

NAS 上 nas-infra/ezbookkeeping/docker-compose.yml 的 image 路径仍需手动更新。
2026-05-05 18:20:30 +08:00
zhengchen.tao 6d0329210f FORK.md 进度表同步:item #8 标记已回滚 2026-05-05 18:10:00 +08:00
zhengchen.tao c57c1e8490 revert(transaction-time): 回滚 #header label click 改 'date' 模式
实际无可见效果——用户点的是下方 #title 里的日期/时间文本(上游 commit 368322f9
已实现按点击内容路由),#header label 行很少被点。改回上游行为。

FORK.md item #8 标记为已回滚。
2026-05-05 18:09:31 +08:00
zhengchen.tao 2425c358e3 docs: 扩充 README fork notice,列出主要定制项;LICENSE 增加 fork 修改的版权行 2026-05-05 18:05:18 +08:00
zhengchen.tao 373ccba9d6 build-image.yml 注释微调(再次触发,验证 DSM API 1.43 锁定后 build 通)
"等" → "等等",顺便利用这次 push 验证 nas-infra cd71de1 把
DOCKER_API_VERSION=1.43 加到 runner 容器 env 后,docker login 是否过。
2026-05-04 01:18:07 +08:00
zhengchen.tao ce345f79ab build-image.yml 注释微调(触发 concurrency cancel-in-progress 测试)
把 "重新打包旧 commit / 用自定义 tag / 等" 里多余的斜杠改成顿号,
顺便利用这次 push 验证上一条 d92e4fe3 的 in-progress run 是否被取消。
2026-05-04 00:40:43 +08:00
zhengchen.tao 6baf668696 build/deploy 合并为单 workflow 双 job,删除 deploy.yml
原 workflow_run 链触发会在 Actions 列表产生两条独立 run,UX 割裂。
合并后单 run + dependency graph 显式串联 build → deploy。

代价:失去"不 rebuild 只 redeploy"的 UI 单点触发,临时只想
重启容器需直接 ssh NAS 跑 docker compose up -d。

paths-ignore 同步移除已不存在的 deploy.yml 项。
2026-05-04 00:37:44 +08:00
zhengchen.tao 9ef0e62b05 fix(deploy): 加 docker login 步骤,否则 pull 私有 registry 401
build-image 跑过 docker login,但 deploy 是独立 workflow 容器,
凭据不继承。需要在 deploy 这边也登一次同一个 PACKAGES_TOKEN。

deploy.yml 在 paths-ignore 里,这次提交不会触发 build。但会触发
deploy 自己(不在 paths-ignore,且 workflow_dispatch 仍可手动)。
要测的话手动 dispatch Deploy Docker Image 即可。
2026-05-02 23:39:15 +08:00
zhengchen.tao 7df2d49c56 build-image paths-ignore 加 deploy.yml / sync-upstream.yml
改 deploy.yml 或 sync-upstream.yml 都不影响镜像内容(前者是部署
脚本、后者是 main reset 脚本),原本会触发整套 ~10 min 的 build
是浪费。把它俩加进 paths-ignore 跳过。

build-image.yml 自己保留触发(不在 ignore 里),这样改 workflow
能 self-test 验证改动有效。
2026-05-02 21:23:18 +08:00
zhengchen.tao 65d52571de build/deploy workflow 加 concurrency cancel-in-progress
并发组 = workflow name + ref。同分支连续 push 时:
- 新 run 入组发现已有 in-progress run → 立即取消旧的,新的开跑
- 最终只构建 + 部署最新代码,省 CI 时间
- 不同分支的 build/deploy 互不干扰(虽然当前只 custom 用)
- build 与 deploy 是两个独立 workflow name,互不影响(build 跑时
  deploy 不会被取消,反之亦然)

CLAUDE.md 同步加"并发取消策略"段说明该行为。
2026-05-02 21:18:39 +08:00
zhengchen.tao 4bdd2c7195 deploy.yml 移除 CUSTOM_DEPLOY_SCRIPTS 变量,部署脚本内联
原 vars.CUSTOM_DEPLOY_SCRIPTS 来自 deploy.yml 上游模板设计,本意是
"通用 hook,让一份 deploy.yml 复用到不同项目"。本仓库只有一个项目
一种部署场景,这层抽象纯属累赘:
- 改部署逻辑得去 Gitea UI 点 Variables,没法 PR review
- git log 看不到部署逻辑改动
- 脚本到底跑啥得对照 yml + Variable 两处

直接把 docker compose pull/up 内联进 deploy.yml,单一事实源在 git。

支持私有 / 公开两种 nas-infra:
- secrets.NAS_INFRA_TOKEN 设了 → 用 token clone(私有适用)
- 没设 → 裸 URL clone(公开适用)

CLAUDE.md 同步更新 deploy.yml 的描述。
2026-05-02 21:10:35 +08:00
zhengchen.tao 9da91ad54f deploy.yml 改自动触发:build 成功后 workflow_run 链式触发
- on.workflow_run: 监听 Build Docker Image 完成事件,分支限 custom
- if 条件:仅在 build 成功时跑 deploy(失败时跳过,避免部署半成品)
- workflow_dispatch 保留作为手动备选(重新部署当前镜像 / 应急脚本)
- 脚本生成改 > 覆盖(原 >> 会累积历史脚本)+ 加 set -e 失败即停
- 加 Deploy summary 步骤把触发链路信息写入 GITHUB_STEP_SUMMARY
  方便从 UI 看到本次 deploy 跟在哪次 build 后面

CLAUDE.md 同步更新 workflow 清单 + 流程图:现在 push → build →
deploy 全自动 CD,仅需在 repo Variables 里配 CUSTOM_DEPLOY_SCRIPTS
脚本内容才能产生实际部署效果。
2026-05-02 21:07:09 +08:00
zhengchen.tao 55c175acca ci: 注释 paths-ignore 对 empty commit 的 vacuously-skip 行为
empirical discovery:c6bb0c85 那个 --allow-empty commit 推送后没有
触发 build。证实 Gitea Actions(同 GitHub Actions)对 paths-ignore
+ empty commit 的处理是"vacuously matches ignore list, skip"。

把这个 quirk 直接注释在 workflow 文件里,后续自己或 Claude 看到
build 没触发时不用再怀疑 trigger 配置错了,知道是 empty commit 的
正常行为。
2026-05-02 20:49:17 +08:00
zhengchen.tao 3ed37e7719 ci: trigger auto-build to verify push trigger works
empty commit, no code changes. 用于验证 build-image.yml 的 push trigger
确实在 commit 推送后自动跑起来,不需要去 Actions UI 手动点。

如果这次 push 后 Gitea Actions 没有自动出现新 run,说明 paths-ignore
对 empty commit 的过滤行为是"vacuously skip",需要至少一个非 ignore 路径
的真实改动才能触发。
2026-05-02 20:47:05 +08:00
zhengchen.tao 32a49be913 build-image workflow 改自动触发 + 保留手动备选
- on.push.branches: [custom] —— 推送 custom 自动跑
- on.push.paths-ignore:屏蔽 *.md / .gitignore / LICENSE / screenshot/**
  避免 doc-only 改动浪费 ~10 min 构建
- on.workflow_dispatch 保留作为应急通道(重打旧 commit / 自定义 tag)
- checkout ref 兼容两种触发:${{ inputs.branch || github.ref_name }}
  - workflow_dispatch:用用户填的 branch(默认 custom)
  - push:fallback 到 github.ref_name(即触发分支,永远是 custom)
- Build summary 加"触发方式"行,便于区分本次是自动还是手动

预期:本提交本身就会触发自动构建(改了 .gitea/workflows/build-image.yml
不在 paths-ignore 内),相当于 self-test。
2026-05-02 20:41:22 +08:00
zhengchen.tao ebcc03d3d0 docs: ci 分支已删除,更新 CLAUDE.md 与 build summary 措辞
ci 分支于 2026-05-02 删除(默认分支已切到 custom,workflow 文件已
迁回 custom)。更新各处反映"两分支模型 main + custom"的最终状态:

- CLAUDE.md "三个分支" → "两个分支",ci 段改写为"已退役 + 历史
  说明",给后续 Claude 解释 git log 里 555ecc1a 这条迁移提交
- 同步历史里 2026-05-02(后续) 那条加上"随后删掉 ci 分支"的事实
- build-image.yml 的 Build summary 步骤移除"UI 顶部 commit 是 ci"
  的警示注释(workflow 已在 custom,runs 列表 commit 直接就是
  代码 commit,不再需要这条解释)
2026-05-02 20:34:38 +08:00
zhengchen.tao 0be04287c8 docs: CLAUDE.md 更新为 workflow 已迁到 custom 的事实
- 三分支表更新:custom 是 default branch + 持有 workflow,ci
  降级为过渡态历史分支
- 改写"为什么 workflow 在 custom 不在独立分支"段,记录设计决策
  演进的真实理由(runs 列表 UX 优于 meta/code 分离的设计美感)
- workflow 清单从 5 项缩到 3 项,记录 docker-release/snapshot
  已删的事实
- "给后续 Claude" 提示中"不要把工作流提交到 custom" 改为相反
  方向(直接在 custom 改 workflow)
- 同步历史补 555ecc1a (workflow 迁移) 与 75b4d78d (numpad fix)
2026-05-02 18:38:00 +08:00
zhengchen.tao dfbc2b1440 ci: workflow 文件迁到 custom 分支
之前 workflow 在 ci 分支,导致每次 dispatch 后 Gitea Actions 列表
显示的 commit 都是 ci 分支的 workflow 文件 commit,不是被实际构建
的 custom 代码 commit,UX 上误导性强。

挪到 custom 后:
- runs 列表的 commit 字段直接显示真实代码 commit
- workflow_dispatch UI 自动从默认分支(待手动切到 custom)发现
  workflow
- rebase 上游时 workflow 文件随 custom 一起平移,无额外操作

同步移除上游残留的 docker-release.yml / docker-snapshot.yml:
- 触发依赖 secrets.DOCKER_REPO(未配),sync-upstream 推 main
  /tags 时空跑失败
- ci 上已禁用,但文件留着是噪声,本次清掉

ci 分支 .gitea/workflows/ 暂保留作过渡,待用户在 Gitea UI 把
默认分支切到 custom + 验证 build 跑通后,再单独 cleanup ci。
2026-05-02 18:35:53 +08:00
zhengchen.tao 47b5641597 docs: CLAUDE.md 解释 ci/custom 不需保持一致 + UI commit 不等于构建 commit
补充三分支拓扑后的"FAQ 段":

- ci 与 custom 内容不重叠是设计,不是 bug
- Gitea Actions UI 顶部显示的 commit 是 workflow dispatch 触发
  位置(即 ci 的 HEAD),不是构建源代码 commit
- 真实构建的代码 commit 在镜像 tag / OCI revision label /
  workflow 末尾 Build summary 三处都能看到,看 summary 区即可

附两条可选替代方案(workflow 挪 custom / 加 push trigger 自动构建),
说明当前选择的中间路径理由。
2026-05-02 18:26:40 +08:00
zhengchen.tao 11da502f75 fix(numpad): 修小键盘点击卡顿,touch-action: none → manipulation
诊断:用户反馈仅小键盘点击有延迟感,其他按钮正常。范围缩小后定位到
.numpad-button 上的 touch-action: none(上游 e178a079 引入)与 F7
内部 tap 事件处理叠加,让 click 事件合成慢一拍。backspace(自定义
.numpad-backspace-button 类)不受影响,刚好印证范围。

修复:改为 touch-action: manipulation(W3C 标准"快速点击"值),禁双
击缩放消除老 300ms 延迟,但保留 click 事件正常合成。

FORK.md #11 状态:🔍 调查中 → 🟢 已完成,附真因记录避免后续误诊。
2026-05-02 18:09:40 +08:00
zhengchen.tao 29c164439c docs: CLAUDE.md 加 ci 分支 workflow 清单 + 链 FORK.md
ci 分支 5 个 workflow(sync-upstream / build-image / deploy /
docker-snapshot / docker-release)全部列表化,含触发条件、职责、
当前状态。明确"日常只用 sync-upstream + build-image 两个,
其他三个要么按需配置要么后续清理"。

custom 分支说明里加链接指 FORK.md(feature 维度的清单),
顶部说明区域厘清"meta(CLAUDE.md)vs 改动清单(FORK.md)vs
通用决策框架(不入库)"三层关系。
2026-05-02 17:58:43 +08:00
zhengchen.tao 989ffef156 docs: rename MY_REQUIREMENTS.md → FORK.md, drop stale DEPLOY.md
- 重命名为 FORK.md,对接更通用的 fork-doc 命名约定
- DEPLOY.md 内容全过期(ghcr.io 镜像、myrequirement 分支、
  docker run 风格部署),全部已废 — 部署文档现在两层:
  nas-infra/README.md(compose level)+ CLAUDE.md(CI 排查)
- README.md 顶部加一行 fork notice,链到 FORK.md 与 CLAUDE.md
  (单行变更,rebase 友好)
- FORK.md 顶部加关联文档表
2026-05-02 17:58:34 +08:00
zhengchen.tao c929e950e1 docs: CLAUDE.md 加 backend 测试踩坑 + 通用排查原则
CI 故障排查路径表加一行"测试/lint 失败 → 先看 Dockerfile ARG",
强调先对齐上游 CI 跳过开关(BUILD_PIPELINE / CHECK_3RD_API /
SKIP_TESTS)再考虑改测试代码。同步历史补 2026-05-02 第三层修复。
2026-05-02 16:07:58 +08:00
zhengchen.tao 6b8d9fcb13 docs: 加仓库级 CLAUDE.md(分支拓扑 + 同步流程 + CI 排查路径)
记录 fork 工作流(main 锚 tag / custom 改动 / ci 工作流),
2026-05-01 rebase 与 2026-05-02 build 修复历史,
以及给后续 Claude 会话的 CI 故障分流路径表。
2026-05-02 15:35:47 +08:00
zhengchen.tao fe265259d7 更新个人需求清单,新增信用额度字段描述;新增部署说明文档,包含镜像地址及操作步骤 2026-05-01 17:29:29 +08:00
zhengchen.tao 69d66c8634 Revert "删除个人需求清单文档"
This reverts commit f045f0702a.
2026-05-01 17:29:29 +08:00
zhengchen.tao 729c04880f 更新 GitHub Actions 工作流:在推送事件中忽略 myrequirement 分支 2026-05-01 17:29:29 +08:00
zhengchen.tao 7cfb5c7457 优化 Docker 镜像构建工作流:新增小写图像名称设置,确保一致性 2026-05-01 17:29:29 +08:00
zhengchen.tao 93630a821d 新增构建和推送 Docker 镜像的 GitHub Actions 工作流 2026-05-01 17:29:29 +08:00
zhengchen.tao 501765d669 删除个人需求清单文档 2026-05-01 17:29:29 +08:00
zhengchen.tao 91fa3b65f3 新增默认展开分类列表设置,更新应用设置以支持云同步 2026-05-01 17:29:29 +08:00
zhengchen.tao b82533233e 优化分类选择功能:新增默认展开分类列表设置,更新相关页面以支持此功能 2026-05-01 17:29:29 +08:00
MaysWind 9c4a0493ee upgrade third party dependencies 2026-05-01 17:29:29 +08:00
MaysWind 9aa6c4102e delete all files before updating the translation progress files 2026-05-01 17:29:29 +08:00
MaysWind f058fa53eb update the translation progress calculation method 2026-05-01 17:29:28 +08:00
zhengchen.tao 4ff73b475a 更新个人需求清单,新增信用额度和可用额度的标注;在账户列表和交易列表中显示可用额度;修改记账页面的时间选择为默认日期选择器;优化账户余额显示逻辑 2026-04-06 01:16:38 +08:00
zhengchen.tao ba85852543 在交易列表页面顶部显示账户信息:新增账户图标、名称和余额信息卡片,优化单账户筛选时的显示逻辑 2026-04-05 18:51:27 +08:00
zhengchen.tao c7c84c74d3 优化账户余额调整功能:新增调整余额的逻辑,更新相关页面以显示账户余额和可用额度,调整路由配置以移除不必要的动画效果。 2026-04-05 18:40:52 +08:00
zhengchen.tao 5fbff39c4f 添加信用额度功能:在账户模型中新增信用额度字段,更新相关请求和响应结构,修改账户创建和修改逻辑,更新界面以支持信用额度的显示和编辑。 2026-04-05 17:04:16 +08:00
zhengchen.tao 285fef6eba 更新 .gitignore,添加 .claude/ 目录;新增 MY_REQUIREMENTS.md 文件,记录个人需求清单;优化小键盘组件,调整布局并添加删除按钮;修改路由配置,禁用某些页面的动画效果;全局样式中调整过渡时长。 2026-04-05 16:31:02 +08:00
MaysWind 97fb73ad43 add translation process badge 2026-03-30 00:40:14 +08:00
MaysWind ce0c9ec65e add new contributor 2026-03-28 17:34:42 +08:00
MaysWind ed084e1ce0 update README.md 2026-03-28 17:33:53 +08:00
1270o1 ec84065f73 Update DE translation (#540)
Big improvement to the German translation (frontend)
2026-03-28 17:26:09 +08:00
MaysWind 2e8aedcfa6 bump version to 1.5.0 2026-03-22 23:34:42 +08:00
45 changed files with 3820 additions and 2756 deletions
+167
View File
@@ -0,0 +1,167 @@
name: Build Docker Image
on:
# 自动触发:push 到 custom 分支时跑(force-push 后的 rebase 也会触发,可接受)
# paths-ignore:纯文档/配置改动跳过,避免浪费 ~10 分钟构建
# ⚠️ 已知 quirk2026-05-02 验证):empty commitgit commit --allow-empty
# 不会触发 paths-ignore 过滤的 workflowGitea 把 zero-paths-changed 当作
# "vacuously matches ignore list" 跳过。要强制触发必须至少改一个非 ignore 路径
# 的真实文件(改这个 yml 自己最稳)。
push:
branches: [custom]
paths-ignore:
- '**.md'
- '.gitignore'
- 'LICENSE'
- 'screenshot/**'
# sync-upstream.yml 改的是 main reset 逻辑,跟 build 无关
# build-image.yml 自己留着会触发,作为 workflow 改动的 self-test
- '.gitea/workflows/sync-upstream.yml'
# 手动触发:保留作为应急通道(重新打包旧 commit、用自定义 tag 等等)
# 注意:手动触发也会跑 deploy job —— 如果只想 build 不部署,临时把 deploy
# job 注释掉或在 deploy 里加 if 条件
workflow_dispatch:
inputs:
branch:
description: '要打包的分支(仅手动触发生效)'
required: true
default: 'custom'
tag:
description: '镜像 tag(留空则用 commit short hash'
required: false
default: ''
# 并发控制:同一分支的连续 push 只跑最新的,旧 in-progress run 会被取消
# 例:连续 3 次 push,第 1 次 build 跑了 30s,第 2 次开始 → 取消第 1,第 2 跑;
# 期间第 3 次又来 → 取消第 2,第 3 跑。最后只构建+部署最新代码,省 CI 时间。
# group 包含 ref 是为了不同分支的 build 互不干扰(虽然当前只有 custom 用)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout target branch
uses: actions/checkout@v4
with:
# workflow_dispatch 时用用户填的 branchpush 触发时 inputs.branch 为空,
# fallback 到 github.ref_name(即触发的分支名,push 到 custom 时就是 custom
ref: ${{ inputs.branch || github.ref_name }}
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
# 钉到 v0.13.2(自带 runc 1.1.x),避免 runc 1.2+ 的 procfs 安全检查
# 在 DSM 老内核(4.4.x)上撞 openat2/fsmount 不存在导致 build 失败
driver-opts: |
image=moby/buildkit:v0.13.2
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: git.zhengchentao.win
username: ${{ gitea.actor }}
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Determine image tag and revision
id: meta
run: |
if [ -n "${{ inputs.tag }}" ]; then
IMAGE_TAG="${{ inputs.tag }}"
else
IMAGE_TAG="$(git rev-parse --short HEAD)"
fi
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "full_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "==> Image tag: $IMAGE_TAG"
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
# 上游 Dockerfile 用 BUILD_PIPELINE 作为 CI 跳过开关:
# 设为 "1" 时 pkg/exchangerates 跳过依赖第三方 API 的活测试
# (加拿大银行/乌兹别克央行 API 国内不稳,跑就超时)
# CHECK_3RD_API 留空 → 三方 API 测试不跑;想跑设 "1"
build-args: |
BUILD_PIPELINE=1
# OCI 标签:
# - source 让 Gitea 收包时自动把镜像关联到对应 repo(不再需要手动去
# "包设置 → 链接到仓库")
# - revision 把构建时的 commit full SHA 烙进镜像 manifest
# docker inspect 能反推回源码版本
labels: |
org.opencontainers.image.source=https://git.zhengchentao.win/zhengchen.tao/ezbookkeeping
org.opencontainers.image.revision=${{ steps.meta.outputs.full_sha }}
tags: |
git.zhengchentao.win/zhengchen.tao/ezbookkeeping:${{ steps.meta.outputs.image_tag }}
git.zhengchentao.win/zhengchen.tao/ezbookkeeping:latest
- name: Build summary
# 把构建出的镜像 tag 与源 commit 显式列在 Action run summary 区,
# 方便从 UI 一眼看到本次 build 产出。always() 保证 build 失败也输出。
if: always()
run: |
{
echo "## Build Summary"
echo ""
echo "| 项 | 值 |"
echo "|---|---|"
echo "| 触发方式 | \`${{ github.event_name }}\` |"
echo "| 源分支 | \`${{ inputs.branch || github.ref_name }}\` |"
echo "| 源 commit (full) | \`${{ steps.meta.outputs.full_sha }}\` |"
echo "| 源 commit (short) | \`${{ steps.meta.outputs.image_tag }}\` |"
echo "| 镜像 tag | \`git.zhengchentao.win/zhengchen.tao/ezbookkeeping:${{ steps.meta.outputs.image_tag }}\` + \`:latest\` |"
} >> "$GITHUB_STEP_SUMMARY"
deploy:
# needs: build 串起来 —— build 失败 deploy 自动跳过,无需 if 条件
needs: build
runs-on: ubuntu-latest
steps:
# 登录 Gitea Container Registry,否则 docker compose pull 私有镜像 401。
# 跟 build job 那步是同一个 PACKAGES_TOKEN,但每个 job 跑在独立 runner 上,
# 凭据不会从 build job 继承,必须在这里再登一次。
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: git.zhengchentao.win
username: ${{ gitea.actor }}
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Pull and restart ezbookkeeping
# 部署逻辑直接内联在这。runner 容器挂了 host docker.sock
# 所以这里 docker 命令直接操作的是宿主机 docker daemon
# 容器层面相当于 "ssh 到 NAS 跑 docker compose"。
#
# NAS_INFRA_TOKEN secret 仅在 nas-infra 是私有仓库时需要;
# 公开仓库不设这个 secret 也能拉。
env:
NAS_INFRA_TOKEN: ${{ secrets.NAS_INFRA_TOKEN }}
run: |
set -e
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# 决定 clone URL:有 token 用 token(私有),没有用裸 URL(公开)
if [ -n "$NAS_INFRA_TOKEN" ]; then
CLONE_URL="https://x-access-token:${NAS_INFRA_TOKEN}@git.zhengchentao.win/dev/nas-infra.git"
else
CLONE_URL="https://git.zhengchentao.win/dev/nas-infra.git"
fi
git clone --depth 1 "$CLONE_URL" "$TMPDIR/nas-infra"
cd "$TMPDIR/nas-infra/ezbookkeeping"
docker compose pull
docker compose up -d
# 简单 health:列容器状态 + 输出最近日志
sleep 3
docker compose ps
docker compose logs --tail=30 ezbookkeeping
-17
View File
@@ -1,17 +0,0 @@
name: Deploy Docker Image
on:
workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Execute custom script
run: |
cat >> deploy.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_DEPLOY_SCRIPTS }}
EOF
chmod +x deploy.sh
./deploy.sh
-64
View File
@@ -1,64 +0,0 @@
name: Docker Release
on:
push:
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up the environment
id: setup
run: |
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v6
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_RELEASE_PLATFORMS }}
push: true
build-args: |
RELEASE_BUILD=1
BUILD_PIPELINE=1
BUILD_UNIXTIME=${{ steps.setup.outputs.build_unix_time }}
BUILD_DATE=${{ steps.setup.outputs.build_date }}
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
SKIP_TESTS=${{ vars.SKIP_TESTS }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-63
View File
@@ -1,63 +0,0 @@
name: Docker Snapshot
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
tags: |
type=raw,value=SNAPSHOT-{{date 'YYYYMMDD'}}
type=raw,value=latest-snapshot
type=sha,format=short,prefix=SNAPSHOT-
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up the environment
id: setup
run: |
echo "build_unix_time=$(date '+%s')" >> "$GITHUB_OUTPUT"
echo "build_date=$(date '+%Y%m%d')" >> "$GITHUB_OUTPUT"
sed -r -i 's#FROM( --.*)? (.*:.*)?#FROM\1 ${{ secrets.DOCKER_REPO }}/mirrors/\2#g' Dockerfile
cat >> docker/custom-backend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_BACKEND_PRE_SETUP }}
EOF
cat >> docker/custom-frontend-pre-setup.sh <<EOF
#!/bin/sh
${{ vars.CUSTOM_FRONTEND_PRE_SETUP }}
EOF
chmod +x docker/custom-backend-pre-setup.sh
chmod +x docker/custom-frontend-pre-setup.sh
- name: Build and push
uses: docker/build-push-action@v6
with:
file: Dockerfile
context: .
platforms: ${{ vars.BUILD_SNAPSHOT_PLATFORMS }}
push: true
build-args: |
BUILD_PIPELINE=1
BUILD_UNIXTIME=${{ steps.setup.outputs.build_unix_time }}
BUILD_DATE=${{ steps.setup.outputs.build_date }}
CHECK_3RD_API=${{ vars.CHECK_3RD_API }}
SKIP_TESTS=${{ vars.SKIP_TESTS }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+39
View File
@@ -0,0 +1,39 @@
name: Sync from upstream
on:
workflow_dispatch:
inputs:
tag:
description: '要同步的 release tag(留空则同步到 upstream/main 的最新 tag'
required: false
default: ''
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.SYNC_TOKEN }}
- name: Sync main to release tag
run: |
git config user.name "gitea-actions"
git config user.email "actions@gitea.local"
git remote add upstream https://git.zhengchentao.win/mirror/ezbookkeeping.git
git fetch upstream --tags
if [ -n "${{ inputs.tag }}" ]; then
TARGET="${{ inputs.tag }}"
else
TARGET=$(git tag -l --sort=-v:refname | head -n 1)
fi
echo "==> Syncing main to $TARGET"
git rev-parse "$TARGET" || { echo "❌ Tag $TARGET not found"; exit 1; }
git checkout -B main origin/main
git reset --hard "$TARGET"
git push origin main --force-with-lease
git push origin --tags
+352
View File
@@ -0,0 +1,352 @@
const fs = require('fs');
const path = require('path');
const FRONTEND_LOCALES_DIR = path.join(__dirname, '..', '..', 'src', 'locales');
const BACKEND_LOCALES_DIR = path.join(__dirname, '..', '..', 'pkg', 'locales');
const OUTPUT_DIR = process.argv[2] || path.join(__dirname, '..', '..', 'i18n-badge');
const DEFAULT_LANGUAGE_TAG = 'en';
const BACKEND_SKIP_STRUCTS = new Set([
'GlobalTextItems',
'DefaultTypes',
'DataConverterTextItems',
]);
function discoverFrontendLanguages() {
const indexPath = path.join(FRONTEND_LOCALES_DIR, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
const importMap = {};
const importRegex = /import\s+(\w+)\s+from\s+['"]\.\/([\w_]+\.json)['"]/g;
let match;
while ((match = importRegex.exec(content)) !== null) {
importMap[match[1]] = match[2];
}
const result = {};
const langRegex = /['"]([^'"]+)['"]\s*:\s*\{[^}]*content\s*:\s*(\w+)/g;
while ((match = langRegex.exec(content)) !== null) {
const tag = match[1];
const varName = match[2];
if (importMap[varName]) {
result[tag] = importMap[varName];
}
}
return result;
}
function discoverBackendLanguages() {
const allLocalesPath = path.join(BACKEND_LOCALES_DIR, 'all_locales.go');
const content = fs.readFileSync(allLocalesPath, 'utf-8');
const result = {};
const entryRegex = /"([^"]+)"\s*:\s*\{[^}]*Content\s*:\s*(\w+)/g;
let match;
while ((match = entryRegex.exec(content)) !== null) {
const tag = match[1];
const fileName = tag.toLowerCase().replace(/-/g, '_') + '.go';
const filePath = path.join(BACKEND_LOCALES_DIR, fileName);
if (fs.existsSync(filePath)) {
result[tag] = fileName;
}
}
return result;
}
function flattenJSON(obj, prefix) {
const result = {};
for (const key of Object.keys(obj)) {
const fullKey = prefix ? prefix + '.' + key : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
Object.assign(result, flattenJSON(obj[key], fullKey));
} else {
result[fullKey] = obj[key];
}
}
return result;
}
function shouldSkipFrontendKey(key) {
if (key.startsWith('global.')) {
return true;
} else if (key.startsWith('default.')) {
return true;
} else if (key.startsWith('currency.')) {
if (key.startsWith('currency.unit.')) {
return true;
} else {
return false;
}
} else if (key.startsWith('mapprovider.')) {
return true;
} else if (key.startsWith('encoding.')) {
return true;
} else if (key.startsWith('document.')) {
if (key.startsWith('document.anchor.')) {
return true;
} else {
return false;
}
} else {
return false;
}
}
function isFrontendAlwaysTranslatedKey(key) {
if (key.startsWith('language.')) {
return true;
} else if (key.startsWith('format.')) {
if (key.startsWith('format.misc.')) {
if (key === 'format.misc.multiTextJoinSeparator') {
return true;
} else if (key === 'format.misc.eachMonthDayInMonthDays') {
return true;
} else {
return false;
}
} else {
return true;
}
} else if (key.startsWith('datetime.')) {
return true;
} else if (key.startsWith('timezone.')) {
return true;
} else if (key.startsWith('currency.')) {
if (key === 'currency.name.EUR') {
return true;
} else {
return false;
}
} else if (key.startsWith('parameter.')) {
if (key === 'parameter.id') {
return true;
} else {
return false;
}
} else {
if (key === 'OK') {
return true;
} else {
return false;
}
}
}
function extractGoStringFields(content) {
const fields = [];
const structBlockRegex = /(\w+):\s*&\w+\{([^}]*)\}/gs;
let blockMatch;
while ((blockMatch = structBlockRegex.exec(content)) !== null) {
const structName = blockMatch[1];
const blockBody = blockMatch[2];
const fieldRegex = /(\w+):\s+"((?:[^"\\]|\\.)*)"/g;
let fieldMatch;
while ((fieldMatch = fieldRegex.exec(blockBody)) !== null) {
fields.push({
struct: structName,
name: fieldMatch[1],
value: fieldMatch[2],
});
}
}
return fields;
}
function getProgressColor(progress) {
if (progress >= 95) {
return 'brightgreen';
} else if (progress >= 90) {
return 'green';
} else if (progress >= 70) {
return 'yellowgreen';
} else if (progress >= 50) {
return 'yellow';
} else if (progress >= 20) {
return 'orange';
} else {
return 'red';
}
}
function main() {
const frontendLangs = discoverFrontendLanguages();
const backendLangs = discoverBackendLanguages();
const allTags = new Set([...Object.keys(frontendLangs), ...Object.keys(backendLangs)]);
console.log('Discovered ' + allTags.size + ' languages: ' + [...allTags].sort().join(', '));
const defaultFrontendJSON = JSON.parse(fs.readFileSync(path.join(FRONTEND_LOCALES_DIR, `${DEFAULT_LANGUAGE_TAG}.json`), 'utf-8'));
const defaultFrontendItemsMap = flattenJSON(defaultFrontendJSON, '');
const defaultFrontendKeys = Object.keys(defaultFrontendItemsMap);
const frontendTranslatableKeys = defaultFrontendKeys.filter(function (k) {
return !shouldSkipFrontendKey(k);
});
const frontendSkippedCount = defaultFrontendKeys.length - frontendTranslatableKeys.length;
const frontendTotal = frontendTranslatableKeys.length;
const defaultBackendContent = fs.readFileSync(path.join(BACKEND_LOCALES_DIR, `${DEFAULT_LANGUAGE_TAG}.go`), 'utf-8');
const defaultBackendItems = extractGoStringFields(defaultBackendContent);
const defaultBackendTranslatableItems = defaultBackendItems.filter(function (f) {
return !BACKEND_SKIP_STRUCTS.has(f.struct);
});
const backendSkippedCount = defaultBackendItems.length - defaultBackendTranslatableItems.length;
const backendTotal = defaultBackendTranslatableItems.length;
console.log('Frontend: ' + frontendTotal + ' translatable keys (' + frontendSkippedCount + ' excluded)');
console.log('Backend: ' + backendTotal + ' translatable fields (' + backendSkippedCount + ' excluded)');
const results = {};
const untranslatedKeys = {};
for (const tag of allTags) {
results[tag] = {
languageTag: tag,
frontendTranslated: 0,
frontendTotal: frontendTotal,
backendTranslated: 0,
backendTotal: backendTotal
};
untranslatedKeys[tag] = [];
}
for (const tag of Object.keys(frontendLangs)) {
if (tag === DEFAULT_LANGUAGE_TAG) {
results[tag].frontendTranslated = frontendTotal;
continue;
}
const file = frontendLangs[tag];
const filePath = path.join(FRONTEND_LOCALES_DIR, file);
if (!fs.existsSync(filePath)) {
continue;
}
const json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
const kv = flattenJSON(json, '');
let translated = 0;
for (const key of frontendTranslatableKeys) {
if (kv[key] !== undefined && kv[key] !== '' && (kv[key] !== defaultFrontendItemsMap[key] || isFrontendAlwaysTranslatedKey(key))) {
translated++;
} else {
untranslatedKeys[tag].push({ source: path.join('src', 'locales', file), key: key, defaultValue: defaultFrontendItemsMap[key], value: kv[key] });
}
}
results[tag].frontendTranslated = translated;
}
for (const tag of Object.keys(backendLangs)) {
if (tag === DEFAULT_LANGUAGE_TAG) {
results[tag].backendTranslated = backendTotal;
continue;
}
const file = backendLangs[tag];
const filePath = path.join(BACKEND_LOCALES_DIR, file);
if (!fs.existsSync(filePath)) {
continue;
}
const content = fs.readFileSync(filePath, 'utf-8');
const fields = extractGoStringFields(content).filter(function (f) {
return !BACKEND_SKIP_STRUCTS.has(f.struct);
});
let translated = 0;
for (let i = 0; i < defaultBackendTranslatableItems.length; i++) {
if (i < fields.length && fields[i].value !== defaultBackendTranslatableItems[i].value) {
translated++;
} else {
untranslatedKeys[tag].push({ source: path.join('pkg', 'locales', file), key: defaultBackendTranslatableItems[i].struct + '.' + defaultBackendTranslatableItems[i].name, defaultValue: defaultBackendTranslatableItems[i].value, value: (i < fields.length) ? fields[i].value : null });
}
}
results[tag].backendTranslated = translated;
}
for (const tag of Object.keys(results)) {
const r = results[tag];
const totalTranslated = r.frontendTranslated + r.backendTranslated;
const totalItems = r.frontendTotal + r.backendTotal;
r.totalProgress = Math.round((totalTranslated / totalItems) * 10000) / 100;
}
const sortedResults = {};
var sortedTags = Object.keys(results).sort();
for (const tag of sortedTags) {
sortedResults[tag] = results[tag];
}
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
var badgesDir = path.join(OUTPUT_DIR, 'badges');
if (!fs.existsSync(badgesDir)) {
fs.mkdirSync(badgesDir, { recursive: true });
}
fs.writeFileSync(
path.join(OUTPUT_DIR, 'i18n-progress.json'),
JSON.stringify(sortedResults, null, 4) + '\n'
);
for (const tag of sortedTags) {
const data = sortedResults[tag];
const badge = {
schemaVersion: 1,
label: 'translation',
message: data.totalProgress + '%',
color: getProgressColor(data.totalProgress)
};
fs.writeFileSync(
path.join(badgesDir, tag + '.json'),
JSON.stringify(badge, null, 4) + '\n'
);
}
var untranslatedDir = path.join(OUTPUT_DIR, 'untranslated');
if (!fs.existsSync(untranslatedDir)) {
fs.mkdirSync(untranslatedDir, { recursive: true });
}
for (const tag of sortedTags) {
const items = untranslatedKeys[tag] || [];
fs.writeFileSync(
path.join(untranslatedDir, tag + '.json'),
JSON.stringify(items, null, 4) + '\n'
);
}
for (const tag of sortedTags) {
const data = sortedResults[tag];
const missingCount = (untranslatedKeys[tag] || []).length;
console.log(tag + ': ' + data.totalProgress + '% (frontend: ' + data.frontendTranslated + '/' + data.frontendTotal + ', backend: ' + data.backendTranslated + '/' + data.backendTotal + ', untranslated: ' + missingCount + ')');
}
console.log('\nResults written to ' + OUTPUT_DIR);
}
main();
+39
View File
@@ -0,0 +1,39 @@
name: Build and Push Docker Image
on:
push:
branches:
- myrequirement
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set lowercase image name
run: echo "IMAGE_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -4,6 +4,7 @@ on:
push:
branches-ignore:
- main
- myrequirement
jobs:
setup:
@@ -0,0 +1,76 @@
name: Update i18n Translation Progress Badges
on:
push:
branches:
- main
paths:
- 'src/locales/**'
- 'pkg/locales/**'
workflow_dispatch:
jobs:
update-i18n-progress:
if: vars.UPDATE_I18N_BADGE_REPO == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Update translation progress data
run: |
node .github/scripts/update-i18n-progress.js ${{ runner.temp }}/i18n-badge
- name: Checkout badge repository
uses: actions/checkout@v5
with:
repository: mayswind/ezbookkeeping-i18n-badge
token: ${{ secrets.I18N_BADGE_REPO_TOKEN }}
path: ezbookkeeping-i18n-badge
- name: Update badge data
run: |
rm -rf ezbookkeeping-i18n-badge/i18n-progress.json
cp ${{ runner.temp }}/i18n-badge/i18n-progress.json ezbookkeeping-i18n-badge/
mkdir -p ezbookkeeping-i18n-badge/badges
rm -rf ezbookkeeping-i18n-badge/badges/*
cp ${{ runner.temp }}/i18n-badge/badges/*.json ezbookkeeping-i18n-badge/badges/
mkdir -p ezbookkeeping-i18n-badge/untranslated
rm -rf ezbookkeeping-i18n-badge/untranslated/*
cp ${{ runner.temp }}/i18n-badge/untranslated/*.json ezbookkeeping-i18n-badge/untranslated/
- name: Commit and push
run: |
cd ezbookkeeping-i18n-badge
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "Update i18n progress data (${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})"
git push
fi
- name: Purge GitHub camo image cache
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CAMO_URLS=$(curl -s -H "Accept: application/vnd.github.html+json" -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/${{ github.repository }}/readme" | grep -oP 'https://camo\.githubusercontent\.com/[^"]+' | sort -u)
if [ -z "$CAMO_URLS" ]; then
echo "No camo URLs found, skipping cache purge"
exit 0
fi
for url in $CAMO_URLS; do
echo "Purging: $url"
curl -s -X PURGE "$url" > /dev/null
done
echo "Purged $(echo "$CAMO_URLS" | wc -l) camo URLs"
+2
View File
@@ -161,3 +161,5 @@ package/
data/
storage/
log/
.claude/
+127
View File
@@ -0,0 +1,127 @@
# CLAUDE.md
本项目是 [mayswind/ezbookkeeping](https://github.com/mayswind/ezbookkeeping) 的个人 fork。
> **本文件**:仓库分支模型、上游同步流程、CI 故障排查 —— meta 层
> **[`FORK.md`](FORK.md)**fork 相对上游的具体改动清单(feature 维度 + 进度状态)
> **个人笔记**:通用 fork 工作流决策框架在 `fork-工作流决策框架.md`(不入库)
本文件只记录**这个仓库的具体事实**,避免 Claude 会话误判。
---
## 仓库拓扑
```
github.com/mayswind/ezbookkeeping (上游)
│ Gitea pull mirror(后台异步)
git.zhengchentao.win/mirror/ezbookkeeping (只读镜像)
│ CI workflow 拉过来
git.zhengchentao.win/dev/ezbookkeeping origin,本地唯一 remote
```
本地 `git remote -v` 只有 origin 一项,**没有手工配 upstream**。上游同步通过 custom 分支上的 workflow 在服务端完成,不是本地操作。
---
## 两个分支的职责(必须先理解,否则会改错地方)
| 分支 | 职责 | force push? |
|---|---|---|
| `main` | **锚定上游 release tag**(当前 v1.4.0)。被 `.gitea/workflows/sync-upstream.yml` `git reset --hard <tag>` 覆写。**别在 main 上做任何改动** | 是(由 CI 做) |
| `custom` | **所有个人改动 + workflow 文件都在这**:信用额度功能、UI 调整、个人需求清单、`.gitea/workflows/*.yml` 等。具体改动清单见 [`FORK.md`](FORK.md)。日常开发分支,**default branch** | 是(rebase 后人工做) |
⚠️ **default branch 是 `custom`**`git clone` 默认 checkout custom,直接是开发分支。
### 历史:曾经存在过的 ci 分支(已退役)
2026-05-02 之前曾经有第三个分支 `ci`,最初设计是把 `.gitea/workflows/*.yml` 单独放它上面以"meta/code 分离"。两周后发现 Gitea Actions runs 列表显示的 commit 是 workflow 文件所在 commit(即 ci 的 HEAD),不是被构建的代码 commit,UX 误导性强。
把 workflow 挪回 custom 之后:
- runs 列表 commit = 真实代码 commit ✅
- `git clone` 默认落 custom 直接是开发分支 ✅
- rebase 上游时 workflow 跟 custom 一起平移 ✅
- 代价:失去"workflow 与代码完全独立"的设计美感 —— 这个分离原本就是过度设计
**ci 分支于 2026-05-02 删除**,仅保留这段说明给后续 Claude 会话理解 git log 里"workflow 文件迁到 custom"这条提交(commit `555ecc1a`)的来龙去脉。**workflow 改动直接在 custom 上做**。
---
## custom 分支 workflow 清单
`.gitea/workflows/` 当前有 2 个 workflow2026-05-04 起 build+deploy 合并为单 workflow 双 job):
| 文件 | 触发 | 干什么 | 状态 |
|---|---|---|---|
| `sync-upstream.yml` | 手动(`workflow_dispatch`,可填 tag | 服务端把 `dev/main` 强制 reset 到 mirror 上的指定 release tag(默认最新),然后 `push --force-with-lease` + 推 tags | ✅ 在用 |
| `build-image.yml` | **自动**push 到 custom 触发,`paths-ignore` 屏蔽 `**.md` / `.gitignore` / `LICENSE` / `screenshot/**` / `sync-upstream.yml`+ 手动备选 | **两个 job 串联在同一 run 里**:①`build` job 装 buildkit v0.13.2 → 登录 Gitea registry → 构建镜像(带 OCI 标签 source/revisionGitea 自动关联包到 repo)→ push 到 `git.zhengchentao.win/dev/ezbookkeeping:<hash>``:latest``build-args: BUILD_PIPELINE=1` 跳过活 API 测试。②`deploy` job (`needs: build`) 登录 registry → clone nas-infra → `docker compose pull && up -d` 重启 ezbookkeeping。私有 nas-infra 需要 `secrets.NAS_INFRA_TOKEN`,公开仓库不需要。UI 上 Actions 列表显示一条 runrun 详情里 dependency graph 显示 build → deploy | ✅ 日常发布通道 + 自动 CD |
**已删**
- `docker-snapshot.yml` / `docker-release.yml`2026-05-02,依赖未配的 `secrets.DOCKER_REPO`,永远失败)
- `deploy.yml`2026-05-04,合并进 `build-image.yml` 作为第二个 job,理由:原先 `workflow_run` 链触发会在 Actions 列表产生两条独立 runUX 割裂;合并后单 run + dependency graph 看 build/deploy 状态一目了然)
需要时再从 git 历史 cherry-pick 回来。
---
## 同步发布流程(rebase 模型)
1. 上游出新 release(如 v1.4.0)→ Gitea pull mirror 自动把 tag 同步到 mirror
2. 人工触发 `Sync from upstream` workflow → 服务端把 dev/main reset 到该 tag
3. 本地 `git fetch && git checkout custom && git rebase origin/main`
4. 解冲突(如有)→ 验证 → `git push --force-with-lease origin custom`
5. **build-image workflow 自动触发**force-push 也算 push 事件),构建新镜像;不需要手动点
日常 feature commit 流程(全自动 CD):
1. 在 custom 上改代码 → commit → push
2. **自动触发 build job**(除非只改了 `**.md` / `.gitignore` / `LICENSE` / `screenshot/**` / `sync-upstream.yml`
3. build 成功 → **同 run 内 deploy job 接力跑**`needs: build` 串联):clone nas-infra → docker compose pull → up -d
4. 整条 push → build → deploy 链路无人工介入,UI 上是单条 run
**并发取消策略**`build-image.yml` 设了 `concurrency.cancel-in-progress: true`,连续多次 push 时**只构建+部署最新那一次**(同 run 里 build/deploy 是原子单元,一起取消)。例:连续 3 次 push 间隔 30 秒,第 1 次 build 跑到 30%、第 2 次到来取消它、第 3 次又取消第 2,最终只 build + deploy 第 3 次的代码。省 CI 时间又保证最终一致性。
如果想跳过 build/deploy(例如手动多次 push 调试),commit 时只改文档相关文件即可(落在 paths-ignore 范围内)。如果想强制重打某个旧 commit,去 Actions UI 手动触发 `Build Docker Image`,填要打包的 branch / tag —— 注意手动触发也会跑 deploy job,**没有"只重新部署不重新 build"的单点入口了**(合并的代价,原 `deploy.yml` 那条路径已废)。临时只想重启容器:直接到 NAS 上 `docker compose up -d` 或在 Actions UI 临时禁用 deploy job。
**为什么 rebase 不 merge**:个人项目,无团队协作语义要保留,线性历史更清爽。
---
## 给后续 Claude 会话的明确提示
- 用户说"我的分支" / "切换到我的分支" → 指 `custom`
- 用户说"rebase main" → 指 `git rebase origin/main`,目标是把 custom 的改动叠到最新上游 tag 之上
- **不要在 `main` 分支上提交任何东西**(会被 CI 覆写)
- **workflow 文件改动直接在 custom 上做**2026-05-02 起,不再是 ci 分支)
- force-push custom 是常规操作,但每次用 `--force-with-lease`,不直接 `--force`
- 如果发现本地配了 upstream remote,那是历史遗留,不要依赖;以 origin/main 为准
- `.claude/``.gitignore` 里(个人本地配置不入库),但 `CLAUDE.md` 本身入库
---
## 同步历史
- **2026-05-01**rebase custom → origin/main (v1.4.0)。22 个 custom-only 提交(含一个旧的 `Merge branch 'main' into myrequirement` commit)压平为 21 个线性提交。已 force-push origin/custom`08c69042``fe265259`)。
- **2026-05-02**:修 Gitea Actions `Build Docker Image` 工作流。三层故障,全部不在本仓库代码里:
- **TLS 雷**`docker login` 走 host 进程不命中 PREROUTING REDIRECT,且 v6 撞 DSM nginx 的 CF Origin Cert。NAS 侧修:iptables 补 OUTPUT 对称规则 + `/etc/hosts` 显式 v4 兜底。详见 obsidian vault [[NAS/notes/内网证书路径]] §三.5/§三.6
- **buildkit 内核兼容**runc 1.2+ 撞 DSM 4.4 内核。`.gitea/workflows/build-image.yml``moby/buildkit:v0.13.2`commit `acdbb5bf`
- **backend 单元测试撞活 API**`pkg/exchangerates/``TestExchangeRatesApiLatestExchangeRateHandler_*` 跑活 API(加拿大银行 / 乌兹别克央行),国内访问超时。upstream Dockerfile 已设 `ARG BUILD_PIPELINE`,测试代码看到 `BUILD_PIPELINE=1 && CHECK_3RD_API!=1` 时早退。修:workflow 加 `build-args: BUILD_PIPELINE=1`commit `2dd8f099`),对齐上游 GH Actions
- **2026-05-02 (后续)**workflow 文件从 ci 分支迁到 customdefault branch 切到 customcommit `555ecc1a`),随后**删掉 ci 分支**。原因:Gitea Actions runs 列表的 commit 字段一直显示 ci 的 workflow commit,不是被构建的代码 commit,UX 误导性强。挪到 custom 后列表直接显示真实代码 commit。同时清理上游残留的 `docker-release.yml` / `docker-snapshot.yml`(依赖未配的 `secrets.DOCKER_REPO`,永远失败)。仓库回到朴素的 main + custom 双分支模型
- **2026-05-02 (numpad fix)**FORK.md #11 定位 + 修复。小键盘点击卡顿真因是 `.numpad-button``touch-action: none`(上游 e178a079 引入)与 F7 tap 处理叠加,改为 `touch-action: manipulation`commit `75b4d78d`
- **2026-05-04**:把 `deploy.yml` 合并进 `build-image.yml` 作为第二个 job`needs: build`),删除 `deploy.yml`。原先 `workflow_run` 链路会在 Actions 列表产生两条独立 runbuild 完一条、deploy 又一条),用户视角割裂;合并后 UI 列表单条 runrun 详情里 dependency graph 显示 build → deploy 串联。代价:失去"不 rebuild 只 redeploy"的 UI 单点触发,临时只想重启容器需直接 ssh NAS 跑 compose。`paths-ignore` 移除已不存在的 `deploy.yml`
## 给后续 Claude 会话:CI 故障排查路径
如果 Gitea Actions build 又炸,按 NAS 域问题 vs 仓库代码问题分别排查:
| 现象 | 大概率位置 | 文档 |
|---|---|---|
| `Login to Gitea Container Registry` 步骤报 `x509: certificate signed by unknown authority` | NAS 网络层(iptables / dnsmasq / DSM nginx 占 443 | obsidian vault `NAS/notes/内网证书路径.md` + `NAS/notes/IPv6 设计.md` |
| `Build and push` 步骤里 `RUN ...` 在第二条之内就炸 `unsafe procfs detected` 之类 | buildkit/runc 与 DSM 内核版本 | `.gitea/workflows/build-image.yml``driver-opts` |
| `Failed to pass unit testing` / `Failed to pass lint checking`build.sh 报) | **先看 Dockerfile 顶部 `ARG`**,多半是 CI 跳过开关没传(如 `BUILD_PIPELINE` / `CHECK_3RD_API` / `SKIP_TESTS`)。**不要先去改测试代码** | `Dockerfile` 顶部 ARG + `.gitea/workflows/build-image.yml``build-args` |
| `actions/checkout` 报 fetch 失败 | Gitea SSH/HTTPS 路径或 token 权限 | gitea-runner 的 `GITEA_RUNNER_REGISTRATION_TOKEN` + NPM `git.zhengchentao.win` 的 Advanced 配置 |
| Dockerfile 里某条指令业务逻辑报错 | 真正的代码问题 | 本仓库 `Dockerfile` |
**通用排查原则**build.sh 报的"测试失败 / lint 失败"先看是不是上游已经设计了 CI 跳过路径。Dockerfile 的 `ARG` + `build.sh` 内的 `os.Getenv()` 检查通常成对出现(如 `BUILD_PIPELINE=1` → 跳过 3rd API 测试,`SKIP_TESTS=...` → 跳过指定测试名)。对齐上游 `.github/actions/` 下的传参,绝大多数情况能直接对齐。
+182
View File
@@ -0,0 +1,182 @@
# ezBookkeeping 个人 fork 改动清单
> 本文件记录这个 fork 相对上游 [mayswind/ezbookkeeping](https://github.com/mayswind/ezbookkeeping) 的所有定制改动 + 进度状态。
> 关联文档:
> - [`CLAUDE.md`](CLAUDE.md) —— 仓库分支模型 / 上游同步流程 / CI 排查路径(meta 层)
> - 部署:见自家 NAS infra repo `git.zhengchentao.win/dev/nas-infra` 的 READMEcompose-level
>
> 标注:❌ 难/暂缓 | ❓ 待定 | 🔍 调查中 | 🟢 已完成
---
## 一、账户功能
### 1. 🟢 信用卡账户:额度与可用额度
**描述:** 为信用卡类型账户新增「信用额度」字段,在账户列表显示可用额度。
**已完成:**
- 后端:`AccountExtend` JSON blob 新增 `CreditLimit` 字段(无需数据库迁移)
- API`AccountCreateRequest` / `AccountModifyRequest` / `AccountInfoResponse` 增加 `creditLimit`
- 前端 model`Account` 类增加 `creditLimit` 字段,同步序列化/反序列化
- 移动端 EditPageCreditCard 分类时显示信用额度输入项(数字键盘)
- 桌面端 EditDialogCreditCard 分类时显示信用额度输入框(`amount-input`
- 移动端 ListPage:账户名下方显示「可用额度: ¥xxx」(= `creditLimit + balance`
- 桌面端 ListPage:账户卡片余额旁显示「Available: ¥xxx」
- 语言包:中英繁均已添加 `"Credit Limit"` / `"Available"`
### 2. 🟢 按账户筛选交易时顶部显示账户信息卡
**描述:** 在按单个账户筛选的交易列表顶部,显示账户图标、名称和余额/可用额度。
**已完成:**
- 仅单账户筛选时(`queryAllFilterAccountIdsCount === 1`)显示,多账户/全量时隐藏
- 信用卡账户显示「欠款 · 可用 ¥xxx」,普通账户显示余额
- 移动端:toolbar 下方插入账户信息卡片;桌面端:日期范围行下方插入 tonal 样式账户卡片
- 多子账户(`MultiSubAccounts`)一级账户:使用 `getAccountSubAccountBalance` 获取汇总余额及正确币种,修复 `currency = '---'` 导致货币符号不显示的问题
- 涉及文件:`src/views/mobile/transactions/ListPage.vue``src/views/desktop/transactions/ListPage.vue`
### 3. 🟢 账户编辑页直接修改余额(自动插入调整记录)
**描述:** 在账户编辑页修改余额字段,保存时自动计算差值并插入一条「余额调整」类型交易。
**已完成:**
- 后端:移除「账户已有交易时不允许添加 ModifyBalance」的限制
- 后端:`Amount``RelatedAccountAmount` 均存储 delta;创建时 `balance += delta`,删除时 `balance -= delta`,修改时 `balance = balance - oldDelta + newDelta`
- 后端响应:`ToTransactionInfoResponse` 对 ModifyBalance 类型返回 `RelatedAccountAmount` 作为 `sourceAmount`
- 前端 store:新增 `adjustAccountBalance({ accountId, targetBalance, currentBalance })` 函数,发送 `sourceAmount = delta`
- 移动端 EditPage:余额字段对已有账户解除只读;保存时若余额变化先调 `adjustAccountBalance`;捕获 `NothingWillBeUpdated (200004)` 视为成功
- 桌面端 EditDialog:同上逻辑,支持多子账户逐一调整
- 「调整余额」入口仅在账户编辑页,已从账户列表「更多」菜单移除
- 涉及文件:`pkg/services/transactions.go``pkg/models/transaction.go``src/stores/transaction.ts``src/views/mobile/accounts/EditPage.vue``src/views/desktop/accounts/list/dialogs/EditDialog.vue`
---
## 二、记账页面
### 4. 🟢 记账页选择账户后显示余额/可用额度
**描述:** 在记账页面选择账户后,在账户行显示该账户的当前余额或信用卡可用额度。
**已完成:**
- 信用卡账户(有 `creditLimit`):显示「欠款金额 · 可用 ¥xxx」
- 普通负债账户:显示欠款正数;普通资产账户:显示余额
- 转账类型时源账户和目标账户均显示
- 移动端:账户列表项 `footer` 字段;桌面端:`two-column-select``custom-selection-secondary-text`
- 涉及文件:`src/views/mobile/transactions/EditPage.vue``src/views/desktop/transactions/list/dialogs/EditDialog.vue`
### 5. ❓ 记录上次选择的账户(待定)
**描述:** 新建交易时,默认选中上次使用的账户。
**实现思路:**
- 保存账户 ID 到 `localStorage`,打开记账页时读取并预选
- 涉及文件:`src/views/mobile/transactions/EditPage.vue``src/lib/settings.ts`
---
## 三、小键盘
### 6. 🟢 小键盘布局调整
**描述:** 调整数字键盘布局。
**已完成:**
```
[数值显示区 ] [ ⌫ ]
7 8 9 ×
4 5 6
1 2 3 +
C 0 . OK
```
- ⌫ 单击退格,长按清除;C 清除全部
- 涉及文件:`src/components/mobile/NumberPadSheet.vue`
---
## 四、交易详情
### 7. 🟢 交易详情页增加「编辑」和「删除」入口(仅移动端)
**描述:** 移动端交易详情页三点菜单中增加编辑和删除操作。PC 端详情直接在编辑弹窗中展示,无需额外入口。
**已完成:**
- 三点菜单:第一项「Edit」跳转编辑页,最后一项红色「Delete」确认后删除并返回
- 从详情返回编辑页后自动刷新数据
- 涉及文件:`src/views/mobile/transactions/EditPage.vue`
---
## 五、交易时间选择
### 8. ❌ 点击交易时间标题默认打开日期选择(已回滚)
**描述:** 原想让点击「Transaction Time」标题行时默认弹日期选择器。
**为何回滚:** 改动改的是 `template #header` 那行 label 的点击 handler`'time'``'date'`),实际操作中用户点的是 `template #title` 里的日期/时间文本。上游早在 commit `368322f9` 已实现"点哪走哪"的智能路由——点日期开日期选择器、点时间开时间选择器。所以这条改动**用户视角无可见差异**,纯空改,回滚到上游行为。
**留档教训:** 改 UI 行为前先把"用户实际点哪个元素"摸清楚,别只看着 DOM 结构想当然。`#header` slot 只是上方的 label 行,正常用户极少触发。
---
## 六、分类选择
### 9. 🟢 分类选择默认全部展开(仅移动端)
**描述:** 在移动端记账页选择分类时,默认展开所有一级分类。PC 端使用不同的分类选择组件,无需此设置。
**已完成:**
- 设置存 localStorage(字段 `expandCategoryTreeByDefault`,默认 `false`),即时生效无需 reload;已加入云同步白名单(`ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES`),可跨设备同步
- `TreeViewSelectionSheet` 新增可选 prop `defaultExpanded``:opened` 改为 `props.defaultExpanded || isPrimaryItemHasSecondaryValue(item)`
- 移动端 EditPage 三个分类 sheet(支出/收入/转账)均传入 `:default-expanded`
- 设置仅在移动端设置页显示,PC 端无对应入口
- 涉及文件:`src/components/mobile/TreeViewSelectionSheet.vue``src/views/mobile/transactions/EditPage.vue``src/views/mobile/SettingsPage.vue`
---
## 七、性能与动画
### 10. 🟢 全局动画加速(仅移动端)
**描述:** 移动端全局页面跳转及各类弹层动画加速。
**已完成:**
- 页面跳转、Sheet、ActionSheet、Popup、Dialog、Popover 动画时长从 300ms → 150ms
- Tab 切换动画保持原样(设置中已有开关可控制)
- 涉及文件:`src/styles/mobile/global.scss`
### 11. 🟢 小键盘点击卡顿(修正:范围非全局)
**描述:** 移动端点击按钮有延迟感。
**真因(2026-05-02 定位):** **不是**全局点击/接口响应问题。诊断后确认仅小键盘有卡顿,其他按钮正常。根因是上游在 `.numpad-button` 上设了 `touch-action: none`commit `e178a079` "code refactor" by MaysWind),与 F7 内部 tap 处理叠加后让 click 事件合成慢一拍。backspace(个人新增 `.numpad-backspace-button` 类)不受影响,刚好佐证范围。
**已完成:**
- `.numpad-button``touch-action: none` 改为 `touch-action: manipulation`
- `manipulation` 是 W3C 标准的"快速点击"值:禁双击缩放(消除老 300ms 延迟)但保留 click 事件正常合成
- 涉及文件:`src/components/mobile/NumberPadSheet.vue`
**附带认知:**#11 假设是"全局点击响应慢"或"接口慢",与 #12 离线缓存挂钩调研。实际诊断后跟那两条都无关,纯 CSS `touch-action` 与框架 tap 处理叠加导致。该认知值得记录避免后续误诊路径。
---
## 八、离线 / 缓存
### 12. ❌ 本地优先 / 离线数据缓存(暂缓)
**描述:** 交易数据本地缓存,优先展示缓存数据,后台静默拉取更新。
**现状:** Service Worker 已实现静态资源缓存,但交易业务数据目前不做本地缓存。
**为何难:**
- 需引入 IndexedDB 存储交易/账户/分类数据
- 需处理本地与服务端的数据同步、冲突解决
- 属于架构级改动,工作量较大
---
## 进度总览
| # | 需求 | 状态 |
|---|------|------|
| 1 | 信用卡额度 | 🟢 已完成 |
| 2 | 账户信息卡片 | 🟢 已完成 |
| 3 | 调整余额入口 | 🟢 已完成 |
| 4 | 记账页显示余额 | 🟢 已完成 |
| 5 | 记住上次账户 | ❓ 待定 |
| 6 | 小键盘布局 | 🟢 已完成 |
| 7 | 详情编辑/删除 | 🟢 已完成 |
| 8 | 点击时间默认日期 | ❌ 已回滚(无效改动) |
| 9 | 分类默认展开 | 🟢 已完成 |
| 10 | 全局动画加速 | 🟢 已完成 |
| 11 | 小键盘点击卡顿(touch-action 修复) | 🟢 已完成 |
| 12 | 离线缓存 | ❌ 暂缓 |
+1
View File
@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2020-2026 MaysWind (i@mayswind.net)
Copyright (c) 2026 Zhengchen Tao (fork modifications)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+47 -22
View File
@@ -1,4 +1,29 @@
# ezBookkeeping
> ## Personal fork notice
>
> This repository is a personal fork of [mayswind/ezbookkeeping](https://github.com/mayswind/ezbookkeeping) (MIT) with the following custom additions on top of upstream releases:
>
> - **Credit card accounts**: credit-limit field; account list shows available credit
> - **Account-filtered transactions**: when filtering by a single account, show an account info card on top (icon / name / balance / available credit)
> - **Account editing**: edit the balance field directly; a "balance adjustment" transaction is generated automatically
> - **Add-transaction page**: live display of balance or available credit after selecting an account
> - **Numpad**: custom layout (4-column calculator style) + `touch-action` fix for tap latency
> - **Mobile animations**: generic transitions 300ms → 150ms
> - **Transaction detail**: edit / delete entries added to the mobile three-dot menu
> - **Category picker**: optional "expand all by default" on mobile (cloud-sync allowlisted)
>
> Full list with implementation details: [`FORK.md`](FORK.md)
> Branch model / upstream sync / CI troubleshooting: [`CLAUDE.md`](CLAUDE.md)
>
> All modifications are released under the same MIT License as upstream — see [`LICENSE`](LICENSE).
>
> ---
>
> Upstream README content follows below.
---
[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
[![Go Report](https://goreportcard.com/badge/github.com/mayswind/ezbookkeeping)](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping)
[![Latest Release](https://img.shields.io/github/release/mayswind/ezbookkeeping.svg?style=flat)](https://github.com/mayswind/ezbookkeeping/releases)
@@ -34,7 +59,7 @@ Live Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.
- **AI-Powered Features**
- Receipt image recognition
- MCP (Model Context Protocol) support for AI integration
- API command-line script tools for AI integration
- Agent Skill and API command-line script tools support for AI integration
- **Powerful Bookkeeping**
- Two-level accounts and categories
- Image attachments for transactions
@@ -129,27 +154,27 @@ Help make ezBookkeeping accessible to users around the world. We welcome help to
Currently available translations:
| Tag | Language | Contributors |
| --- | --- | --- |
| de | Deutsch | [@chrgm](https://github.com/chrgm) |
| en | English | / |
| es | Español | [@Miguelonlonlon](https://github.com/Miguelonlonlon), [@abrugues](https://github.com/abrugues), [@AndresTeller](https://github.com/AndresTeller), [@diegofercri](https://github.com/diegofercri) |
| fr | Français | [@brieucdlf](https://github.com/brieucdlf) |
| it | Italiano | [@waron97](https://github.com/waron97) |
| ja | 日本語 | [@tkymmm](https://github.com/tkymmm) |
| kn | ಕನ್ನಡ | [@Darshanbm05](https://github.com/Darshanbm05) |
| ko | 한국어 | [@overworks](https://github.com/overworks) |
| nl | Nederlands | [@automagics](https://github.com/automagics) |
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus), [@balaios](https://github.com/balaios) |
| ru | Русский | [@artegoser](https://github.com/artegoser), [@dshemin](https://github.com/dshemin) |
| sl | Slovenščina | [@thehijacker](https://github.com/thehijacker) |
| ta | தமிழ் | [@hhharsha36](https://github.com/hhharsha36) |
| th | ไทย | [@natthavat28](https://github.com/natthavat28) |
| tr | Türkçe | [@aydnykn](https://github.com/aydnykn) |
| uk | Українська | [@nktlitvinenko](https://github.com/nktlitvinenko) |
| vi | Tiếng Việt | [@f97](https://github.com/f97) |
| zh-Hans | 中文 (简体) | / |
| zh-Hant | 中文 (繁體) | / |
| Tag | Language | Progress | Contributors |
| --- | --- | --- | --- |
| de | Deutsch | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fde.json) | [@chrgm](https://github.com/chrgm), [@1270o1](https://github.com/1270o1) |
| en | English | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fen.json) | / |
| es | Español | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fes.json) | [@Miguelonlonlon](https://github.com/Miguelonlonlon), [@abrugues](https://github.com/abrugues), [@AndresTeller](https://github.com/AndresTeller), [@diegofercri](https://github.com/diegofercri) |
| fr | Français | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Ffr.json) | [@brieucdlf](https://github.com/brieucdlf) |
| it | Italiano | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fit.json) | [@waron97](https://github.com/waron97) |
| ja | 日本語 | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fja.json) | [@tkymmm](https://github.com/tkymmm) |
| kn | ಕನ್ನಡ | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fkn.json) | [@Darshanbm05](https://github.com/Darshanbm05) |
| ko | 한국어 | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fko.json) | [@overworks](https://github.com/overworks) |
| nl | Nederlands | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fnl.json) | [@automagics](https://github.com/automagics) |
| pt-BR | Português (Brasil) | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fpt-BR.json) | [@thecodergus](https://github.com/thecodergus), [@balaios](https://github.com/balaios) |
| ru | Русский | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fru.json) | [@artegoser](https://github.com/artegoser), [@dshemin](https://github.com/dshemin) |
| sl | Slovenščina | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fsl.json) | [@thehijacker](https://github.com/thehijacker) |
| ta | தமிழ் | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fta.json) | [@hhharsha36](https://github.com/hhharsha36) |
| th | ไทย | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fth.json) | [@natthavat28](https://github.com/natthavat28) |
| tr | Türkçe | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Ftr.json) | [@aydnykn](https://github.com/aydnykn) |
| uk | Українська | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fuk.json) | [@nktlitvinenko](https://github.com/nktlitvinenko) |
| vi | Tiếng Việt | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fvi.json) | [@f97](https://github.com/f97) |
| zh-Hans | 中文 (简体) | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fzh-Hans.json) | / |
| zh-Hant | 中文 (繁體) | ![Translation Progress](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmayswind%2FezBookkeeping-i18n-badge%2Fmain%2Fbadges%2Fzh-Hant.json) | / |
## Documentation
1. [English](https://ezbookkeeping.mayswind.net)
+2 -1
View File
@@ -14,7 +14,8 @@
],
"translators": {
"de": [
"chrgm"
"chrgm",
"1270o1"
],
"en": [],
"es": [
+29 -28
View File
@@ -1,33 +1,33 @@
module github.com/mayswind/ezbookkeeping
go 1.25
go 1.25.0
require (
github.com/boombuler/barcode v1.1.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
github.com/gin-contrib/cache v1.4.1
github.com/gin-contrib/gzip v1.2.5
github.com/gin-gonic/gin v1.11.0
github.com/gin-contrib/cache v1.4.3
github.com/gin-contrib/gzip v1.2.6
github.com/gin-gonic/gin v1.12.0
github.com/go-co-op/gocron/v2 v2.19.1
github.com/go-playground/validator/v10 v10.30.1
github.com/go-playground/validator/v10 v10.30.2
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/invopop/jsonschema v0.13.0
github.com/lib/pq v1.11.1
github.com/mattn/go-sqlite3 v1.14.33
github.com/minio/minio-go/v7 v7.0.98
github.com/lib/pq v1.12.1
github.com/mattn/go-sqlite3 v1.14.38
github.com/minio/minio-go/v7 v7.0.99
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pquerna/otp v1.5.0
github.com/sirupsen/logrus v1.9.4
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.6.2
github.com/urfave/cli/v3 v3.8.0
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/xuri/excelize/v2 v2.10.0
golang.org/x/crypto v0.47.0
golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.34.0
golang.org/x/text v0.33.0
github.com/xuri/excelize/v2 v2.10.1
golang.org/x/crypto v0.49.0
golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.36.0
golang.org/x/text v0.35.0
gopkg.in/ini.v1 v1.67.1
gopkg.in/mail.v2 v2.3.1
xorm.io/builder v0.3.13
@@ -40,8 +40,8 @@ require (
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
@@ -52,14 +52,14 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.9.2 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -80,30 +80,31 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/richardlehane/mscfb v1.0.6 // indirect
github.com/richardlehane/msoleps v1.0.6 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/tealeg/xlsx v1.0.5 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // indirect
github.com/tiendc/go-deepcopy v1.7.2 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/tools v0.40.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+54 -55
View File
@@ -14,10 +14,10 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
@@ -43,16 +43,16 @@ github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7/go.mod h1:GPpMrAfHdb8
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b h1:jqW/h4gcXYEB6kVf6iuxjU9ONWA0ugUB94TP9UNmgdg=
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b/go.mod h1:iACcgahst7BboCpIMSpnFs4SKyU9ZjsvZBfNbUxZOJI=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cache v1.4.1 h1:HcLwLfw7p+FasNp5VAnFbbBj9SzB4bDtswvon7wYSg4=
github.com/gin-contrib/cache v1.4.1/go.mod h1:tykDV+FgItJHYEO0eCasuRYsZKPPyb4BYhAjuTlG6RM=
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cache v1.4.3 h1:6rmIlmTf2Vyfd/ue53+BLdTxC7hrQ7FqRgfjz31nEEE=
github.com/gin-contrib/cache v1.4.3/go.mod h1:Znf5Qa8HTQ+QHku6ODf72WOPnJ2fHUd2nXD6mSi+6+g=
github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg=
github.com/gin-contrib/gzip v1.2.6/go.mod h1:BQy8/+JApnRjAVUplSGZiVtD2k8GmIE2e9rYu/hLzzU=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-co-op/gocron/v2 v2.19.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI=
github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
@@ -63,14 +63,14 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -104,22 +104,22 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI=
github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.12.1 h1:x1nbl/338GLqeDJ/FAiILallhAsqubLzEZu/pXtHUow=
github.com/lib/pq v1.12.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
github.com/minio/minio-go/v7 v7.0.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE=
github.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -139,15 +139,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko=
@@ -167,58 +166,58 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
+1359 -1624
View File
File diff suppressed because it is too large Load Diff
+57 -57
View File
@@ -1,6 +1,6 @@
{
"name": "ezbookkeeping",
"version": "1.4.0",
"version": "1.5.0",
"private": true,
"repository": {
"type": "git",
@@ -19,64 +19,64 @@
"test": "cross-env TS_NODE_PROJECT=\"./tsconfig.jest.json\" jest"
},
"dependencies": {
"@mdi/js": "^7.4.47",
"@vuepic/vue-datepicker": "^12.1.0",
"axios": "^1.13.4",
"cbor-js": "^0.1.0",
"chardet": "^2.1.1",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dom7": "^4.0.6",
"echarts": "^6.0.0",
"framework7": "^9.0.3",
"framework7-icons": "^5.0.5",
"framework7-vue": "^9.0.3",
"jalaali-js": "^1.2.8",
"leaflet": "^1.9.4",
"line-awesome": "^1.3.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"pinia": "^3.0.4",
"register-service-worker": "^1.7.2",
"skeleton-elements": "^4.0.1",
"swiper": "^12.1.0",
"ua-parser-js": "^1.0.39",
"vue": "^3.5.27",
"vue-echarts": "^8.0.1",
"vue-i18n": "^11.2.8",
"vue-router": "^5.0.2",
"vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.11.8"
"@mdi/js": "7.4.47",
"@vuepic/vue-datepicker": "12.1.0",
"axios": "1.14.0",
"cbor-js": "0.1.0",
"chardet": "2.1.1",
"clipboard": "2.0.11",
"crypto-js": "4.2.0",
"dom7": "4.0.6",
"echarts": "6.0.0",
"framework7": "9.0.3",
"framework7-icons": "5.0.5",
"framework7-vue": "9.0.3",
"jalaali-js": "1.2.8",
"leaflet": "1.9.4",
"line-awesome": "1.3.0",
"moment": "2.30.1",
"moment-timezone": "0.6.1",
"pinia": "3.0.4",
"register-service-worker": "1.7.2",
"skeleton-elements": "4.0.1",
"swiper": "12.1.3",
"ua-parser-js": "1.0.39",
"vue": "3.5.31",
"vue-echarts": "8.0.1",
"vue-i18n": "11.3.0",
"vue-router": "5.0.4",
"vue3-perfect-scrollbar": "2.0.0",
"vuedraggable": "4.1.0",
"vuetify": "3.12.4"
},
"devDependencies": {
"@jest/globals": "^30.2.0",
"@tsconfig/node24": "^24.0.4",
"@types/cbor-js": "^0.1.1",
"@types/crypto-js": "^4.2.2",
"@types/git-rev-sync": "^2.0.2",
"@types/jalaali-js": "^1.2.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.1.0",
"@types/ua-parser-js": "^0.7.39",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"eslint-plugin-vue": "^10.7.0",
"git-rev-sync": "^3.0.2",
"jest": "^30.2.0",
"postcss-preset-env": "^11.1.3",
"sass": "^1.97.3",
"ts-jest": "^29.4.6",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-checker": "^0.12.0",
"vite-plugin-pwa": "^1.2.0",
"vite-plugin-vuetify": "^2.1.3",
"vue-tsc": "^3.2.4"
"@jest/globals": "30.3.0",
"@tsconfig/node24": "24.0.4",
"@types/cbor-js": "0.1.1",
"@types/crypto-js": "4.2.2",
"@types/git-rev-sync": "2.0.2",
"@types/jalaali-js": "1.2.0",
"@types/jest": "30.0.0",
"@types/node": "25.5.0",
"@types/ua-parser-js": "0.7.39",
"@vitejs/plugin-vue": "6.0.5",
"@vue/eslint-config-typescript": "14.7.0",
"@vue/tsconfig": "0.9.1",
"cross-env": "10.1.0",
"eslint": "10.1.0",
"eslint-plugin-vue": "10.8.0",
"git-rev-sync": "3.0.2",
"jest": "30.3.0",
"postcss-preset-env": "11.2.0",
"sass": "1.98.0",
"ts-jest": "29.4.6",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite": "7.3.1",
"vite-plugin-checker": "0.12.0",
"vite-plugin-pwa": "1.2.0",
"vite-plugin-vuetify": "2.1.3",
"vue-tsc": "3.2.6"
},
"browserslist": [
"last 5 Chrome versions",
+12
View File
@@ -713,6 +713,9 @@ func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.
if !isSubAccount && accountCreateReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD {
accountExtend.CreditCardStatementDate = &accountCreateReq.CreditCardStatementDate
if accountCreateReq.CreditLimit > 0 {
accountExtend.CreditLimit = &accountCreateReq.CreditLimit
}
}
return &models.Account{
@@ -769,6 +772,9 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
if !isSubAccount && accountModifyReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD {
newAccountExtend.CreditCardStatementDate = &accountModifyReq.CreditCardStatementDate
if accountModifyReq.CreditLimit > 0 {
newAccountExtend.CreditLimit = &accountModifyReq.CreditLimit
}
}
newAccount := &models.Account{
@@ -804,6 +810,12 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
return newAccount
}
if newAccountExtend.CreditLimit != oldAccountExtend.CreditLimit {
if newAccountExtend.CreditLimit == nil || oldAccountExtend.CreditLimit == nil || *newAccountExtend.CreditLimit != *oldAccountExtend.CreditLimit {
return newAccount
}
}
return nil
}
+9
View File
@@ -91,6 +91,7 @@ type Account struct {
// AccountExtend represents account extend data stored in database
type AccountExtend struct {
CreditCardStatementDate *int `json:"creditCardStatementDate"`
CreditLimit *int64 `json:"creditLimit"`
}
// AccountCreateRequest represents all parameters of account creation request
@@ -105,6 +106,7 @@ type AccountCreateRequest struct {
BalanceTime int64 `json:"balanceTime"`
Comment string `json:"comment" binding:"max=255"`
CreditCardStatementDate int `json:"creditCardStatementDate" binding:"min=0,max=28"`
CreditLimit int64 `json:"creditLimit" binding:"min=0"`
SubAccounts []*AccountCreateRequest `json:"subAccounts" binding:"omitempty"`
ClientSessionId string `json:"clientSessionId"`
}
@@ -121,6 +123,7 @@ type AccountModifyRequest struct {
BalanceTime *int64 `json:"balanceTime" binding:"omitempty"`
Comment string `json:"comment" binding:"max=255"`
CreditCardStatementDate int `json:"creditCardStatementDate" binding:"min=0,max=28"`
CreditLimit int64 `json:"creditLimit" binding:"min=0"`
Hidden bool `json:"hidden"`
SubAccounts []*AccountModifyRequest `json:"subAccounts" binding:"omitempty"`
ClientSessionId string `json:"clientSessionId"`
@@ -171,6 +174,7 @@ type AccountInfoResponse struct {
Balance int64 `json:"balance"`
Comment string `json:"comment"`
CreditCardStatementDate *int `json:"creditCardStatementDate,omitempty"`
CreditLimit *int64 `json:"creditLimit,omitempty"`
DisplayOrder int32 `json:"displayOrder"`
IsAsset bool `json:"isAsset,omitempty"`
IsLiability bool `json:"isLiability,omitempty"`
@@ -181,10 +185,14 @@ type AccountInfoResponse struct {
// ToAccountInfoResponse returns a view-object according to database model
func (a *Account) ToAccountInfoResponse() *AccountInfoResponse {
var creditCardStatementDate *int
var creditLimit *int64
if a.ParentAccountId == LevelOneAccountParentId && a.Category == ACCOUNT_CATEGORY_CREDIT_CARD {
if a.Extend != nil {
creditCardStatementDate = a.Extend.CreditCardStatementDate
if a.Extend.CreditLimit != nil && *a.Extend.CreditLimit > 0 {
creditLimit = a.Extend.CreditLimit
}
} else {
creditCardStatementDate = &defaultCreditCardAccountStatementDate
}
@@ -202,6 +210,7 @@ func (a *Account) ToAccountInfoResponse() *AccountInfoResponse {
Balance: a.Balance,
Comment: a.Comment,
CreditCardStatementDate: creditCardStatementDate,
CreditLimit: creditLimit,
DisplayOrder: a.DisplayOrder,
IsAsset: assetAccountCategory[a.Category],
IsLiability: liabilityAccountCategory[a.Category],
+3 -1
View File
@@ -544,7 +544,9 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *
destinationAccountId := int64(0)
destinationAmount := int64(0)
if t.Type == TRANSACTION_DB_TYPE_TRANSFER_OUT {
if t.Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE {
sourceAmount = t.RelatedAccountAmount // always return delta
} else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_OUT {
destinationAccountId = t.RelatedAccountId
destinationAmount = t.RelatedAccountAmount
} else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_IN {
+2 -11
View File
@@ -954,7 +954,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
if transaction.Amount != oldTransaction.Amount {
if oldTransaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
transaction.RelatedAccountAmount = oldTransaction.RelatedAccountAmount + transaction.Amount - oldTransaction.Amount
transaction.RelatedAccountAmount = transaction.Amount // Amount IS the new delta
updateCols = append(updateCols, "related_account_amount")
}
@@ -2251,17 +2251,8 @@ func (s *TransactionService) doCreateTransaction(c core.Context, database *datas
// Verify balance modification transaction and calculate real amount
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
otherTransactionExists, err := sess.Cols("uid", "deleted", "account_id").Where("uid=? AND deleted=? AND account_id=?", transaction.Uid, false, sourceAccount.AccountId).Limit(1).Exist(&models.Transaction{})
if err != nil {
log.Errorf(c, "[transactions.doCreateTransaction] failed to get whether other transactions exist, because %s", err.Error())
return err
} else if otherTransactionExists {
return errs.ErrBalanceModificationTransactionCannotAddWhenNotEmpty
}
transaction.RelatedAccountId = transaction.AccountId
transaction.RelatedAccountAmount = transaction.Amount - sourceAccount.Balance
transaction.RelatedAccountAmount = transaction.Amount // Amount IS the delta
} else { // Not allow to add transaction before balance modification transaction
otherTransactionExists := false
+34 -11
View File
@@ -6,8 +6,11 @@
<div class="margin-top padding-horizontal" v-if="hint">
<span>{{ hint }}</span>
</div>
<div class="numpad-values" @click="onDisplayValueClick">
<span id="numpad-value" class="numpad-value" :class="currentDisplayNumClass">{{ currentDisplay }}</span>
<div class="numpad-values">
<span id="numpad-value" class="numpad-value" :class="currentDisplayNumClass" @click="onDisplayValueClick">{{ currentDisplay }}</span>
<f7-button class="numpad-backspace-button" @click="backspace" @taphold="clear()">
<f7-icon class="icon-with-direction" f7="delete_left"></f7-icon>
</f7-button>
</div>
<f7-popover class="numpad-paste-popover" target-el="#numpad-value"
@@ -54,20 +57,18 @@
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('+')">
<span class="numpad-button-text numpad-button-text-normal">&plus;</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="clear()">
<span class="numpad-button-text numpad-button-text-normal">C</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(0)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[0] }}</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" v-if="supportDecimalSeparator" @click="inputDecimalSeparator()">
<span class="numpad-button-text numpad-button-text-normal">{{ decimalSeparator }}</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" v-if="!supportDecimalSeparator" @click="inputDoubleNum(0)">
<span class="numpad-button-text numpad-button-text-normal">{{ `${digits[0]}${digits[0]}` }}</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(0)">
<span class="numpad-button-text numpad-button-text-normal">{{ digits[0] }}</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="backspace" @taphold="clear()">
<span class="numpad-button-text numpad-button-text-normal">
<f7-icon class="icon-with-direction" f7="delete_left"></f7-icon>
</span>
</f7-button>
<f7-button class="numpad-button numpad-button-confirm no-right-border no-bottom-border" fill @click="confirm()">
<span :class="{ 'numpad-button-text': true, 'numpad-button-text-confirm': !currentSymbol }">{{ confirmText }}</span>
</f7-button>
@@ -467,12 +468,15 @@ watch(() => props.flipNegative, (newValue) => {
}
.numpad-values {
display: flex;
align-items: center;
border-bottom: 1px solid var(--f7-page-bg-color);
}
.numpad-value {
display: flex;
position: relative;
flex: 1;
padding-inline-start: 16px;
line-height: 1;
height: var(--ebk-numpad-value-height);
@@ -481,6 +485,22 @@ watch(() => props.flipNegative, (newValue) => {
user-select: none;
}
.numpad-backspace-button {
display: flex;
align-items: center;
justify-content: center;
width: 20%;
height: var(--ebk-numpad-value-height);
font-size: 22px;
color: var(--f7-color-black);
flex-shrink: 0;
border-left: 1px solid var(--f7-page-bg-color);
}
.dark .numpad-backspace-button {
color: var(--f7-color-white);
}
.numpad-value-small {
font-size: var(--ebk-numpad-value-small-font-size);
}
@@ -511,7 +531,10 @@ watch(() => props.flipNegative, (newValue) => {
align-items: center;
box-sizing: border-box;
user-select: none;
touch-action: none;
/* 上游设的 touch-action: none 在 F7 tap 处理下让 click 慢一拍(小键盘卡顿
的实际根因,不是网络/渲染)。改 manipulation:禁双击缩放 + 消除 300ms
老延迟,但保留 click 正常合成。详见 FORK.md #11 */
touch-action: manipulation;
}
.numpad-button-num {
@@ -21,7 +21,7 @@
</f7-list>
<f7-treeview class="tree-view-selection-treeview">
<f7-treeview-item item-toggle
:opened="isPrimaryItemHasSecondaryValue(item)"
:opened="props.defaultExpanded || isPrimaryItemHasSecondaryValue(item)"
:label="ti((primaryTitleField ? item[primaryTitleField] : item) as string, !!primaryTitleI18n)"
:key="primaryKeyField ? item[primaryKeyField] : item"
v-for="item in filteredItems">
@@ -59,6 +59,7 @@ import { type Framework7Dom, scrollSheetToTop } from '@/lib/ui/mobile.ts';
interface MobileTwoLevelItemSelectionBaseProps extends TwoLevelItemSelectionBaseProps {
show: boolean;
defaultExpanded?: boolean;
}
const props = defineProps<MobileTwoLevelItemSelectionBaseProps>();
+3
View File
@@ -36,6 +36,7 @@ export interface ApplicationSettings extends BaseApplicationSetting {
showAccountBalance: boolean;
swipeBack: boolean;
animate: boolean;
expandCategoryTreeByDefault: boolean;
// Application Lock
applicationLock: boolean;
applicationLockWebAuthn: boolean;
@@ -134,6 +135,7 @@ export const ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES: Record<string, UserAp
'autoSaveTransactionDraft': UserApplicationCloudSettingType.String,
'autoGetCurrentGeoLocation': UserApplicationCloudSettingType.Boolean,
'alwaysShowTransactionPicturesInMobileTransactionEditPage': UserApplicationCloudSettingType.Boolean,
'expandCategoryTreeByDefault': UserApplicationCloudSettingType.Boolean,
// Import Transaction Dialog
'rememberLastSelectedFileTypeInImportTransactionDialog': UserApplicationCloudSettingType.Boolean,
'lastSelectedFileTypeInImportTransactionDialog': UserApplicationCloudSettingType.String,
@@ -174,6 +176,7 @@ export const DEFAULT_APPLICATION_SETTINGS: ApplicationSettings = {
showAccountBalance: true,
swipeBack: true,
animate: true,
expandCategoryTreeByDefault: false,
// Application Lock
applicationLock: false,
applicationLockWebAuthn: false,
+742 -742
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -1860,6 +1860,8 @@
"Balance Time": "Balance Time",
"Sub-account Balance Time": "Sub-account Balance Time",
"Statement Date": "Statement Date",
"Credit Limit": "Credit Limit",
"Available": "Available",
"Description": "Description",
"Your account description (optional)": "Your account description (optional)",
"Your sub-account description (optional)": "Your sub-account description (optional)",
@@ -2280,6 +2282,7 @@
"Exchange Rate": "Exchange Rate",
"Enable Swipe Back": "Enable Swipe Back",
"Enable Animation": "Enable Animation",
"Expand Category List By Default": "Expand Category List By Default",
"Basic Information": "Basic Information",
"User Information": "User Information",
"Already have an account?": "Already have an account?",
+3
View File
@@ -1860,6 +1860,8 @@
"Balance Time": "余额时间",
"Sub-account Balance Time": "子账户余额时间",
"Statement Date": "账单日",
"Credit Limit": "信用额度",
"Available": "可用额度",
"Description": "描述",
"Your account description (optional)": "你的账户描述 (可选)",
"Your sub-account description (optional)": "你的子账户描述 (可选)",
@@ -2280,6 +2282,7 @@
"Exchange Rate": "汇率",
"Enable Swipe Back": "启用侧滑返回",
"Enable Animation": "启用动画",
"Expand Category List By Default": "默认展开分类列表",
"Basic Information": "基本信息",
"User Information": "用户信息",
"Already have an account?": "已经有账号?",
+3
View File
@@ -1889,6 +1889,8 @@
"Are you sure you want to move all transactions?": "您確定要移動所有交易?",
"Unable to move transactions": "無法移動交易",
"All transactions in this account have been moved.": "此帳戶中的所有交易已被移動。",
"Credit Limit": "信用額度",
"Available": "可用額度",
"Reconciliation Statement": "對帳單",
"Account Balance Trends": "帳戶餘額趨勢",
"Update Closing Balance": "更新期末餘額",
@@ -2280,6 +2282,7 @@
"Exchange Rate": "匯率",
"Enable Swipe Back": "啟用滑動返回",
"Enable Animation": "啟用動畫",
"Expand Category List By Default": "預設展開分類列表",
"Basic Information": "基本資訊",
"User Information": "使用者資訊",
"Already have an account?": "已經有帳號?",
+21 -6
View File
@@ -17,6 +17,7 @@ export class Account implements AccountInfoResponse {
public balanceTime?: number;
public comment: string;
public creditCardStatementDate?: number;
public creditLimit?: number;
public displayOrder: number;
public visible: boolean;
public subAccounts?: Account[];
@@ -24,7 +25,7 @@ export class Account implements AccountInfoResponse {
private readonly _isAsset?: boolean;
private readonly _isLiability?: boolean;
protected constructor(id: string, name: string, parentId: string, category: number, type: number, icon: string, color: string, currency: string, balance: number, comment: string, displayOrder: number, visible: boolean, balanceTime?: number, creditCardStatementDate?: number, isAsset?: boolean, isLiability?: boolean, subAccounts?: Account[]) {
protected constructor(id: string, name: string, parentId: string, category: number, type: number, icon: string, color: string, currency: string, balance: number, comment: string, displayOrder: number, visible: boolean, balanceTime?: number, creditCardStatementDate?: number, isAsset?: boolean, isLiability?: boolean, subAccounts?: Account[], creditLimit?: number) {
this.id = id;
this.name = name;
this.parentId = parentId;
@@ -39,6 +40,7 @@ export class Account implements AccountInfoResponse {
this.displayOrder = displayOrder;
this.visible = visible;
this.creditCardStatementDate = creditCardStatementDate;
this.creditLimit = creditLimit;
this._isAsset = isAsset;
this._isLiability = isLiability;
@@ -95,7 +97,8 @@ export class Account implements AccountInfoResponse {
this.comment === other.comment &&
this.displayOrder === other.displayOrder &&
this.visible === other.visible &&
this.creditCardStatementDate === other.creditCardStatementDate;
this.creditCardStatementDate === other.creditCardStatementDate &&
this.creditLimit === other.creditLimit;
if (!isEqual) {
return false;
@@ -130,6 +133,7 @@ export class Account implements AccountInfoResponse {
this.balanceTime = other.balanceTime;
this.comment = other.comment;
this.creditCardStatementDate = other.creditCardStatementDate;
this.creditLimit = other.creditLimit;
this.visible = other.visible;
}
@@ -181,6 +185,7 @@ export class Account implements AccountInfoResponse {
balanceTime: (parentAccount || this.type === AccountType.SingleAccount.type) && this.balanceTime ? this.balanceTime : 0,
comment: this.comment,
creditCardStatementDate: !parentAccount && this.category === AccountCategory.CreditCard.type ? this.creditCardStatementDate : undefined,
creditLimit: !parentAccount && this.category === AccountCategory.CreditCard.type ? (this.creditLimit ?? 0) : undefined,
subAccounts: !parentAccount ? subAccountCreateRequests : undefined,
clientSessionId: !parentAccount ? clientSessionId : undefined
};
@@ -214,6 +219,7 @@ export class Account implements AccountInfoResponse {
balanceTime: parentAccount && (!this.id || this.id === '0') ? this.balanceTime : undefined,
comment: this.comment,
creditCardStatementDate: !parentAccount && this.category === AccountCategory.CreditCard.type ? this.creditCardStatementDate : undefined,
creditLimit: !parentAccount && this.category === AccountCategory.CreditCard.type ? (this.creditLimit ?? 0) : undefined,
hidden: !this.visible,
subAccounts: !parentAccount ? subAccountModifyRequests : undefined,
clientSessionId: !parentAccount ? clientSessionId : undefined
@@ -365,7 +371,9 @@ export class Account implements AccountInfoResponse {
this.balanceTime,
this.creditCardStatementDate,
this.isAsset,
this.isLiability
this.isLiability,
undefined,
this.creditLimit
);
}
@@ -387,7 +395,9 @@ export class Account implements AccountInfoResponse {
this.creditCardStatementDate,
this.isAsset,
this.isLiability,
typeof(this.subAccounts) !== 'undefined' ? Account.cloneAccounts(this.subAccounts) : undefined);
typeof(this.subAccounts) !== 'undefined' ? Account.cloneAccounts(this.subAccounts) : undefined,
this.creditLimit
);
}
public createNewSubAccount(currency: string, balanceTime: number): Account {
@@ -446,7 +456,8 @@ export class Account implements AccountInfoResponse {
accountResponse.creditCardStatementDate,
accountResponse.isAsset,
accountResponse.isLiability,
accountResponse.subAccounts ? Account.ofMulti(accountResponse.subAccounts) : undefined
accountResponse.subAccounts ? Account.ofMulti(accountResponse.subAccounts) : undefined,
accountResponse.creditLimit
);
}
@@ -560,7 +571,8 @@ export class AccountWithDisplayBalance extends Account {
account.creditCardStatementDate,
account.isAsset,
account.isLiability,
account.subAccounts
account.subAccounts,
account.creditLimit
);
this.displayBalance = displayBalance;
@@ -582,6 +594,7 @@ export interface AccountCreateRequest {
readonly balanceTime: number;
readonly comment: string;
readonly creditCardStatementDate?: number;
readonly creditLimit?: number;
readonly subAccounts?: AccountCreateRequest[];
readonly clientSessionId?: string;
}
@@ -597,6 +610,7 @@ export interface AccountModifyRequest {
readonly balanceTime?: number;
readonly comment: string;
readonly creditCardStatementDate?: number;
readonly creditLimit?: number;
readonly hidden: boolean;
readonly subAccounts?: AccountModifyRequest[];
readonly clientSessionId?: string;
@@ -614,6 +628,7 @@ export interface AccountInfoResponse {
readonly balance: number;
readonly comment: string;
readonly creditCardStatementDate?: number;
readonly creditLimit?: number;
readonly displayOrder: number;
readonly isAsset?: boolean;
readonly isLiability?: boolean;
+4 -7
View File
@@ -128,9 +128,6 @@ const routes: Router.RouteParameters[] = [
path: '/',
async: asyncResolve(HomePage),
beforeEnter: [checkLogin],
options: {
animate: false,
}
},
{
path: '/login',
@@ -159,7 +156,7 @@ const routes: Router.RouteParameters[] = [
{
path: '/transaction/list',
async: asyncResolve(TransactionListPage),
beforeEnter: [checkLogin]
beforeEnter: [checkLogin],
},
{
path: '/transaction/filter/amount',
@@ -184,7 +181,7 @@ const routes: Router.RouteParameters[] = [
{
path: '/account/list',
async: asyncResolve(AccountListPage),
beforeEnter: [checkLogin]
beforeEnter: [checkLogin],
},
{
path: '/account/add',
@@ -209,7 +206,7 @@ const routes: Router.RouteParameters[] = [
{
path: '/statistic/transaction',
async: asyncResolve(StatisticsTransactionPage),
beforeEnter: [checkLogin]
beforeEnter: [checkLogin],
},
{
path: '/statistic/settings',
@@ -259,7 +256,7 @@ const routes: Router.RouteParameters[] = [
{
path: '/settings',
async: asyncResolve(SettingsPage),
beforeEnter: [checkLogin]
beforeEnter: [checkLogin],
},
{
path: '/app_lock',
+6
View File
@@ -171,6 +171,11 @@ export const useSettingsStore = defineStore('settings', () => {
appSettings.value.animate = value;
}
function setExpandCategoryTreeByDefault(value: boolean): void {
updateApplicationSettingsValue('expandCategoryTreeByDefault', value);
appSettings.value.expandCategoryTreeByDefault = value;
}
// Application Lock
function setEnableApplicationLock(value: boolean): void {
updateApplicationSettingsValue('applicationLock', value);
@@ -528,6 +533,7 @@ export const useSettingsStore = defineStore('settings', () => {
setShowAccountBalance,
setEnableSwipeBack,
setEnableAnimate,
setExpandCategoryTreeByDefault,
// -- Application Lock
setEnableApplicationLock,
setEnableApplicationLockWebAuthn,
+56 -1
View File
@@ -54,11 +54,12 @@ import {
splitItemsToMap,
countSplitItems
} from '@/lib/common.ts';
import { parseDateTimeFromUnixTimeWithTimezoneOffset } from '@/lib/datetime.ts';
import { parseDateTimeFromUnixTimeWithTimezoneOffset, getCurrentUnixTime, getTimezoneOffsetMinutes } from '@/lib/datetime.ts';
import { getAmountWithDecimalNumberCount } from '@/lib/numeral.ts';
import { getCurrencyFraction } from '@/lib/currency.ts';
import { getFirstVisibleCategoryId } from '@/lib/category.ts';
import services, { type ApiResponsePromise } from '@/lib/services.ts';
import { generateRandomUUID } from '@/lib/misc.ts';
import logger from '@/lib/logger.ts';
export interface TransactionListPartialFilter {
@@ -1166,6 +1167,59 @@ export const useTransactionsStore = defineStore('transactions', () => {
});
}
function adjustAccountBalance({ accountId, targetBalance, currentBalance }: { accountId: string, targetBalance: number, currentBalance: number }): Promise<boolean> {
return new Promise((resolve, reject) => {
const now = getCurrentUnixTime();
const utcOffset = getTimezoneOffsetMinutes(now);
const delta = targetBalance - currentBalance;
if (delta === 0) {
resolve(true);
return;
}
services.addTransaction({
type: TransactionType.ModifyBalance,
categoryId: '0',
time: now,
utcOffset: utcOffset,
sourceAccountId: accountId,
destinationAccountId: '0',
sourceAmount: delta,
destinationAmount: 0,
hideAmount: false,
tagIds: [],
pictureIds: [],
comment: '',
clientSessionId: generateRandomUUID()
}).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to adjust account balance' });
return;
}
const accountsStore = useAccountsStore();
accountsStore.loadAllAccounts({ force: true }).then(() => {
updateTransactionListInvalidState(true);
resolve(true);
}).catch(() => {
updateTransactionListInvalidState(true);
resolve(true);
});
}).catch(error => {
if (error.response && error.response.data && error.response.data.errorMessage) {
reject({ error: error.response.data });
} else if (!error.processed) {
reject({ message: 'Unable to adjust account balance' });
} else {
reject(error);
}
});
});
}
function deleteTransaction({ transaction, defaultCurrency, beforeResolve }: { transaction: TransactionInfoResponse, defaultCurrency: string, beforeResolve?: BeforeResolveFunction }): Promise<boolean> {
return new Promise((resolve, reject) => {
services.deleteTransaction({
@@ -1472,6 +1526,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
getReconciliationStatements,
getTransaction,
saveTransaction,
adjustAccountBalance,
moveAllTransactionsBetweenAccounts,
deleteTransaction,
recognizeReceiptImage,
+9
View File
@@ -3,6 +3,15 @@ html, body {
position: fixed;
}
:root {
--f7-page-transition-duration: 150ms;
--f7-sheet-transition-duration: 150ms;
--f7-actions-transition-duration: 150ms;
--f7-popup-transition-duration: 150ms;
--f7-dialog-transition-duration: 150ms;
--f7-popover-transition-duration: 150ms;
}
body {
-ms-user-select: none;
-webkit-user-select: none;
+10 -1
View File
@@ -276,6 +276,10 @@
</v-btn>
<v-spacer/>
<span class="account-balance ms-2">{{ accountBalance(element, activeSubAccount[element.id]) }}</span>
<small class="text-medium-emphasis ms-2"
v-if="!activeSubAccount[element.id] && element.creditLimit">
{{ tt('Available') }}: {{ getRemainingCredit(element) }}
</small>
</div>
</v-card-text>
</v-card>
@@ -365,7 +369,7 @@ type ClearAllTransactionsDialogType = InstanceType<typeof ClearAllTransactionsDi
const display = useDisplay();
const { tt, getAllDateRanges, getCurrencyName, joinMultiText } = useI18n();
const { tt, getAllDateRanges, getCurrencyName, joinMultiText, formatAmountToLocalizedNumeralsWithCurrency } = useI18n();
const {
loading,
@@ -473,6 +477,11 @@ function hasAccount(accountCategory: AccountCategory): boolean {
return accountsStore.hasAccount(accountCategory, !showHidden.value);
}
function getRemainingCredit(account: Account): string {
const available = (account.creditLimit ?? 0) + account.balance;
return formatAmountToLocalizedNumeralsWithCurrency(available, account.currency);
}
function accountCurrency(account: Account): string | null {
if (account.type === AccountType.SingleAccount.type) {
return getCurrencyName(account.currency);
@@ -129,9 +129,20 @@
v-model="account.creditCardStatementDate"
></v-autocomplete>
</v-col>
<v-col cols="12" md="6" v-if="currentAccountIndex < 0 && isAccountSupportCreditCardStatementDate">
<amount-input :disabled="loading || submitting"
:persistent-placeholder="true"
:currency="selectedAccount.currency"
:show-currency="true"
:flip-negative="false"
:label="tt('Credit Limit')"
:placeholder="tt('Credit Limit')"
:model-value="account.creditLimit ?? 0"
@update:model-value="account.creditLimit = $event > 0 ? $event : undefined"/>
</v-col>
<v-col cols="12" :md="(!editAccountId || isNewAccount(selectedAccount)) && selectedAccount.balance ? 6 : 12"
v-if="account.type === AccountType.SingleAccount.type || currentAccountIndex >= 0">
<amount-input :disabled="loading || submitting || (!!editAccountId && !isNewAccount(selectedAccount))"
<amount-input :disabled="loading || submitting"
:persistent-placeholder="true"
:currency="selectedAccount.currency"
:show-currency="true"
@@ -204,6 +215,9 @@ import { useAccountEditPageBase } from '@/views/base/accounts/AccountEditPageBas
import { useUserStore } from '@/stores/user.ts';
import { useAccountsStore } from '@/stores/account.ts';
import { useTransactionsStore } from '@/stores/transaction.ts';
import { KnownErrorCode } from '@/consts/api.ts';
import { itemAndIndex } from '@/core/base.ts';
import { AccountType } from '@/core/account.ts';
@@ -254,6 +268,7 @@ const {
const userStore = useUserStore();
const accountsStore = useAccountsStore();
const transactionsStore = useTransactionsStore();
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
const snackbar = useTemplateRef<SnackBarType>('snackbar');
@@ -261,6 +276,7 @@ const snackbar = useTemplateRef<SnackBarType>('snackbar');
const showState = ref<boolean>(false);
const activeTab = ref<string>('account');
const currentAccountIndex = ref<number>(-1);
const originalBalances = ref<Map<string, number>>(new Map());
const selectedAccount = computed<Account>(() => {
if (currentAccountIndex.value < 0) {
@@ -310,6 +326,15 @@ function open(options?: { id?: string, currentAccount?: Account, category?: numb
accountId: editAccountId.value
}).then(response => {
setAccount(response);
originalBalances.value.clear();
if (account.value.id) {
originalBalances.value.set(account.value.id, account.value.balance);
}
for (const subAccount of subAccounts.value) {
if (subAccount.id) {
originalBalances.value.set(subAccount.id, subAccount.balance);
}
}
loading.value = false;
}).catch(error => {
loading.value = false;
@@ -337,7 +362,7 @@ function open(options?: { id?: string, currentAccount?: Account, category?: numb
});
}
function save(): void {
async function save(): Promise<void> {
const problemMessage = inputEmptyProblemMessage.value;
if (problemMessage) {
@@ -347,6 +372,30 @@ function save(): void {
submitting.value = true;
// Collect balance adjustments needed for existing accounts
let balanceChanged = false;
if (editAccountId.value) {
const allAccounts = [account.value, ...subAccounts.value];
for (const acc of allAccounts) {
if (!acc.id || isNewAccount(acc)) continue;
const origBalance = originalBalances.value.get(acc.id);
if (origBalance !== undefined && acc.balance !== origBalance) {
balanceChanged = true;
try {
await transactionsStore.adjustAccountBalance({
accountId: acc.id,
targetBalance: acc.balance,
currentBalance: origBalance
});
} catch (error: unknown) {
submitting.value = false;
snackbar.value?.showError(error as { message: string });
return;
}
}
}
}
accountsStore.saveAccount({
account: account.value,
subAccounts: subAccounts.value,
@@ -366,6 +415,12 @@ function save(): void {
}).catch(error => {
submitting.value = false;
if (balanceChanged && error.error && error.error.errorCode === KnownErrorCode.NothingWillBeUpdated) {
resolveFunc?.({ message: 'You have saved this account' });
showState.value = false;
return;
}
if (!error.processed) {
snackbar.value?.showError(error);
}
+36 -1
View File
@@ -172,6 +172,16 @@
</div>
</v-card-text>
<v-card-text class="pt-0" v-if="filteredSingleAccount">
<v-card variant="tonal" class="d-inline-flex align-center px-4 py-2" rounded="lg">
<ItemIcon icon-type="account" :icon-id="filteredSingleAccount.icon" :color="filteredSingleAccount.color" />
<div class="ms-3">
<div class="text-subtitle-1 font-weight-bold">{{ filteredSingleAccount.name }}</div>
<div class="text-body-2 text-medium-emphasis">{{ filteredAccountBalanceText }}</div>
</div>
</v-card>
</v-card-text>
<v-card-text class="transaction-calendar-container pt-0" v-if="pageType === TransactionListPageType.Calendar.type">
<transaction-calendar day-has-transaction-class="font-weight-bold"
:readonly="loading" :is-dark-mode="isDarkMode"
@@ -688,6 +698,7 @@ import { type NumeralSystem, AmountFilterType } from '@/core/numeral.ts';
import { ThemeType } from '@/core/theme.ts';
import { TransactionType } from '@/core/transaction.ts';
import { TemplateType } from '@/core/template.ts';
import { AccountType } from '@/core/account.ts';
import type { TransactionCategory } from '@/models/transaction_category.ts';
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
import type { TransactionTemplate } from '@/models/transaction_template.ts';
@@ -775,7 +786,8 @@ const {
tt,
getAllRecentMonthDateRanges,
getWeekdayLongName,
getCurrentNumeralSystemType
getCurrentNumeralSystemType,
formatAmountToLocalizedNumeralsWithCurrency
} = useI18n();
const {
@@ -875,6 +887,29 @@ const showFilterTagDialog = ref<boolean>(false);
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
const filteredSingleAccount = computed(() => {
if (queryAllFilterAccountIdsCount.value !== 1) return null;
return allAccountsMap.value[query.value.accountIds] ?? null;
});
const filteredAccountBalanceText = computed<string>(() => {
const account = filteredSingleAccount.value;
if (!account) return '';
if (account.type === AccountType.MultiSubAccounts.type) {
const result = accountsStore.getAccountSubAccountBalance(true, true, account);
if (!result) return '';
return formatAmountToLocalizedNumeralsWithCurrency(result.balance, result.currency);
}
if (account.creditLimit) {
const outstanding = -account.balance;
const available = account.creditLimit + account.balance;
return formatAmountToLocalizedNumeralsWithCurrency(outstanding, account.currency)
+ ' · ' + tt('Available') + ' ' + formatAmountToLocalizedNumeralsWithCurrency(available, account.currency);
}
const displayBalance = account.isLiability ? -account.balance : account.balance;
return formatAmountToLocalizedNumeralsWithCurrency(displayBalance, account.currency);
});
const allPageCounts = computed<NameNumeralValue[]>(() => {
const pageCounts: NameNumeralValue[] = [];
const availableCountPerPage: number[] = [ 5, 10, 15, 20, 25, 30, 50 ];
@@ -211,6 +211,7 @@
:disabled="loading || submitting || !allVisibleAccounts.length || (mode === TransactionEditPageMode.Edit && transaction.type === TransactionType.ModifyBalance)"
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
:custom-selection-primary-text="sourceAccountName"
:custom-selection-secondary-text="sourceAccountBalanceDisplay"
:label="tt(sourceAccountTitle)"
:placeholder="tt(sourceAccountTitle)"
:items="allVisibleCategorizedAccounts"
@@ -236,6 +237,7 @@
:disabled="loading || submitting || !allVisibleAccounts.length"
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
:custom-selection-primary-text="destinationAccountName"
:custom-selection-secondary-text="destinationAccountBalanceDisplay"
:label="tt('Destination Account')"
:placeholder="tt('Destination Account')"
:items="allVisibleCategorizedAccounts"
@@ -510,6 +512,7 @@ import { SUPPORTED_IMAGE_EXTENSIONS } from '@/consts/file.ts';
import { TransactionTemplate } from '@/models/transaction_template.ts';
import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
import { Transaction } from '@/models/transaction.ts';
import type { Account } from '@/models/account.ts';
import {
getTimezoneOffsetMinutes,
@@ -567,7 +570,7 @@ const props = defineProps<{
show?: boolean;
}>();
const { tt } = useI18n();
const { tt, formatAmountToLocalizedNumeralsWithCurrency } = useI18n();
const {
mode,
@@ -644,6 +647,31 @@ const initOptions = ref<TransactionEditOptions | undefined>(undefined);
let resolveFunc: ((response?: TransactionEditResponse) => void) | null = null;
let rejectFunc: ((reason?: unknown) => void) | null = null;
function getAccountBalanceDisplay(account: Account): string {
if (account.creditLimit) {
const outstanding = -account.balance;
const available = account.creditLimit + account.balance;
return formatAmountToLocalizedNumeralsWithCurrency(outstanding, account.currency)
+ ' · ' + tt('Available') + ' ' + formatAmountToLocalizedNumeralsWithCurrency(available, account.currency);
}
const displayBalance = account.isLiability ? -account.balance : account.balance;
return formatAmountToLocalizedNumeralsWithCurrency(displayBalance, account.currency);
}
const sourceAccountBalanceDisplay = computed<string>(() => {
if (!transaction.value.sourceAccountId) return '';
const account = allVisibleAccounts.value.find(a => a.id === transaction.value.sourceAccountId);
if (!account) return '';
return getAccountBalanceDisplay(account);
});
const destinationAccountBalanceDisplay = computed<string>(() => {
if (!transaction.value.destinationAccountId) return '';
const account = allVisibleAccounts.value.find(a => a.id === transaction.value.destinationAccountId);
if (!account) return '';
return getAccountBalanceDisplay(account);
});
const sourceAmountColor = computed<string | undefined>(() => {
if (transaction.value.type === TransactionType.Expense) {
return 'expense';
+16 -1
View File
@@ -108,6 +108,15 @@
</template>
</f7-list-item>
<f7-list-item>
<template #after-title>
{{ tt('Expand Category List By Default') }}
</template>
<template #after>
<f7-toggle :checked="expandCategoryTreeByDefault" @toggle:change="expandCategoryTreeByDefault = $event"></f7-toggle>
</template>
</f7-list-item>
<f7-list-item :title="tt('Browser Cache Management')" link="/settings/browser_caches"></f7-list-item>
<f7-list-item link="#" no-chevron :title="tt('Switch to Desktop Version')" @click="switchToDesktopVersion"></f7-list-item>
@@ -153,7 +162,6 @@ const version = `${getClientDisplayVersion()}`;
const logouting = ref<boolean>(false);
const showThemePopup = ref<boolean>(false);
const showTimezonePopup = ref<boolean>(false);
const currentNickName = computed<string>(() => userStore.currentUserNickname || tt('User'));
const currentTheme = computed<string>({
@@ -176,6 +184,13 @@ const currentTimezoneName = computed<string>(() => {
return '';
});
const expandCategoryTreeByDefault = computed<boolean>({
get: () => settingsStore.appSettings.expandCategoryTreeByDefault,
set: value => {
settingsStore.setExpandCategoryTreeByDefault(value);
}
});
const isEnableSwipeBack = computed<boolean>({
get: () => settingsStore.appSettings.swipeBack,
set: value => {
+72 -1
View File
@@ -204,7 +204,24 @@
<f7-list-item
link="#" no-chevron
class="list-item-with-header-and-title"
:class="{ 'disabled': editAccountId }"
:header="tt('Credit Limit')"
:title="formatCreditLimitDisplay(account)"
v-if="isAccountSupportCreditCardStatementDate"
@click="accountContext.showCreditLimitSheet = true"
>
<number-pad-sheet :min-value="0"
:max-value="TRANSACTION_MAX_AMOUNT"
:currency="account.currency"
:flip-negative="false"
v-model:show="accountContext.showCreditLimitSheet"
:model-value="account.creditLimit ?? 0"
@update:model-value="account.creditLimit = $event > 0 ? $event : undefined"
></number-pad-sheet>
</f7-list-item>
<f7-list-item
link="#" no-chevron
class="list-item-with-header-and-title"
:header="account.isLiability ? tt('Account Outstanding Balance') : tt('Account Balance')"
:title="formatAccountDisplayBalance(account)"
@click="accountContext.showBalanceSheet = true"
@@ -334,6 +351,24 @@
</list-item-selection-popup>
</f7-list-item>
<f7-list-item
link="#" no-chevron
class="list-item-with-header-and-title"
:header="tt('Credit Limit')"
:title="formatCreditLimitDisplay(account)"
v-if="isAccountSupportCreditCardStatementDate"
@click="accountContext.showCreditLimitSheet = true"
>
<number-pad-sheet :min-value="0"
:max-value="TRANSACTION_MAX_AMOUNT"
:currency="account.currency"
:flip-negative="false"
v-model:show="accountContext.showCreditLimitSheet"
:model-value="account.creditLimit ?? 0"
@update:model-value="account.creditLimit = $event > 0 ? $event : undefined"
></number-pad-sheet>
</f7-list-item>
<f7-list-item :title="tt('Visible')" v-if="editAccountId">
<f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle>
</f7-list-item>
@@ -529,11 +564,13 @@ import { useI18nUIComponents, showLoading, hideLoading } from '@/lib/ui/mobile.t
import { useAccountEditPageBase } from '@/views/base/accounts/AccountEditPageBase.ts';
import { useAccountsStore } from '@/stores/account.ts';
import { useTransactionsStore } from '@/stores/transaction.ts';
import { itemAndIndex } from '@/core/base.ts';
import type { LocalizedCurrencyInfo } from '@/core/currency.ts';
import { AccountType } from '@/core/account.ts';
import { ALL_ACCOUNT_ICONS } from '@/consts/icon.ts';
import { KnownErrorCode } from '@/consts/api.ts';
import { ALL_ACCOUNT_COLORS } from '@/consts/color.ts';
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
import type { Account } from '@/models/account.ts';
@@ -550,6 +587,7 @@ interface AccountContext {
showColorSelectionSheet: boolean;
showCurrencyPopup: boolean;
showCreditCardStatementDatePopup: boolean;
showCreditLimitSheet: boolean;
showBalanceSheet: boolean;
showBalanceDateTimeSheet: boolean;
balanceDateTimeSheetMode: string;
@@ -594,17 +632,21 @@ const {
} = useAccountEditPageBase();
const accountsStore = useAccountsStore();
const transactionsStore = useTransactionsStore();
const DEFAULT_ACCOUNT_CONTEXT: AccountContext = {
showIconSelectionSheet: false,
showColorSelectionSheet: false,
showCurrencyPopup: false,
showCreditCardStatementDatePopup: false,
showCreditLimitSheet: false,
showBalanceSheet: false,
showBalanceDateTimeSheet: false,
balanceDateTimeSheetMode: 'time'
};
const originalBalance = ref<number>(0);
const accountContext = ref<AccountContext>(Object.assign({}, DEFAULT_ACCOUNT_CONTEXT));
const subAccountContexts = ref<AccountContext[]>([]);
const subAccountToDelete = ref<Account | null>(null);
@@ -621,6 +663,13 @@ function formatAccountDisplayBalance(selectedAccount: Account): string {
return formatAmountToLocalizedNumeralsWithCurrency(balance, selectedAccount.currency);
}
function formatCreditLimitDisplay(selectedAccount: Account): string {
if (!selectedAccount.creditLimit) {
return tt('Not set');
}
return formatAmountToLocalizedNumeralsWithCurrency(selectedAccount.creditLimit, selectedAccount.currency);
}
function formatAccountBalanceDate(account: Account): string {
if (!isDefined(account.balanceTime)) {
return '';
@@ -652,6 +701,7 @@ function init(): void {
accountId: editAccountId.value
}).then(response => {
setAccount(response);
originalBalance.value = response.balance;
subAccountContexts.value = [];
for (let i = 0; i < subAccounts.value.length; i++) {
@@ -684,6 +734,12 @@ function save(): void {
submitting.value = true;
showLoading(() => submitting.value);
const balanceChanged = !!editAccountId.value && account.value.balance !== originalBalance.value;
const adjustPromise = balanceChanged
? transactionsStore.adjustAccountBalance({ accountId: editAccountId.value!, targetBalance: account.value.balance, currentBalance: originalBalance.value })
: Promise.resolve(true);
adjustPromise.then(() => {
accountsStore.saveAccount({
account: account.value,
subAccounts: subAccounts.value,
@@ -704,6 +760,21 @@ function save(): void {
submitting.value = false;
hideLoading();
if (balanceChanged && error.error && error.error.errorCode === KnownErrorCode.NothingWillBeUpdated) {
// Balance was adjusted but other fields unchanged treat as success
showToast('You have saved this account');
router.back();
return;
}
if (!error.processed) {
showToast(error.message || error);
}
});
}).catch(error => {
submitting.value = false;
hideLoading();
if (!error.processed) {
showToast(error.message || error);
}
+10 -1
View File
@@ -113,6 +113,7 @@
<div class="nested-list-item-title">
<span>{{ account.name }}</span>
<div class="item-footer" v-if="account.comment">{{ account.comment }}</div>
<div class="item-footer" v-if="account.creditLimit">{{ tt('Available') }}: {{ getRemainingCredit(account) }}</div>
</div>
<div class="nested-list-item-after" v-if="account.type === AccountType.MultiSubAccounts.type">
<span>{{ accountBalance(account) }}</span>
@@ -241,7 +242,7 @@ const props = defineProps<{
f7router: Router.Router;
}>();
const { tt, getCurrentLanguageTextDirection } = useI18n();
const { tt, getCurrentLanguageTextDirection, formatAmountToLocalizedNumeralsWithCurrency } = useI18n();
const { showAlert, showToast, routeBackOnError } = useI18nUIComponents();
const {
@@ -288,6 +289,14 @@ const noAvailableAccount = computed<boolean>(() => {
}
});
function getRemainingCredit(account: Account): string {
if (!account.creditLimit) {
return '';
}
const remaining = account.creditLimit + account.balance; // balance is negative for credit cards
return formatAmountToLocalizedNumeralsWithCurrency(remaining, account.currency);
}
function hasAccount(accountCategory: AccountCategory, visibleOnly: boolean): boolean {
return accountsStore.hasAccount(accountCategory, visibleOnly);
}
+73 -1
View File
@@ -119,6 +119,7 @@
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:default-expanded="settingsStore.appSettings.expandCategoryTreeByDefault"
:items="allCategories[CategoryType.Expense]"
v-model:show="showCategorySheet"
v-model="transaction.expenseCategoryId">
@@ -151,6 +152,7 @@
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:default-expanded="settingsStore.appSettings.expandCategoryTreeByDefault"
:items="allCategories[CategoryType.Income]"
v-model:show="showCategorySheet"
v-model="transaction.incomeCategoryId">
@@ -183,6 +185,7 @@
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
secondary-hidden-field="hidden"
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
:default-expanded="settingsStore.appSettings.expandCategoryTreeByDefault"
:items="allCategories[CategoryType.Transfer]"
v-model:show="showCategorySheet"
v-model="transaction.transferCategoryId">
@@ -195,6 +198,7 @@
:class="{ 'disabled': !allVisibleAccounts.length || (mode === TransactionEditPageMode.Edit && transaction.type === TransactionType.ModifyBalance), 'readonly': mode === TransactionEditPageMode.View }"
:header="tt(sourceAccountTitle)"
:title="sourceAccountName"
:footer="sourceAccountBalanceDisplay"
@click="showSourceAccountSheet = true"
>
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
@@ -218,6 +222,7 @@
:class="{ 'disabled': !allVisibleAccounts.length, 'readonly': mode === TransactionEditPageMode.View }"
:header="tt('Destination Account')"
:title="destinationAccountName"
:footer="destinationAccountBalanceDisplay"
v-if="transaction.type === TransactionType.Transfer"
@click="showDestinationAccountSheet = true"
>
@@ -459,10 +464,12 @@
<f7-actions-button @click="showTransactionPictures = true">{{ tt('Add Picture') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group v-if="pageTypeAndMode?.type === TransactionEditPageType.Transaction && mode === TransactionEditPageMode.View && transaction.type !== TransactionType.ModifyBalance">
<f7-actions-button @click="navigateToEdit()">{{ tt('Edit') }}</f7-actions-button>
<f7-actions-button @click="duplicate(false, false)">{{ tt('Duplicate') }}</f7-actions-button>
<f7-actions-button @click="duplicate(true, false)">{{ tt('Duplicate (With Time)') }}</f7-actions-button>
<f7-actions-button @click="duplicate(false, true)" v-if="transaction.geoLocation">{{ tt('Duplicate (With Geographic Location)') }}</f7-actions-button>
<f7-actions-button @click="duplicate(true, true)" v-if="transaction.geoLocation">{{ tt('Duplicate (With Time and Geographic Location)') }}</f7-actions-button>
<f7-actions-button color="red" @click="remove()">{{ tt('Delete') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
@@ -521,6 +528,7 @@ import {
import { useSettingsStore } from '@/stores/setting.ts';
import { useUserStore } from '@/stores/user.ts';
import { useAccountsStore } from '@/stores/account.ts';
import type { Account } from '@/models/account.ts';
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
import { useTransactionsStore } from '@/stores/transaction.ts';
@@ -570,7 +578,8 @@ const {
formatDateTimeToLongDate,
formatDateTimeToLongTime,
formatGregorianTextualYearMonthDayToLongDate,
parseAmountFromLocalizedNumerals
parseAmountFromLocalizedNumerals,
formatAmountToLocalizedNumeralsWithCurrency
} = useI18n();
const { showAlert, showConfirm, showToast, routeBackOnError } = useI18nUIComponents();
@@ -640,6 +649,7 @@ const pictureInput = useTemplateRef<HTMLInputElement>('pictureInput');
const isSupportClipboard = !!navigator.clipboard;
const loadingError = ref<unknown | null>(null);
const isFirstEntry = ref<boolean>(true);
const removingPictureId = ref<string | null>(null);
const transactionDateTimeSheetMode = ref<string>('time');
const showTimeInDefaultTimezone = ref<boolean>(false);
@@ -676,6 +686,31 @@ const quickSaveButtonFloatingPosition = computed<string>(() => {
}
});
function getAccountBalanceDisplay(account: Account): string {
if (account.creditLimit) {
const outstanding = -account.balance;
const available = account.creditLimit + account.balance;
return formatAmountToLocalizedNumeralsWithCurrency(outstanding, account.currency)
+ ' · ' + tt('Available') + ' ' + formatAmountToLocalizedNumeralsWithCurrency(available, account.currency);
}
const displayBalance = account.isLiability ? -account.balance : account.balance;
return formatAmountToLocalizedNumeralsWithCurrency(displayBalance, account.currency);
}
const sourceAccountBalanceDisplay = computed<string>(() => {
if (!transaction.value.sourceAccountId) return '';
const account = allVisibleAccounts.value.find(a => a.id === transaction.value.sourceAccountId);
if (!account) return '';
return getAccountBalanceDisplay(account);
});
const destinationAccountBalanceDisplay = computed<string>(() => {
if (!transaction.value.destinationAccountId) return '';
const account = allVisibleAccounts.value.find(a => a.id === transaction.value.destinationAccountId);
if (!account) return '';
return getAccountBalanceDisplay(account);
});
const sourceAmountClass = computed<Record<string, boolean>>(() => {
const classes: Record<string, boolean> = {
'readonly': mode.value === TransactionEditPageMode.View,
@@ -1319,6 +1354,28 @@ function duplicate(withTime?: boolean, withGeoLocation?: boolean): void {
props.f7router.navigate(`/transaction/add?id=${transaction.value.id}&type=${transaction.value.type}&withTime=${withTime ?? false}&withGeoLocation=${withGeoLocation ?? false}`);
}
function navigateToEdit(): void {
props.f7router.navigate(`/transaction/edit?id=${transaction.value.id}&type=${transaction.value.type}`);
}
function remove(): void {
showConfirm('Are you sure you want to delete this transaction?', () => {
showLoading();
transactionsStore.deleteTransaction({
transaction: transaction.value,
defaultCurrency: defaultCurrency.value
}).then(() => {
hideLoading();
props.f7router.back();
}).catch(error => {
hideLoading();
if (!error.processed) {
showToast(error.message || error);
}
});
});
}
function onPageAfterIn(): void {
routeBackOnError(props.f7router, loadingError);
@@ -1326,6 +1383,21 @@ function onPageAfterIn(): void {
&& !geoLocationStatus.value && !transaction.value.geoLocation) {
updateGeoLocation(false);
}
if (isFirstEntry.value) {
isFirstEntry.value = false;
return;
}
if (mode.value === TransactionEditPageMode.View && query['id']) {
transactionsStore.getTransaction({ transactionId: query['id'], withPictures: true }).then(t => {
setTransactionModel(t, {}, true);
}).catch(error => {
if (!error.processed) {
showToast(error.message || error);
}
});
}
}
function onPageBeforeOut(): void {
+36 -1
View File
@@ -69,6 +69,16 @@
</f7-link>
</f7-toolbar>
<f7-card class="margin-vertical" v-if="filteredSingleAccount">
<f7-card-content class="display-flex align-items-center padding-half">
<ItemIcon icon-type="account" :icon-id="filteredSingleAccount.icon" :color="filteredSingleAccount.color" />
<div class="margin-inline-start-half">
<div class="font-weight-bold">{{ filteredSingleAccount.name }}</div>
<div><small>{{ filteredAccountBalanceText }}</small></div>
</div>
</f7-card-content>
</f7-card>
<f7-block class="transaction-calendar-container margin-vertical" v-if="pageType === TransactionListPageType.Calendar.type">
<transaction-calendar calendar-class="justify-content-center" week-day-name-type="short"
:readonly="loading" :is-dark-mode="isDarkMode"
@@ -633,6 +643,7 @@ import {
} from '@/core/datetime.ts';
import { type NumeralSystem, AmountFilterType } from '@/core/numeral.ts';
import { TransactionType } from '@/core/transaction.ts';
import { AccountType } from '@/core/account.ts';
import type { TransactionCategory } from '@/models/transaction_category.ts';
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
@@ -670,7 +681,8 @@ const {
tt,
getCurrentLanguageTextDirection,
getCurrentNumeralSystemType,
getWeekdayShortName
getWeekdayShortName,
formatAmountToLocalizedNumeralsWithCurrency
} = useI18n();
const { showAlert, showToast, routeBackOnError } = useI18nUIComponents();
@@ -748,6 +760,29 @@ const textDirection = computed<TextDirection>(() => getCurrentLanguageTextDirect
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
const isDarkMode = computed<boolean>(() => environmentsStore.framework7DarkMode || false);
const filteredSingleAccount = computed(() => {
if (queryAllFilterAccountIdsCount.value !== 1) return null;
return allAccountsMap.value[query.value.accountIds] ?? null;
});
const filteredAccountBalanceText = computed<string>(() => {
const account = filteredSingleAccount.value;
if (!account) return '';
if (account.type === AccountType.MultiSubAccounts.type) {
const result = accountsStore.getAccountSubAccountBalance(true, true, account);
if (!result) return '';
return formatAmountToLocalizedNumeralsWithCurrency(result.balance, result.currency);
}
if (account.creditLimit) {
const outstanding = -account.balance;
const available = account.creditLimit + account.balance;
return formatAmountToLocalizedNumeralsWithCurrency(outstanding, account.currency)
+ ' · ' + tt('Available') + ' ' + formatAmountToLocalizedNumeralsWithCurrency(available, account.currency);
}
const displayBalance = account.isLiability ? -account.balance : account.balance;
return formatAmountToLocalizedNumeralsWithCurrency(displayBalance, account.currency);
});
const transactions = computed<TransactionMonthList[]>(() => {
if (loading.value) {
return [];
+20 -20
View File
@@ -4,21 +4,21 @@
"copyright": "Copyright (c) 2014 Manuel Martínez-Almeida",
"url": "https://github.com/gin-gonic/gin",
"license": "MIT License",
"licenseUrl": "https://github.com/gin-gonic/gin/blob/v1.11.0/LICENSE"
"licenseUrl": "https://github.com/gin-gonic/gin/blob/v1.12.0/LICENSE"
},
{
"name": "Cache gin's middleware",
"copyright": "Copyright (c) 2016 Gin-Gonic",
"url": "https://github.com/gin-contrib/cache",
"license": "MIT License",
"licenseUrl": "https://github.com/gin-contrib/cache/blob/v1.4.1/LICENSE"
"licenseUrl": "https://github.com/gin-contrib/cache/blob/v1.4.3/LICENSE"
},
{
"name": "GZIP gin's middleware",
"copyright": "Copyright (c) 2017 Gin-Gonic",
"url": "https://github.com/gin-contrib/gzip",
"license": "MIT License",
"licenseUrl": "https://github.com/gin-contrib/gzip/blob/v1.2.5/LICENSE"
"licenseUrl": "https://github.com/gin-contrib/gzip/blob/v1.2.6/LICENSE"
},
{
"name": "xorm",
@@ -46,7 +46,7 @@
"copyright": "Copyright (c) 2023 urfave/cli maintainers",
"url": "https://github.com/urfave/cli",
"license": "MIT License",
"licenseUrl": "https://github.com/urfave/cli/blob/v3.6.2/LICENSE"
"licenseUrl": "https://github.com/urfave/cli/blob/v3.8.0/LICENSE"
},
{
"name": "INI",
@@ -60,7 +60,7 @@
"copyright": "Copyright (c) 2015 Dean Karn",
"url": "https://github.com/go-playground/validator",
"license": "MIT License",
"licenseUrl": "https://github.com/go-playground/validator/blob/v10.30.1/LICENSE"
"licenseUrl": "https://github.com/go-playground/validator/blob/v10.30.2/LICENSE"
},
{
"name": "jwt-go",
@@ -86,42 +86,42 @@
"copyright": "Copyright (c) 2011-2013, 'pq' Contributors. Portions Copyright (c) 2011 Blake Mizerany",
"url": "https://github.com/lib/pq",
"license": "MIT License",
"licenseUrl": "https://github.com/lib/pq/blob/v1.11.1/LICENSE"
"licenseUrl": "https://github.com/lib/pq/blob/v1.12.1/LICENSE"
},
{
"name": "go-sqlite3",
"copyright": "Copyright (c) 2014 Yasuhiro Matsumoto",
"url": "https://github.com/mattn/go-sqlite3",
"license": "MIT License",
"licenseUrl": "https://github.com/mattn/go-sqlite3/blob/v1.14.33/LICENSE"
"licenseUrl": "https://github.com/mattn/go-sqlite3/blob/v1.14.38/LICENSE"
},
{
"name": "Go Cryptography",
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
"url": "https://golang.org/x/crypto",
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
"licenseUrl": "https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.47.0:LICENSE"
"licenseUrl": "https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.49.0:LICENSE"
},
{
"name": "Go Networking",
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
"url": "https://golang.org/x/net",
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
"licenseUrl": "https://cs.opensource.google/go/x/net/+/refs/tags/v0.49.0:LICENSE"
"licenseUrl": "https://cs.opensource.google/go/x/net/+/refs/tags/v0.52.0:LICENSE"
},
{
"name": "Go Text",
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
"url": "https://golang.org/x/text",
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
"licenseUrl": "https://cs.opensource.google/go/x/text/+/refs/tags/v0.33.0:LICENSE"
"licenseUrl": "https://cs.opensource.google/go/x/text/+/refs/tags/v0.35.0:LICENSE"
},
{
"name": "Go OAuth2",
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
"url": "https://golang.org/x/oauth2",
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
"licenseUrl": "https://cs.opensource.google/go/x/oauth2/+/refs/tags/v0.34.0:LICENSE"
"licenseUrl": "https://cs.opensource.google/go/x/oauth2/+/refs/tags/v0.36.0:LICENSE"
},
{
"name": "go-oidc",
@@ -142,7 +142,7 @@
"copyright": "MinIO Cloud Storage, (C) 2014-2020 MinIO, Inc.",
"url": "https://github.com/minio/minio-go",
"license": "Apache License 2.0",
"licenseUrl": "https://github.com/minio/minio-go/blob/v7.0.98/LICENSE"
"licenseUrl": "https://github.com/minio/minio-go/blob/v7.0.99/LICENSE"
},
{
"name": "gocron",
@@ -163,7 +163,7 @@
"copyright": "Copyright (c) 2016-2025 The excelize Authors. Copyright (c) 2011-2017 Geoffrey J. Teale",
"url": "https://github.com/qax-os/excelize",
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
"licenseUrl": "https://github.com/qax-os/excelize/blob/v2.10.0/LICENSE"
"licenseUrl": "https://github.com/qax-os/excelize/blob/v2.10.1/LICENSE"
},
{
"name": "xls",
@@ -203,7 +203,7 @@
"copyright": "Copyright (c) 2018-present, Yuxi (Evan) You and Vue contributors",
"url": "https://github.com/vuejs/core",
"license": "MIT License",
"licenseUrl": "https://github.com/vuejs/core/blob/v3.5.27/LICENSE"
"licenseUrl": "https://github.com/vuejs/core/blob/v3.5.31/LICENSE"
},
{
"name": "Pinia",
@@ -217,21 +217,21 @@
"copyright": "Copyright (c) 2019-present Eduardo San Martin Morote",
"url": "https://github.com/vuejs/router",
"license": "MIT License",
"licenseUrl": "https://github.com/vuejs/router/blob/v5.0.2/LICENSE"
"licenseUrl": "https://github.com/vuejs/router/blob/v5.0.4/LICENSE"
},
{
"name": "vue-i18n",
"copyright": "Copyright (c) 2016 kazuya kawaguchi",
"url": "https://github.com/intlify/vue-i18n-next",
"license": "MIT License",
"licenseUrl": "https://github.com/intlify/vue-i18n-next/blob/v11.2.8/LICENSE"
"licenseUrl": "https://github.com/intlify/vue-i18n-next/blob/v11.3.0/LICENSE"
},
{
"name": "vuetify",
"copyright": "Copyright (c) 2016-now Vuetify, LLC",
"url": "https://vuetifyjs.com",
"license": "MIT License",
"licenseUrl": "https://github.com/vuetifyjs/vuetify/blob/v3.11.8/packages/vuetify/LICENSE.md"
"licenseUrl": "https://github.com/vuetifyjs/vuetify/blob/v3.12.4/packages/vuetify/LICENSE.md"
},
{
"name": "register-service-worker",
@@ -273,7 +273,7 @@
"copyright": "Copyright (c) 2019 Vladimir Kharlampidi",
"url": "https://swiperjs.com",
"license": "MIT License",
"licenseUrl": "https://github.com/nolimits4web/swiper/blob/v12.1.0/LICENSE"
"licenseUrl": "https://github.com/nolimits4web/swiper/blob/v12.1.3/LICENSE"
},
{
"name": "Skeleton Elements",
@@ -315,7 +315,7 @@
"copyright": "Copyright (c) 2014-present Matt Zabriskie & Collaborators",
"url": "https://axios-http.com",
"license": "MIT License",
"licenseUrl": "https://github.com/axios/axios/blob/v1.13.4/LICENSE"
"licenseUrl": "https://github.com/axios/axios/blob/v1.14.0/LICENSE"
},
{
"name": "Moment.js",
@@ -329,7 +329,7 @@
"copyright": "Copyright (c) JS Foundation and other contributors",
"url": "https://momentjs.com",
"license": "MIT License",
"licenseUrl": "https://github.com/moment/moment-timezone/blob/0.6.0/LICENSE"
"licenseUrl": "https://github.com/moment/moment-timezone/blob/0.6.1/LICENSE"
},
{
"name": "Leaflet",