Compare commits
48 Commits
main
...
d629dfe18c
| Author | SHA1 | Date | |
|---|---|---|---|
| d629dfe18c | |||
| 76a274e1cc | |||
| 291bd86c94 | |||
| 8658e849e7 | |||
| 6d0329210f | |||
| c57c1e8490 | |||
| 2425c358e3 | |||
| 373ccba9d6 | |||
| ce345f79ab | |||
| 6baf668696 | |||
| 9ef0e62b05 | |||
| 7df2d49c56 | |||
| 65d52571de | |||
| 4bdd2c7195 | |||
| 9da91ad54f | |||
| 55c175acca | |||
| 3ed37e7719 | |||
| 32a49be913 | |||
| ebcc03d3d0 | |||
| 0be04287c8 | |||
| dfbc2b1440 | |||
| 47b5641597 | |||
| 11da502f75 | |||
| 29c164439c | |||
| 989ffef156 | |||
| c929e950e1 | |||
| 6b8d9fcb13 | |||
| fe265259d7 | |||
| 69d66c8634 | |||
| 729c04880f | |||
| 7cfb5c7457 | |||
| 93630a821d | |||
| 501765d669 | |||
| 91fa3b65f3 | |||
| b82533233e | |||
| 9c4a0493ee | |||
| 9aa6c4102e | |||
| f058fa53eb | |||
| 4ff73b475a | |||
| ba85852543 | |||
| c7c84c74d3 | |||
| 5fbff39c4f | |||
| 285fef6eba | |||
| 97fb73ad43 | |||
| ce0c9ec65e | |||
| ed084e1ce0 | |||
| ec84065f73 | |||
| 2e8aedcfa6 |
@@ -0,0 +1,167 @@
|
|||||||
|
name: Build Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
# 自动触发:push 到 custom 分支时跑(force-push 后的 rebase 也会触发,可接受)
|
||||||
|
# paths-ignore:纯文档/配置改动跳过,避免浪费 ~10 分钟构建
|
||||||
|
# ⚠️ 已知 quirk(2026-05-02 验证):empty commit(git commit --allow-empty)
|
||||||
|
# 不会触发 paths-ignore 过滤的 workflow,Gitea 把 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 时用用户填的 branch;push 触发时 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
|
||||||
@@ -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
|
|
||||||
@@ -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 }}
|
|
||||||
@@ -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 }}
|
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
@@ -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:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- main
|
- main
|
||||||
|
- myrequirement
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
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"
|
||||||
@@ -161,3 +161,5 @@ package/
|
|||||||
data/
|
data/
|
||||||
storage/
|
storage/
|
||||||
log/
|
log/
|
||||||
|
|
||||||
|
.claude/
|
||||||
@@ -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 个 workflow(2026-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/revision,Gitea 自动关联包到 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 列表显示一条 run,run 详情里 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 列表产生两条独立 run,UX 割裂;合并后单 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 分支迁到 custom,default branch 切到 custom(commit `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 列表产生两条独立 run(build 完一条、deploy 又一条),用户视角割裂;合并后 UI 列表单条 run,run 详情里 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/` 下的传参,绝大多数情况能直接对齐。
|
||||||
@@ -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` 的 README(compose-level)
|
||||||
|
>
|
||||||
|
> 标注:❌ 难/暂缓 | ❓ 待定 | 🔍 调查中 | 🟢 已完成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、账户功能
|
||||||
|
|
||||||
|
### 1. 🟢 信用卡账户:额度与可用额度
|
||||||
|
**描述:** 为信用卡类型账户新增「信用额度」字段,在账户列表显示可用额度。
|
||||||
|
|
||||||
|
**已完成:**
|
||||||
|
- 后端:`AccountExtend` JSON blob 新增 `CreditLimit` 字段(无需数据库迁移)
|
||||||
|
- API:`AccountCreateRequest` / `AccountModifyRequest` / `AccountInfoResponse` 增加 `creditLimit`
|
||||||
|
- 前端 model:`Account` 类增加 `creditLimit` 字段,同步序列化/反序列化
|
||||||
|
- 移动端 EditPage:CreditCard 分类时显示信用额度输入项(数字键盘)
|
||||||
|
- 桌面端 EditDialog:CreditCard 分类时显示信用额度输入框(`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,6 +1,7 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020-2026 MaysWind (i@mayswind.net)
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,4 +1,29 @@
|
|||||||
# ezBookkeeping
|
# 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
[](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
|
[](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
|
||||||
[](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping)
|
[](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping)
|
||||||
[](https://github.com/mayswind/ezbookkeeping/releases)
|
[](https://github.com/mayswind/ezbookkeeping/releases)
|
||||||
@@ -34,7 +59,7 @@ Live Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.
|
|||||||
- **AI-Powered Features**
|
- **AI-Powered Features**
|
||||||
- Receipt image recognition
|
- Receipt image recognition
|
||||||
- MCP (Model Context Protocol) support for AI integration
|
- 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**
|
- **Powerful Bookkeeping**
|
||||||
- Two-level accounts and categories
|
- Two-level accounts and categories
|
||||||
- Image attachments for transactions
|
- Image attachments for transactions
|
||||||
@@ -129,27 +154,27 @@ Help make ezBookkeeping accessible to users around the world. We welcome help to
|
|||||||
|
|
||||||
Currently available translations:
|
Currently available translations:
|
||||||
|
|
||||||
| Tag | Language | Contributors |
|
| Tag | Language | Progress | Contributors |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| de | Deutsch | [@chrgm](https://github.com/chrgm) |
|
| de | Deutsch |  | [@chrgm](https://github.com/chrgm), [@1270o1](https://github.com/1270o1) |
|
||||||
| en | English | / |
|
| 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) |
|
| 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) |
|
| fr | Français |  | [@brieucdlf](https://github.com/brieucdlf) |
|
||||||
| it | Italiano | [@waron97](https://github.com/waron97) |
|
| it | Italiano |  | [@waron97](https://github.com/waron97) |
|
||||||
| ja | 日本語 | [@tkymmm](https://github.com/tkymmm) |
|
| ja | 日本語 |  | [@tkymmm](https://github.com/tkymmm) |
|
||||||
| kn | ಕನ್ನಡ | [@Darshanbm05](https://github.com/Darshanbm05) |
|
| kn | ಕನ್ನಡ |  | [@Darshanbm05](https://github.com/Darshanbm05) |
|
||||||
| ko | 한국어 | [@overworks](https://github.com/overworks) |
|
| ko | 한국어 |  | [@overworks](https://github.com/overworks) |
|
||||||
| nl | Nederlands | [@automagics](https://github.com/automagics) |
|
| nl | Nederlands |  | [@automagics](https://github.com/automagics) |
|
||||||
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus), [@balaios](https://github.com/balaios) |
|
| 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) |
|
| ru | Русский |  | [@artegoser](https://github.com/artegoser), [@dshemin](https://github.com/dshemin) |
|
||||||
| sl | Slovenščina | [@thehijacker](https://github.com/thehijacker) |
|
| sl | Slovenščina |  | [@thehijacker](https://github.com/thehijacker) |
|
||||||
| ta | தமிழ் | [@hhharsha36](https://github.com/hhharsha36) |
|
| ta | தமிழ் |  | [@hhharsha36](https://github.com/hhharsha36) |
|
||||||
| th | ไทย | [@natthavat28](https://github.com/natthavat28) |
|
| th | ไทย |  | [@natthavat28](https://github.com/natthavat28) |
|
||||||
| tr | Türkçe | [@aydnykn](https://github.com/aydnykn) |
|
| tr | Türkçe |  | [@aydnykn](https://github.com/aydnykn) |
|
||||||
| uk | Українська | [@nktlitvinenko](https://github.com/nktlitvinenko) |
|
| uk | Українська |  | [@nktlitvinenko](https://github.com/nktlitvinenko) |
|
||||||
| vi | Tiếng Việt | [@f97](https://github.com/f97) |
|
| vi | Tiếng Việt |  | [@f97](https://github.com/f97) |
|
||||||
| zh-Hans | 中文 (简体) | / |
|
| zh-Hans | 中文 (简体) |  | / |
|
||||||
| zh-Hant | 中文 (繁體) | / |
|
| zh-Hant | 中文 (繁體) |  | / |
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
1. [English](https://ezbookkeeping.mayswind.net)
|
1. [English](https://ezbookkeeping.mayswind.net)
|
||||||
|
|||||||
+2
-1
@@ -14,7 +14,8 @@
|
|||||||
],
|
],
|
||||||
"translators": {
|
"translators": {
|
||||||
"de": [
|
"de": [
|
||||||
"chrgm"
|
"chrgm",
|
||||||
|
"1270o1"
|
||||||
],
|
],
|
||||||
"en": [],
|
"en": [],
|
||||||
"es": [
|
"es": [
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
module github.com/mayswind/ezbookkeeping
|
module github.com/mayswind/ezbookkeeping
|
||||||
|
|
||||||
go 1.25
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/boombuler/barcode v1.1.0
|
github.com/boombuler/barcode v1.1.0
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0
|
github.com/coreos/go-oidc/v3 v3.17.0
|
||||||
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
|
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
|
||||||
github.com/gin-contrib/cache v1.4.1
|
github.com/gin-contrib/cache v1.4.3
|
||||||
github.com/gin-contrib/gzip v1.2.5
|
github.com/gin-contrib/gzip v1.2.6
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/go-co-op/gocron/v2 v2.19.1
|
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/go-sql-driver/mysql v1.9.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/invopop/jsonschema v0.13.0
|
github.com/invopop/jsonschema v0.13.0
|
||||||
github.com/lib/pq v1.11.1
|
github.com/lib/pq v1.12.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.33
|
github.com/mattn/go-sqlite3 v1.14.38
|
||||||
github.com/minio/minio-go/v7 v7.0.98
|
github.com/minio/minio-go/v7 v7.0.99
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/stretchr/testify v1.11.1
|
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/wk8/go-ordered-map/v2 v2.1.8
|
||||||
github.com/xuri/excelize/v2 v2.10.0
|
github.com/xuri/excelize/v2 v2.10.1
|
||||||
golang.org/x/crypto v0.47.0
|
golang.org/x/crypto v0.49.0
|
||||||
golang.org/x/net v0.49.0
|
golang.org/x/net v0.52.0
|
||||||
golang.org/x/oauth2 v0.34.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/text v0.33.0
|
golang.org/x/text v0.35.0
|
||||||
gopkg.in/ini.v1 v1.67.1
|
gopkg.in/ini.v1 v1.67.1
|
||||||
gopkg.in/mail.v2 v2.3.1
|
gopkg.in/mail.v2 v2.3.1
|
||||||
xorm.io/builder v0.3.13
|
xorm.io/builder v0.3.13
|
||||||
@@ -40,8 +40,8 @@ require (
|
|||||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
|
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
|
||||||
github.com/buger/jsonparser v1.1.1 // indirect
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic v1.14.1 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.1 // 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/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect
|
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect
|
||||||
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // 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/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-ini/ini v1.67.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-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.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-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/golang/snappy v0.0.4 // indirect
|
||||||
github.com/gomodule/redigo v1.9.2 // indirect
|
github.com/gomodule/redigo v1.9.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // 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/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
github.com/richardlehane/mscfb v1.0.6 // indirect
|
||||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
|
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||||
github.com/tealeg/xlsx v1.0.5 // 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/tinylib/msgp v1.6.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.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/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
github.com/xuri/efp v0.0.1 // indirect
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // 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
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||||
golang.org/x/mod v0.31.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/tools v0.40.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -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/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.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.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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=
|
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 h1:jqW/h4gcXYEB6kVf6iuxjU9ONWA0ugUB94TP9UNmgdg=
|
||||||
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b/go.mod h1:iACcgahst7BboCpIMSpnFs4SKyU9ZjsvZBfNbUxZOJI=
|
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/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.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/cache v1.4.1 h1:HcLwLfw7p+FasNp5VAnFbbBj9SzB4bDtswvon7wYSg4=
|
github.com/gin-contrib/cache v1.4.3 h1:6rmIlmTf2Vyfd/ue53+BLdTxC7hrQ7FqRgfjz31nEEE=
|
||||||
github.com/gin-contrib/cache v1.4.1/go.mod h1:tykDV+FgItJHYEO0eCasuRYsZKPPyb4BYhAjuTlG6RM=
|
github.com/gin-contrib/cache v1.4.3/go.mod h1:Znf5Qa8HTQ+QHku6ODf72WOPnJ2fHUd2nXD6mSi+6+g=
|
||||||
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
|
github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg=
|
||||||
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
|
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 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
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.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
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 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI=
|
||||||
github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
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=
|
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/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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
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.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
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 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
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 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
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.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
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 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
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=
|
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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
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.12.1 h1:x1nbl/338GLqeDJ/FAiILallhAsqubLzEZu/pXtHUow=
|
||||||
github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
|
||||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
|
||||||
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
|
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 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
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 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
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.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE=
|
||||||
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
|
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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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/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 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
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.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
|
||||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
github.com/richardlehane/msoleps v1.0.4/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 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
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=
|
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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
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 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
|
||||||
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
|
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.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||||
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
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 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
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.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
|
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
||||||
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
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 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
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/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 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
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.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
|
||||||
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
|
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 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
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.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 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
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/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.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
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.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.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
|||||||
Generated
+1359
-1624
File diff suppressed because it is too large
Load Diff
+57
-57
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ezbookkeeping",
|
"name": "ezbookkeeping",
|
||||||
"version": "1.4.0",
|
"version": "1.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -19,64 +19,64 @@
|
|||||||
"test": "cross-env TS_NODE_PROJECT=\"./tsconfig.jest.json\" jest"
|
"test": "cross-env TS_NODE_PROJECT=\"./tsconfig.jest.json\" jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "7.4.47",
|
||||||
"@vuepic/vue-datepicker": "^12.1.0",
|
"@vuepic/vue-datepicker": "12.1.0",
|
||||||
"axios": "^1.13.4",
|
"axios": "1.14.0",
|
||||||
"cbor-js": "^0.1.0",
|
"cbor-js": "0.1.0",
|
||||||
"chardet": "^2.1.1",
|
"chardet": "2.1.1",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "4.2.0",
|
||||||
"dom7": "^4.0.6",
|
"dom7": "4.0.6",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "6.0.0",
|
||||||
"framework7": "^9.0.3",
|
"framework7": "9.0.3",
|
||||||
"framework7-icons": "^5.0.5",
|
"framework7-icons": "5.0.5",
|
||||||
"framework7-vue": "^9.0.3",
|
"framework7-vue": "9.0.3",
|
||||||
"jalaali-js": "^1.2.8",
|
"jalaali-js": "1.2.8",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"line-awesome": "^1.3.0",
|
"line-awesome": "1.3.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "2.30.1",
|
||||||
"moment-timezone": "^0.6.0",
|
"moment-timezone": "0.6.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "3.0.4",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "1.7.2",
|
||||||
"skeleton-elements": "^4.0.1",
|
"skeleton-elements": "4.0.1",
|
||||||
"swiper": "^12.1.0",
|
"swiper": "12.1.3",
|
||||||
"ua-parser-js": "^1.0.39",
|
"ua-parser-js": "1.0.39",
|
||||||
"vue": "^3.5.27",
|
"vue": "3.5.31",
|
||||||
"vue-echarts": "^8.0.1",
|
"vue-echarts": "8.0.1",
|
||||||
"vue-i18n": "^11.2.8",
|
"vue-i18n": "11.3.0",
|
||||||
"vue-router": "^5.0.2",
|
"vue-router": "5.0.4",
|
||||||
"vue3-perfect-scrollbar": "^2.0.0",
|
"vue3-perfect-scrollbar": "2.0.0",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "4.1.0",
|
||||||
"vuetify": "^3.11.8"
|
"vuetify": "3.12.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^30.2.0",
|
"@jest/globals": "30.3.0",
|
||||||
"@tsconfig/node24": "^24.0.4",
|
"@tsconfig/node24": "24.0.4",
|
||||||
"@types/cbor-js": "^0.1.1",
|
"@types/cbor-js": "0.1.1",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "4.2.2",
|
||||||
"@types/git-rev-sync": "^2.0.2",
|
"@types/git-rev-sync": "2.0.2",
|
||||||
"@types/jalaali-js": "^1.2.0",
|
"@types/jalaali-js": "1.2.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "30.0.0",
|
||||||
"@types/node": "^24.1.0",
|
"@types/node": "25.5.0",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@vitejs/plugin-vue": "^6.0.4",
|
"@vitejs/plugin-vue": "6.0.5",
|
||||||
"@vue/eslint-config-typescript": "^14.6.0",
|
"@vue/eslint-config-typescript": "14.7.0",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "0.9.1",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "10.1.0",
|
||||||
"eslint-plugin-vue": "^10.7.0",
|
"eslint-plugin-vue": "10.8.0",
|
||||||
"git-rev-sync": "^3.0.2",
|
"git-rev-sync": "3.0.2",
|
||||||
"jest": "^30.2.0",
|
"jest": "30.3.0",
|
||||||
"postcss-preset-env": "^11.1.3",
|
"postcss-preset-env": "11.2.0",
|
||||||
"sass": "^1.97.3",
|
"sass": "1.98.0",
|
||||||
"ts-jest": "^29.4.6",
|
"ts-jest": "29.4.6",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "7.3.1",
|
||||||
"vite-plugin-checker": "^0.12.0",
|
"vite-plugin-checker": "0.12.0",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "1.2.0",
|
||||||
"vite-plugin-vuetify": "^2.1.3",
|
"vite-plugin-vuetify": "2.1.3",
|
||||||
"vue-tsc": "^3.2.4"
|
"vue-tsc": "3.2.6"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 5 Chrome versions",
|
"last 5 Chrome versions",
|
||||||
|
|||||||
@@ -713,6 +713,9 @@ func (a *AccountsApi) createNewAccountModel(uid int64, accountCreateReq *models.
|
|||||||
|
|
||||||
if !isSubAccount && accountCreateReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD {
|
if !isSubAccount && accountCreateReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD {
|
||||||
accountExtend.CreditCardStatementDate = &accountCreateReq.CreditCardStatementDate
|
accountExtend.CreditCardStatementDate = &accountCreateReq.CreditCardStatementDate
|
||||||
|
if accountCreateReq.CreditLimit > 0 {
|
||||||
|
accountExtend.CreditLimit = &accountCreateReq.CreditLimit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &models.Account{
|
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 {
|
if !isSubAccount && accountModifyReq.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD {
|
||||||
newAccountExtend.CreditCardStatementDate = &accountModifyReq.CreditCardStatementDate
|
newAccountExtend.CreditCardStatementDate = &accountModifyReq.CreditCardStatementDate
|
||||||
|
if accountModifyReq.CreditLimit > 0 {
|
||||||
|
newAccountExtend.CreditLimit = &accountModifyReq.CreditLimit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newAccount := &models.Account{
|
newAccount := &models.Account{
|
||||||
@@ -804,6 +810,12 @@ func (a *AccountsApi) getToUpdateAccount(uid int64, accountModifyReq *models.Acc
|
|||||||
return newAccount
|
return newAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newAccountExtend.CreditLimit != oldAccountExtend.CreditLimit {
|
||||||
|
if newAccountExtend.CreditLimit == nil || oldAccountExtend.CreditLimit == nil || *newAccountExtend.CreditLimit != *oldAccountExtend.CreditLimit {
|
||||||
|
return newAccount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -90,7 +90,8 @@ type Account struct {
|
|||||||
|
|
||||||
// AccountExtend represents account extend data stored in database
|
// AccountExtend represents account extend data stored in database
|
||||||
type AccountExtend struct {
|
type AccountExtend struct {
|
||||||
CreditCardStatementDate *int `json:"creditCardStatementDate"`
|
CreditCardStatementDate *int `json:"creditCardStatementDate"`
|
||||||
|
CreditLimit *int64 `json:"creditLimit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountCreateRequest represents all parameters of account creation request
|
// AccountCreateRequest represents all parameters of account creation request
|
||||||
@@ -105,6 +106,7 @@ type AccountCreateRequest struct {
|
|||||||
BalanceTime int64 `json:"balanceTime"`
|
BalanceTime int64 `json:"balanceTime"`
|
||||||
Comment string `json:"comment" binding:"max=255"`
|
Comment string `json:"comment" binding:"max=255"`
|
||||||
CreditCardStatementDate int `json:"creditCardStatementDate" binding:"min=0,max=28"`
|
CreditCardStatementDate int `json:"creditCardStatementDate" binding:"min=0,max=28"`
|
||||||
|
CreditLimit int64 `json:"creditLimit" binding:"min=0"`
|
||||||
SubAccounts []*AccountCreateRequest `json:"subAccounts" binding:"omitempty"`
|
SubAccounts []*AccountCreateRequest `json:"subAccounts" binding:"omitempty"`
|
||||||
ClientSessionId string `json:"clientSessionId"`
|
ClientSessionId string `json:"clientSessionId"`
|
||||||
}
|
}
|
||||||
@@ -121,6 +123,7 @@ type AccountModifyRequest struct {
|
|||||||
BalanceTime *int64 `json:"balanceTime" binding:"omitempty"`
|
BalanceTime *int64 `json:"balanceTime" binding:"omitempty"`
|
||||||
Comment string `json:"comment" binding:"max=255"`
|
Comment string `json:"comment" binding:"max=255"`
|
||||||
CreditCardStatementDate int `json:"creditCardStatementDate" binding:"min=0,max=28"`
|
CreditCardStatementDate int `json:"creditCardStatementDate" binding:"min=0,max=28"`
|
||||||
|
CreditLimit int64 `json:"creditLimit" binding:"min=0"`
|
||||||
Hidden bool `json:"hidden"`
|
Hidden bool `json:"hidden"`
|
||||||
SubAccounts []*AccountModifyRequest `json:"subAccounts" binding:"omitempty"`
|
SubAccounts []*AccountModifyRequest `json:"subAccounts" binding:"omitempty"`
|
||||||
ClientSessionId string `json:"clientSessionId"`
|
ClientSessionId string `json:"clientSessionId"`
|
||||||
@@ -171,6 +174,7 @@ type AccountInfoResponse struct {
|
|||||||
Balance int64 `json:"balance"`
|
Balance int64 `json:"balance"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
CreditCardStatementDate *int `json:"creditCardStatementDate,omitempty"`
|
CreditCardStatementDate *int `json:"creditCardStatementDate,omitempty"`
|
||||||
|
CreditLimit *int64 `json:"creditLimit,omitempty"`
|
||||||
DisplayOrder int32 `json:"displayOrder"`
|
DisplayOrder int32 `json:"displayOrder"`
|
||||||
IsAsset bool `json:"isAsset,omitempty"`
|
IsAsset bool `json:"isAsset,omitempty"`
|
||||||
IsLiability bool `json:"isLiability,omitempty"`
|
IsLiability bool `json:"isLiability,omitempty"`
|
||||||
@@ -181,10 +185,14 @@ type AccountInfoResponse struct {
|
|||||||
// ToAccountInfoResponse returns a view-object according to database model
|
// ToAccountInfoResponse returns a view-object according to database model
|
||||||
func (a *Account) ToAccountInfoResponse() *AccountInfoResponse {
|
func (a *Account) ToAccountInfoResponse() *AccountInfoResponse {
|
||||||
var creditCardStatementDate *int
|
var creditCardStatementDate *int
|
||||||
|
var creditLimit *int64
|
||||||
|
|
||||||
if a.ParentAccountId == LevelOneAccountParentId && a.Category == ACCOUNT_CATEGORY_CREDIT_CARD {
|
if a.ParentAccountId == LevelOneAccountParentId && a.Category == ACCOUNT_CATEGORY_CREDIT_CARD {
|
||||||
if a.Extend != nil {
|
if a.Extend != nil {
|
||||||
creditCardStatementDate = a.Extend.CreditCardStatementDate
|
creditCardStatementDate = a.Extend.CreditCardStatementDate
|
||||||
|
if a.Extend.CreditLimit != nil && *a.Extend.CreditLimit > 0 {
|
||||||
|
creditLimit = a.Extend.CreditLimit
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
creditCardStatementDate = &defaultCreditCardAccountStatementDate
|
creditCardStatementDate = &defaultCreditCardAccountStatementDate
|
||||||
}
|
}
|
||||||
@@ -202,6 +210,7 @@ func (a *Account) ToAccountInfoResponse() *AccountInfoResponse {
|
|||||||
Balance: a.Balance,
|
Balance: a.Balance,
|
||||||
Comment: a.Comment,
|
Comment: a.Comment,
|
||||||
CreditCardStatementDate: creditCardStatementDate,
|
CreditCardStatementDate: creditCardStatementDate,
|
||||||
|
CreditLimit: creditLimit,
|
||||||
DisplayOrder: a.DisplayOrder,
|
DisplayOrder: a.DisplayOrder,
|
||||||
IsAsset: assetAccountCategory[a.Category],
|
IsAsset: assetAccountCategory[a.Category],
|
||||||
IsLiability: liabilityAccountCategory[a.Category],
|
IsLiability: liabilityAccountCategory[a.Category],
|
||||||
|
|||||||
@@ -544,7 +544,9 @@ func (t *Transaction) ToTransactionInfoResponse(tagIds []int64, editable bool) *
|
|||||||
destinationAccountId := int64(0)
|
destinationAccountId := int64(0)
|
||||||
destinationAmount := 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
|
destinationAccountId = t.RelatedAccountId
|
||||||
destinationAmount = t.RelatedAccountAmount
|
destinationAmount = t.RelatedAccountAmount
|
||||||
} else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_IN {
|
} else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_IN {
|
||||||
|
|||||||
@@ -954,7 +954,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
|
|
||||||
if transaction.Amount != oldTransaction.Amount {
|
if transaction.Amount != oldTransaction.Amount {
|
||||||
if oldTransaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
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")
|
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
|
// Verify balance modification transaction and calculate real amount
|
||||||
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
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.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
|
} else { // Not allow to add transaction before balance modification transaction
|
||||||
otherTransactionExists := false
|
otherTransactionExists := false
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,11 @@
|
|||||||
<div class="margin-top padding-horizontal" v-if="hint">
|
<div class="margin-top padding-horizontal" v-if="hint">
|
||||||
<span>{{ hint }}</span>
|
<span>{{ hint }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="numpad-values" @click="onDisplayValueClick">
|
<div class="numpad-values">
|
||||||
<span id="numpad-value" class="numpad-value" :class="currentDisplayNumClass">{{ currentDisplay }}</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<f7-popover class="numpad-paste-popover" target-el="#numpad-value"
|
<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('+')">
|
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('+')">
|
||||||
<span class="numpad-button-text numpad-button-text-normal">+</span>
|
<span class="numpad-button-text numpad-button-text-normal">+</span>
|
||||||
</f7-button>
|
</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()">
|
<f7-button class="numpad-button numpad-button-num" v-if="supportDecimalSeparator" @click="inputDecimalSeparator()">
|
||||||
<span class="numpad-button-text numpad-button-text-normal">{{ decimalSeparator }}</span>
|
<span class="numpad-button-text numpad-button-text-normal">{{ decimalSeparator }}</span>
|
||||||
</f7-button>
|
</f7-button>
|
||||||
<f7-button class="numpad-button numpad-button-num" v-if="!supportDecimalSeparator" @click="inputDoubleNum(0)">
|
<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>
|
<span class="numpad-button-text numpad-button-text-normal">{{ `${digits[0]}${digits[0]}` }}</span>
|
||||||
</f7-button>
|
</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()">
|
<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>
|
<span :class="{ 'numpad-button-text': true, 'numpad-button-text-confirm': !currentSymbol }">{{ confirmText }}</span>
|
||||||
</f7-button>
|
</f7-button>
|
||||||
@@ -467,12 +468,15 @@ watch(() => props.flipNegative, (newValue) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.numpad-values {
|
.numpad-values {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
border-bottom: 1px solid var(--f7-page-bg-color);
|
border-bottom: 1px solid var(--f7-page-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.numpad-value {
|
.numpad-value {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
padding-inline-start: 16px;
|
padding-inline-start: 16px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
height: var(--ebk-numpad-value-height);
|
height: var(--ebk-numpad-value-height);
|
||||||
@@ -481,6 +485,22 @@ watch(() => props.flipNegative, (newValue) => {
|
|||||||
user-select: none;
|
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 {
|
.numpad-value-small {
|
||||||
font-size: var(--ebk-numpad-value-small-font-size);
|
font-size: var(--ebk-numpad-value-small-font-size);
|
||||||
}
|
}
|
||||||
@@ -511,7 +531,10 @@ watch(() => props.flipNegative, (newValue) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
touch-action: none;
|
/* 上游设的 touch-action: none 在 F7 tap 处理下让 click 慢一拍(小键盘卡顿
|
||||||
|
的实际根因,不是网络/渲染)。改 manipulation:禁双击缩放 + 消除 300ms
|
||||||
|
老延迟,但保留 click 正常合成。详见 FORK.md #11 */
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.numpad-button-num {
|
.numpad-button-num {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</f7-list>
|
</f7-list>
|
||||||
<f7-treeview class="tree-view-selection-treeview">
|
<f7-treeview class="tree-view-selection-treeview">
|
||||||
<f7-treeview-item item-toggle
|
<f7-treeview-item item-toggle
|
||||||
:opened="isPrimaryItemHasSecondaryValue(item)"
|
:opened="props.defaultExpanded || isPrimaryItemHasSecondaryValue(item)"
|
||||||
:label="ti((primaryTitleField ? item[primaryTitleField] : item) as string, !!primaryTitleI18n)"
|
:label="ti((primaryTitleField ? item[primaryTitleField] : item) as string, !!primaryTitleI18n)"
|
||||||
:key="primaryKeyField ? item[primaryKeyField] : item"
|
:key="primaryKeyField ? item[primaryKeyField] : item"
|
||||||
v-for="item in filteredItems">
|
v-for="item in filteredItems">
|
||||||
@@ -59,6 +59,7 @@ import { type Framework7Dom, scrollSheetToTop } from '@/lib/ui/mobile.ts';
|
|||||||
|
|
||||||
interface MobileTwoLevelItemSelectionBaseProps extends TwoLevelItemSelectionBaseProps {
|
interface MobileTwoLevelItemSelectionBaseProps extends TwoLevelItemSelectionBaseProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
|
defaultExpanded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<MobileTwoLevelItemSelectionBaseProps>();
|
const props = defineProps<MobileTwoLevelItemSelectionBaseProps>();
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface ApplicationSettings extends BaseApplicationSetting {
|
|||||||
showAccountBalance: boolean;
|
showAccountBalance: boolean;
|
||||||
swipeBack: boolean;
|
swipeBack: boolean;
|
||||||
animate: boolean;
|
animate: boolean;
|
||||||
|
expandCategoryTreeByDefault: boolean;
|
||||||
// Application Lock
|
// Application Lock
|
||||||
applicationLock: boolean;
|
applicationLock: boolean;
|
||||||
applicationLockWebAuthn: boolean;
|
applicationLockWebAuthn: boolean;
|
||||||
@@ -134,6 +135,7 @@ export const ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES: Record<string, UserAp
|
|||||||
'autoSaveTransactionDraft': UserApplicationCloudSettingType.String,
|
'autoSaveTransactionDraft': UserApplicationCloudSettingType.String,
|
||||||
'autoGetCurrentGeoLocation': UserApplicationCloudSettingType.Boolean,
|
'autoGetCurrentGeoLocation': UserApplicationCloudSettingType.Boolean,
|
||||||
'alwaysShowTransactionPicturesInMobileTransactionEditPage': UserApplicationCloudSettingType.Boolean,
|
'alwaysShowTransactionPicturesInMobileTransactionEditPage': UserApplicationCloudSettingType.Boolean,
|
||||||
|
'expandCategoryTreeByDefault': UserApplicationCloudSettingType.Boolean,
|
||||||
// Import Transaction Dialog
|
// Import Transaction Dialog
|
||||||
'rememberLastSelectedFileTypeInImportTransactionDialog': UserApplicationCloudSettingType.Boolean,
|
'rememberLastSelectedFileTypeInImportTransactionDialog': UserApplicationCloudSettingType.Boolean,
|
||||||
'lastSelectedFileTypeInImportTransactionDialog': UserApplicationCloudSettingType.String,
|
'lastSelectedFileTypeInImportTransactionDialog': UserApplicationCloudSettingType.String,
|
||||||
@@ -174,6 +176,7 @@ export const DEFAULT_APPLICATION_SETTINGS: ApplicationSettings = {
|
|||||||
showAccountBalance: true,
|
showAccountBalance: true,
|
||||||
swipeBack: true,
|
swipeBack: true,
|
||||||
animate: true,
|
animate: true,
|
||||||
|
expandCategoryTreeByDefault: false,
|
||||||
// Application Lock
|
// Application Lock
|
||||||
applicationLock: false,
|
applicationLock: false,
|
||||||
applicationLockWebAuthn: false,
|
applicationLockWebAuthn: false,
|
||||||
|
|||||||
+742
-742
File diff suppressed because it is too large
Load Diff
@@ -1860,6 +1860,8 @@
|
|||||||
"Balance Time": "Balance Time",
|
"Balance Time": "Balance Time",
|
||||||
"Sub-account Balance Time": "Sub-account Balance Time",
|
"Sub-account Balance Time": "Sub-account Balance Time",
|
||||||
"Statement Date": "Statement Date",
|
"Statement Date": "Statement Date",
|
||||||
|
"Credit Limit": "Credit Limit",
|
||||||
|
"Available": "Available",
|
||||||
"Description": "Description",
|
"Description": "Description",
|
||||||
"Your account description (optional)": "Your account description (optional)",
|
"Your account description (optional)": "Your account description (optional)",
|
||||||
"Your sub-account description (optional)": "Your sub-account description (optional)",
|
"Your sub-account description (optional)": "Your sub-account description (optional)",
|
||||||
@@ -2280,6 +2282,7 @@
|
|||||||
"Exchange Rate": "Exchange Rate",
|
"Exchange Rate": "Exchange Rate",
|
||||||
"Enable Swipe Back": "Enable Swipe Back",
|
"Enable Swipe Back": "Enable Swipe Back",
|
||||||
"Enable Animation": "Enable Animation",
|
"Enable Animation": "Enable Animation",
|
||||||
|
"Expand Category List By Default": "Expand Category List By Default",
|
||||||
"Basic Information": "Basic Information",
|
"Basic Information": "Basic Information",
|
||||||
"User Information": "User Information",
|
"User Information": "User Information",
|
||||||
"Already have an account?": "Already have an account?",
|
"Already have an account?": "Already have an account?",
|
||||||
|
|||||||
@@ -1860,6 +1860,8 @@
|
|||||||
"Balance Time": "余额时间",
|
"Balance Time": "余额时间",
|
||||||
"Sub-account Balance Time": "子账户余额时间",
|
"Sub-account Balance Time": "子账户余额时间",
|
||||||
"Statement Date": "账单日",
|
"Statement Date": "账单日",
|
||||||
|
"Credit Limit": "信用额度",
|
||||||
|
"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": "默认展开分类列表",
|
||||||
"Basic Information": "基本信息",
|
"Basic Information": "基本信息",
|
||||||
"User Information": "用户信息",
|
"User Information": "用户信息",
|
||||||
"Already have an account?": "已经有账号?",
|
"Already have an account?": "已经有账号?",
|
||||||
|
|||||||
@@ -1889,6 +1889,8 @@
|
|||||||
"Are you sure you want to move all transactions?": "您確定要移動所有交易?",
|
"Are you sure you want to move all transactions?": "您確定要移動所有交易?",
|
||||||
"Unable to move transactions": "無法移動交易",
|
"Unable to move transactions": "無法移動交易",
|
||||||
"All transactions in this account have been moved.": "此帳戶中的所有交易已被移動。",
|
"All transactions in this account have been moved.": "此帳戶中的所有交易已被移動。",
|
||||||
|
"Credit Limit": "信用額度",
|
||||||
|
"Available": "可用額度",
|
||||||
"Reconciliation Statement": "對帳單",
|
"Reconciliation Statement": "對帳單",
|
||||||
"Account Balance Trends": "帳戶餘額趨勢",
|
"Account Balance Trends": "帳戶餘額趨勢",
|
||||||
"Update Closing Balance": "更新期末餘額",
|
"Update Closing Balance": "更新期末餘額",
|
||||||
@@ -2280,6 +2282,7 @@
|
|||||||
"Exchange Rate": "匯率",
|
"Exchange Rate": "匯率",
|
||||||
"Enable Swipe Back": "啟用滑動返回",
|
"Enable Swipe Back": "啟用滑動返回",
|
||||||
"Enable Animation": "啟用動畫",
|
"Enable Animation": "啟用動畫",
|
||||||
|
"Expand Category List By Default": "預設展開分類列表",
|
||||||
"Basic Information": "基本資訊",
|
"Basic Information": "基本資訊",
|
||||||
"User Information": "使用者資訊",
|
"User Information": "使用者資訊",
|
||||||
"Already have an account?": "已經有帳號?",
|
"Already have an account?": "已經有帳號?",
|
||||||
|
|||||||
+21
-6
@@ -17,6 +17,7 @@ export class Account implements AccountInfoResponse {
|
|||||||
public balanceTime?: number;
|
public balanceTime?: number;
|
||||||
public comment: string;
|
public comment: string;
|
||||||
public creditCardStatementDate?: number;
|
public creditCardStatementDate?: number;
|
||||||
|
public creditLimit?: number;
|
||||||
public displayOrder: number;
|
public displayOrder: number;
|
||||||
public visible: boolean;
|
public visible: boolean;
|
||||||
public subAccounts?: Account[];
|
public subAccounts?: Account[];
|
||||||
@@ -24,7 +25,7 @@ export class Account implements AccountInfoResponse {
|
|||||||
private readonly _isAsset?: boolean;
|
private readonly _isAsset?: boolean;
|
||||||
private readonly _isLiability?: 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.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.parentId = parentId;
|
this.parentId = parentId;
|
||||||
@@ -39,6 +40,7 @@ export class Account implements AccountInfoResponse {
|
|||||||
this.displayOrder = displayOrder;
|
this.displayOrder = displayOrder;
|
||||||
this.visible = visible;
|
this.visible = visible;
|
||||||
this.creditCardStatementDate = creditCardStatementDate;
|
this.creditCardStatementDate = creditCardStatementDate;
|
||||||
|
this.creditLimit = creditLimit;
|
||||||
this._isAsset = isAsset;
|
this._isAsset = isAsset;
|
||||||
this._isLiability = isLiability;
|
this._isLiability = isLiability;
|
||||||
|
|
||||||
@@ -95,7 +97,8 @@ export class Account implements AccountInfoResponse {
|
|||||||
this.comment === other.comment &&
|
this.comment === other.comment &&
|
||||||
this.displayOrder === other.displayOrder &&
|
this.displayOrder === other.displayOrder &&
|
||||||
this.visible === other.visible &&
|
this.visible === other.visible &&
|
||||||
this.creditCardStatementDate === other.creditCardStatementDate;
|
this.creditCardStatementDate === other.creditCardStatementDate &&
|
||||||
|
this.creditLimit === other.creditLimit;
|
||||||
|
|
||||||
if (!isEqual) {
|
if (!isEqual) {
|
||||||
return false;
|
return false;
|
||||||
@@ -130,6 +133,7 @@ export class Account implements AccountInfoResponse {
|
|||||||
this.balanceTime = other.balanceTime;
|
this.balanceTime = other.balanceTime;
|
||||||
this.comment = other.comment;
|
this.comment = other.comment;
|
||||||
this.creditCardStatementDate = other.creditCardStatementDate;
|
this.creditCardStatementDate = other.creditCardStatementDate;
|
||||||
|
this.creditLimit = other.creditLimit;
|
||||||
this.visible = other.visible;
|
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,
|
balanceTime: (parentAccount || this.type === AccountType.SingleAccount.type) && this.balanceTime ? this.balanceTime : 0,
|
||||||
comment: this.comment,
|
comment: this.comment,
|
||||||
creditCardStatementDate: !parentAccount && this.category === AccountCategory.CreditCard.type ? this.creditCardStatementDate : undefined,
|
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,
|
subAccounts: !parentAccount ? subAccountCreateRequests : undefined,
|
||||||
clientSessionId: !parentAccount ? clientSessionId : undefined
|
clientSessionId: !parentAccount ? clientSessionId : undefined
|
||||||
};
|
};
|
||||||
@@ -214,6 +219,7 @@ export class Account implements AccountInfoResponse {
|
|||||||
balanceTime: parentAccount && (!this.id || this.id === '0') ? this.balanceTime : undefined,
|
balanceTime: parentAccount && (!this.id || this.id === '0') ? this.balanceTime : undefined,
|
||||||
comment: this.comment,
|
comment: this.comment,
|
||||||
creditCardStatementDate: !parentAccount && this.category === AccountCategory.CreditCard.type ? this.creditCardStatementDate : undefined,
|
creditCardStatementDate: !parentAccount && this.category === AccountCategory.CreditCard.type ? this.creditCardStatementDate : undefined,
|
||||||
|
creditLimit: !parentAccount && this.category === AccountCategory.CreditCard.type ? (this.creditLimit ?? 0) : undefined,
|
||||||
hidden: !this.visible,
|
hidden: !this.visible,
|
||||||
subAccounts: !parentAccount ? subAccountModifyRequests : undefined,
|
subAccounts: !parentAccount ? subAccountModifyRequests : undefined,
|
||||||
clientSessionId: !parentAccount ? clientSessionId : undefined
|
clientSessionId: !parentAccount ? clientSessionId : undefined
|
||||||
@@ -365,7 +371,9 @@ export class Account implements AccountInfoResponse {
|
|||||||
this.balanceTime,
|
this.balanceTime,
|
||||||
this.creditCardStatementDate,
|
this.creditCardStatementDate,
|
||||||
this.isAsset,
|
this.isAsset,
|
||||||
this.isLiability
|
this.isLiability,
|
||||||
|
undefined,
|
||||||
|
this.creditLimit
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +395,9 @@ export class Account implements AccountInfoResponse {
|
|||||||
this.creditCardStatementDate,
|
this.creditCardStatementDate,
|
||||||
this.isAsset,
|
this.isAsset,
|
||||||
this.isLiability,
|
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 {
|
public createNewSubAccount(currency: string, balanceTime: number): Account {
|
||||||
@@ -446,7 +456,8 @@ export class Account implements AccountInfoResponse {
|
|||||||
accountResponse.creditCardStatementDate,
|
accountResponse.creditCardStatementDate,
|
||||||
accountResponse.isAsset,
|
accountResponse.isAsset,
|
||||||
accountResponse.isLiability,
|
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.creditCardStatementDate,
|
||||||
account.isAsset,
|
account.isAsset,
|
||||||
account.isLiability,
|
account.isLiability,
|
||||||
account.subAccounts
|
account.subAccounts,
|
||||||
|
account.creditLimit
|
||||||
);
|
);
|
||||||
|
|
||||||
this.displayBalance = displayBalance;
|
this.displayBalance = displayBalance;
|
||||||
@@ -582,6 +594,7 @@ export interface AccountCreateRequest {
|
|||||||
readonly balanceTime: number;
|
readonly balanceTime: number;
|
||||||
readonly comment: string;
|
readonly comment: string;
|
||||||
readonly creditCardStatementDate?: number;
|
readonly creditCardStatementDate?: number;
|
||||||
|
readonly creditLimit?: number;
|
||||||
readonly subAccounts?: AccountCreateRequest[];
|
readonly subAccounts?: AccountCreateRequest[];
|
||||||
readonly clientSessionId?: string;
|
readonly clientSessionId?: string;
|
||||||
}
|
}
|
||||||
@@ -597,6 +610,7 @@ export interface AccountModifyRequest {
|
|||||||
readonly balanceTime?: number;
|
readonly balanceTime?: number;
|
||||||
readonly comment: string;
|
readonly comment: string;
|
||||||
readonly creditCardStatementDate?: number;
|
readonly creditCardStatementDate?: number;
|
||||||
|
readonly creditLimit?: number;
|
||||||
readonly hidden: boolean;
|
readonly hidden: boolean;
|
||||||
readonly subAccounts?: AccountModifyRequest[];
|
readonly subAccounts?: AccountModifyRequest[];
|
||||||
readonly clientSessionId?: string;
|
readonly clientSessionId?: string;
|
||||||
@@ -614,6 +628,7 @@ export interface AccountInfoResponse {
|
|||||||
readonly balance: number;
|
readonly balance: number;
|
||||||
readonly comment: string;
|
readonly comment: string;
|
||||||
readonly creditCardStatementDate?: number;
|
readonly creditCardStatementDate?: number;
|
||||||
|
readonly creditLimit?: number;
|
||||||
readonly displayOrder: number;
|
readonly displayOrder: number;
|
||||||
readonly isAsset?: boolean;
|
readonly isAsset?: boolean;
|
||||||
readonly isLiability?: boolean;
|
readonly isLiability?: boolean;
|
||||||
|
|||||||
@@ -128,9 +128,6 @@ const routes: Router.RouteParameters[] = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
async: asyncResolve(HomePage),
|
async: asyncResolve(HomePage),
|
||||||
beforeEnter: [checkLogin],
|
beforeEnter: [checkLogin],
|
||||||
options: {
|
|
||||||
animate: false,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
@@ -159,7 +156,7 @@ const routes: Router.RouteParameters[] = [
|
|||||||
{
|
{
|
||||||
path: '/transaction/list',
|
path: '/transaction/list',
|
||||||
async: asyncResolve(TransactionListPage),
|
async: asyncResolve(TransactionListPage),
|
||||||
beforeEnter: [checkLogin]
|
beforeEnter: [checkLogin],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/transaction/filter/amount',
|
path: '/transaction/filter/amount',
|
||||||
@@ -184,7 +181,7 @@ const routes: Router.RouteParameters[] = [
|
|||||||
{
|
{
|
||||||
path: '/account/list',
|
path: '/account/list',
|
||||||
async: asyncResolve(AccountListPage),
|
async: asyncResolve(AccountListPage),
|
||||||
beforeEnter: [checkLogin]
|
beforeEnter: [checkLogin],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/account/add',
|
path: '/account/add',
|
||||||
@@ -209,7 +206,7 @@ const routes: Router.RouteParameters[] = [
|
|||||||
{
|
{
|
||||||
path: '/statistic/transaction',
|
path: '/statistic/transaction',
|
||||||
async: asyncResolve(StatisticsTransactionPage),
|
async: asyncResolve(StatisticsTransactionPage),
|
||||||
beforeEnter: [checkLogin]
|
beforeEnter: [checkLogin],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/statistic/settings',
|
path: '/statistic/settings',
|
||||||
@@ -259,7 +256,7 @@ const routes: Router.RouteParameters[] = [
|
|||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
async: asyncResolve(SettingsPage),
|
async: asyncResolve(SettingsPage),
|
||||||
beforeEnter: [checkLogin]
|
beforeEnter: [checkLogin],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/app_lock',
|
path: '/app_lock',
|
||||||
|
|||||||
@@ -171,6 +171,11 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
appSettings.value.animate = value;
|
appSettings.value.animate = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setExpandCategoryTreeByDefault(value: boolean): void {
|
||||||
|
updateApplicationSettingsValue('expandCategoryTreeByDefault', value);
|
||||||
|
appSettings.value.expandCategoryTreeByDefault = value;
|
||||||
|
}
|
||||||
|
|
||||||
// Application Lock
|
// Application Lock
|
||||||
function setEnableApplicationLock(value: boolean): void {
|
function setEnableApplicationLock(value: boolean): void {
|
||||||
updateApplicationSettingsValue('applicationLock', value);
|
updateApplicationSettingsValue('applicationLock', value);
|
||||||
@@ -528,6 +533,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
setShowAccountBalance,
|
setShowAccountBalance,
|
||||||
setEnableSwipeBack,
|
setEnableSwipeBack,
|
||||||
setEnableAnimate,
|
setEnableAnimate,
|
||||||
|
setExpandCategoryTreeByDefault,
|
||||||
// -- Application Lock
|
// -- Application Lock
|
||||||
setEnableApplicationLock,
|
setEnableApplicationLock,
|
||||||
setEnableApplicationLockWebAuthn,
|
setEnableApplicationLockWebAuthn,
|
||||||
|
|||||||
@@ -54,11 +54,12 @@ import {
|
|||||||
splitItemsToMap,
|
splitItemsToMap,
|
||||||
countSplitItems
|
countSplitItems
|
||||||
} from '@/lib/common.ts';
|
} 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 { getAmountWithDecimalNumberCount } from '@/lib/numeral.ts';
|
||||||
import { getCurrencyFraction } from '@/lib/currency.ts';
|
import { getCurrencyFraction } from '@/lib/currency.ts';
|
||||||
import { getFirstVisibleCategoryId } from '@/lib/category.ts';
|
import { getFirstVisibleCategoryId } from '@/lib/category.ts';
|
||||||
import services, { type ApiResponsePromise } from '@/lib/services.ts';
|
import services, { type ApiResponsePromise } from '@/lib/services.ts';
|
||||||
|
import { generateRandomUUID } from '@/lib/misc.ts';
|
||||||
import logger from '@/lib/logger.ts';
|
import logger from '@/lib/logger.ts';
|
||||||
|
|
||||||
export interface TransactionListPartialFilter {
|
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> {
|
function deleteTransaction({ transaction, defaultCurrency, beforeResolve }: { transaction: TransactionInfoResponse, defaultCurrency: string, beforeResolve?: BeforeResolveFunction }): Promise<boolean> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
services.deleteTransaction({
|
services.deleteTransaction({
|
||||||
@@ -1472,6 +1526,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
|
|||||||
getReconciliationStatements,
|
getReconciliationStatements,
|
||||||
getTransaction,
|
getTransaction,
|
||||||
saveTransaction,
|
saveTransaction,
|
||||||
|
adjustAccountBalance,
|
||||||
moveAllTransactionsBetweenAccounts,
|
moveAllTransactionsBetweenAccounts,
|
||||||
deleteTransaction,
|
deleteTransaction,
|
||||||
recognizeReceiptImage,
|
recognizeReceiptImage,
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ html, body {
|
|||||||
position: fixed;
|
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 {
|
body {
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
|||||||
@@ -276,6 +276,10 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
<v-spacer/>
|
<v-spacer/>
|
||||||
<span class="account-balance ms-2">{{ accountBalance(element, activeSubAccount[element.id]) }}</span>
|
<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>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -365,7 +369,7 @@ type ClearAllTransactionsDialogType = InstanceType<typeof ClearAllTransactionsDi
|
|||||||
|
|
||||||
const display = useDisplay();
|
const display = useDisplay();
|
||||||
|
|
||||||
const { tt, getAllDateRanges, getCurrencyName, joinMultiText } = useI18n();
|
const { tt, getAllDateRanges, getCurrencyName, joinMultiText, formatAmountToLocalizedNumeralsWithCurrency } = useI18n();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
@@ -473,6 +477,11 @@ function hasAccount(accountCategory: AccountCategory): boolean {
|
|||||||
return accountsStore.hasAccount(accountCategory, !showHidden.value);
|
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 {
|
function accountCurrency(account: Account): string | null {
|
||||||
if (account.type === AccountType.SingleAccount.type) {
|
if (account.type === AccountType.SingleAccount.type) {
|
||||||
return getCurrencyName(account.currency);
|
return getCurrencyName(account.currency);
|
||||||
|
|||||||
@@ -129,9 +129,20 @@
|
|||||||
v-model="account.creditCardStatementDate"
|
v-model="account.creditCardStatementDate"
|
||||||
></v-autocomplete>
|
></v-autocomplete>
|
||||||
</v-col>
|
</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-col cols="12" :md="(!editAccountId || isNewAccount(selectedAccount)) && selectedAccount.balance ? 6 : 12"
|
||||||
v-if="account.type === AccountType.SingleAccount.type || currentAccountIndex >= 0">
|
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"
|
:persistent-placeholder="true"
|
||||||
:currency="selectedAccount.currency"
|
:currency="selectedAccount.currency"
|
||||||
:show-currency="true"
|
:show-currency="true"
|
||||||
@@ -204,6 +215,9 @@ import { useAccountEditPageBase } from '@/views/base/accounts/AccountEditPageBas
|
|||||||
|
|
||||||
import { useUserStore } from '@/stores/user.ts';
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
import { useAccountsStore } from '@/stores/account.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 { itemAndIndex } from '@/core/base.ts';
|
||||||
import { AccountType } from '@/core/account.ts';
|
import { AccountType } from '@/core/account.ts';
|
||||||
@@ -254,6 +268,7 @@ const {
|
|||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const accountsStore = useAccountsStore();
|
const accountsStore = useAccountsStore();
|
||||||
|
const transactionsStore = useTransactionsStore();
|
||||||
|
|
||||||
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
|
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
|
||||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||||
@@ -261,6 +276,7 @@ const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
|||||||
const showState = ref<boolean>(false);
|
const showState = ref<boolean>(false);
|
||||||
const activeTab = ref<string>('account');
|
const activeTab = ref<string>('account');
|
||||||
const currentAccountIndex = ref<number>(-1);
|
const currentAccountIndex = ref<number>(-1);
|
||||||
|
const originalBalances = ref<Map<string, number>>(new Map());
|
||||||
|
|
||||||
const selectedAccount = computed<Account>(() => {
|
const selectedAccount = computed<Account>(() => {
|
||||||
if (currentAccountIndex.value < 0) {
|
if (currentAccountIndex.value < 0) {
|
||||||
@@ -310,6 +326,15 @@ function open(options?: { id?: string, currentAccount?: Account, category?: numb
|
|||||||
accountId: editAccountId.value
|
accountId: editAccountId.value
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
setAccount(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;
|
loading.value = false;
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
loading.value = false;
|
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;
|
const problemMessage = inputEmptyProblemMessage.value;
|
||||||
|
|
||||||
if (problemMessage) {
|
if (problemMessage) {
|
||||||
@@ -347,6 +372,30 @@ function save(): void {
|
|||||||
|
|
||||||
submitting.value = true;
|
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({
|
accountsStore.saveAccount({
|
||||||
account: account.value,
|
account: account.value,
|
||||||
subAccounts: subAccounts.value,
|
subAccounts: subAccounts.value,
|
||||||
@@ -366,6 +415,12 @@ function save(): void {
|
|||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
submitting.value = false;
|
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) {
|
if (!error.processed) {
|
||||||
snackbar.value?.showError(error);
|
snackbar.value?.showError(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,6 +172,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</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">
|
<v-card-text class="transaction-calendar-container pt-0" v-if="pageType === TransactionListPageType.Calendar.type">
|
||||||
<transaction-calendar day-has-transaction-class="font-weight-bold"
|
<transaction-calendar day-has-transaction-class="font-weight-bold"
|
||||||
:readonly="loading" :is-dark-mode="isDarkMode"
|
: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 { ThemeType } from '@/core/theme.ts';
|
||||||
import { TransactionType } from '@/core/transaction.ts';
|
import { TransactionType } from '@/core/transaction.ts';
|
||||||
import { TemplateType } from '@/core/template.ts';
|
import { TemplateType } from '@/core/template.ts';
|
||||||
|
import { AccountType } from '@/core/account.ts';
|
||||||
import type { TransactionCategory } from '@/models/transaction_category.ts';
|
import type { TransactionCategory } from '@/models/transaction_category.ts';
|
||||||
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
|
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
|
||||||
import type { TransactionTemplate } from '@/models/transaction_template.ts';
|
import type { TransactionTemplate } from '@/models/transaction_template.ts';
|
||||||
@@ -775,7 +786,8 @@ const {
|
|||||||
tt,
|
tt,
|
||||||
getAllRecentMonthDateRanges,
|
getAllRecentMonthDateRanges,
|
||||||
getWeekdayLongName,
|
getWeekdayLongName,
|
||||||
getCurrentNumeralSystemType
|
getCurrentNumeralSystemType,
|
||||||
|
formatAmountToLocalizedNumeralsWithCurrency
|
||||||
} = useI18n();
|
} = useI18n();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -875,6 +887,29 @@ const showFilterTagDialog = ref<boolean>(false);
|
|||||||
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
|
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
|
||||||
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
|
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 allPageCounts = computed<NameNumeralValue[]>(() => {
|
||||||
const pageCounts: NameNumeralValue[] = [];
|
const pageCounts: NameNumeralValue[] = [];
|
||||||
const availableCountPerPage: number[] = [ 5, 10, 15, 20, 25, 30, 50 ];
|
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)"
|
: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')"
|
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
|
||||||
:custom-selection-primary-text="sourceAccountName"
|
:custom-selection-primary-text="sourceAccountName"
|
||||||
|
:custom-selection-secondary-text="sourceAccountBalanceDisplay"
|
||||||
:label="tt(sourceAccountTitle)"
|
:label="tt(sourceAccountTitle)"
|
||||||
:placeholder="tt(sourceAccountTitle)"
|
:placeholder="tt(sourceAccountTitle)"
|
||||||
:items="allVisibleCategorizedAccounts"
|
:items="allVisibleCategorizedAccounts"
|
||||||
@@ -236,6 +237,7 @@
|
|||||||
:disabled="loading || submitting || !allVisibleAccounts.length"
|
:disabled="loading || submitting || !allVisibleAccounts.length"
|
||||||
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
|
:enable-filter="true" :filter-placeholder="tt('Find account')" :filter-no-items-text="tt('No available account')"
|
||||||
:custom-selection-primary-text="destinationAccountName"
|
:custom-selection-primary-text="destinationAccountName"
|
||||||
|
:custom-selection-secondary-text="destinationAccountBalanceDisplay"
|
||||||
:label="tt('Destination Account')"
|
:label="tt('Destination Account')"
|
||||||
:placeholder="tt('Destination Account')"
|
:placeholder="tt('Destination Account')"
|
||||||
:items="allVisibleCategorizedAccounts"
|
:items="allVisibleCategorizedAccounts"
|
||||||
@@ -510,6 +512,7 @@ import { SUPPORTED_IMAGE_EXTENSIONS } from '@/consts/file.ts';
|
|||||||
import { TransactionTemplate } from '@/models/transaction_template.ts';
|
import { TransactionTemplate } from '@/models/transaction_template.ts';
|
||||||
import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
|
import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
|
||||||
import { Transaction } from '@/models/transaction.ts';
|
import { Transaction } from '@/models/transaction.ts';
|
||||||
|
import type { Account } from '@/models/account.ts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getTimezoneOffsetMinutes,
|
getTimezoneOffsetMinutes,
|
||||||
@@ -567,7 +570,7 @@ const props = defineProps<{
|
|||||||
show?: boolean;
|
show?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { tt } = useI18n();
|
const { tt, formatAmountToLocalizedNumeralsWithCurrency } = useI18n();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mode,
|
mode,
|
||||||
@@ -644,6 +647,31 @@ const initOptions = ref<TransactionEditOptions | undefined>(undefined);
|
|||||||
let resolveFunc: ((response?: TransactionEditResponse) => void) | null = null;
|
let resolveFunc: ((response?: TransactionEditResponse) => void) | null = null;
|
||||||
let rejectFunc: ((reason?: unknown) => 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>(() => {
|
const sourceAmountColor = computed<string | undefined>(() => {
|
||||||
if (transaction.value.type === TransactionType.Expense) {
|
if (transaction.value.type === TransactionType.Expense) {
|
||||||
return 'expense';
|
return 'expense';
|
||||||
|
|||||||
@@ -108,6 +108,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</f7-list-item>
|
</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 :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>
|
<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 logouting = ref<boolean>(false);
|
||||||
const showThemePopup = ref<boolean>(false);
|
const showThemePopup = ref<boolean>(false);
|
||||||
const showTimezonePopup = ref<boolean>(false);
|
const showTimezonePopup = ref<boolean>(false);
|
||||||
|
|
||||||
const currentNickName = computed<string>(() => userStore.currentUserNickname || tt('User'));
|
const currentNickName = computed<string>(() => userStore.currentUserNickname || tt('User'));
|
||||||
|
|
||||||
const currentTheme = computed<string>({
|
const currentTheme = computed<string>({
|
||||||
@@ -176,6 +184,13 @@ const currentTimezoneName = computed<string>(() => {
|
|||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const expandCategoryTreeByDefault = computed<boolean>({
|
||||||
|
get: () => settingsStore.appSettings.expandCategoryTreeByDefault,
|
||||||
|
set: value => {
|
||||||
|
settingsStore.setExpandCategoryTreeByDefault(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const isEnableSwipeBack = computed<boolean>({
|
const isEnableSwipeBack = computed<boolean>({
|
||||||
get: () => settingsStore.appSettings.swipeBack,
|
get: () => settingsStore.appSettings.swipeBack,
|
||||||
set: value => {
|
set: value => {
|
||||||
|
|||||||
@@ -204,7 +204,24 @@
|
|||||||
<f7-list-item
|
<f7-list-item
|
||||||
link="#" no-chevron
|
link="#" no-chevron
|
||||||
class="list-item-with-header-and-title"
|
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')"
|
:header="account.isLiability ? tt('Account Outstanding Balance') : tt('Account Balance')"
|
||||||
:title="formatAccountDisplayBalance(account)"
|
:title="formatAccountDisplayBalance(account)"
|
||||||
@click="accountContext.showBalanceSheet = true"
|
@click="accountContext.showBalanceSheet = true"
|
||||||
@@ -334,6 +351,24 @@
|
|||||||
</list-item-selection-popup>
|
</list-item-selection-popup>
|
||||||
</f7-list-item>
|
</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-list-item :title="tt('Visible')" v-if="editAccountId">
|
||||||
<f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle>
|
<f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle>
|
||||||
</f7-list-item>
|
</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 { useAccountEditPageBase } from '@/views/base/accounts/AccountEditPageBase.ts';
|
||||||
|
|
||||||
import { useAccountsStore } from '@/stores/account.ts';
|
import { useAccountsStore } from '@/stores/account.ts';
|
||||||
|
import { useTransactionsStore } from '@/stores/transaction.ts';
|
||||||
|
|
||||||
import { itemAndIndex } from '@/core/base.ts';
|
import { itemAndIndex } from '@/core/base.ts';
|
||||||
import type { LocalizedCurrencyInfo } from '@/core/currency.ts';
|
import type { LocalizedCurrencyInfo } from '@/core/currency.ts';
|
||||||
import { AccountType } from '@/core/account.ts';
|
import { AccountType } from '@/core/account.ts';
|
||||||
import { ALL_ACCOUNT_ICONS } from '@/consts/icon.ts';
|
import { ALL_ACCOUNT_ICONS } from '@/consts/icon.ts';
|
||||||
|
import { KnownErrorCode } from '@/consts/api.ts';
|
||||||
import { ALL_ACCOUNT_COLORS } from '@/consts/color.ts';
|
import { ALL_ACCOUNT_COLORS } from '@/consts/color.ts';
|
||||||
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
|
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
|
||||||
import type { Account } from '@/models/account.ts';
|
import type { Account } from '@/models/account.ts';
|
||||||
@@ -550,6 +587,7 @@ interface AccountContext {
|
|||||||
showColorSelectionSheet: boolean;
|
showColorSelectionSheet: boolean;
|
||||||
showCurrencyPopup: boolean;
|
showCurrencyPopup: boolean;
|
||||||
showCreditCardStatementDatePopup: boolean;
|
showCreditCardStatementDatePopup: boolean;
|
||||||
|
showCreditLimitSheet: boolean;
|
||||||
showBalanceSheet: boolean;
|
showBalanceSheet: boolean;
|
||||||
showBalanceDateTimeSheet: boolean;
|
showBalanceDateTimeSheet: boolean;
|
||||||
balanceDateTimeSheetMode: string;
|
balanceDateTimeSheetMode: string;
|
||||||
@@ -594,17 +632,21 @@ const {
|
|||||||
} = useAccountEditPageBase();
|
} = useAccountEditPageBase();
|
||||||
|
|
||||||
const accountsStore = useAccountsStore();
|
const accountsStore = useAccountsStore();
|
||||||
|
const transactionsStore = useTransactionsStore();
|
||||||
|
|
||||||
const DEFAULT_ACCOUNT_CONTEXT: AccountContext = {
|
const DEFAULT_ACCOUNT_CONTEXT: AccountContext = {
|
||||||
showIconSelectionSheet: false,
|
showIconSelectionSheet: false,
|
||||||
showColorSelectionSheet: false,
|
showColorSelectionSheet: false,
|
||||||
showCurrencyPopup: false,
|
showCurrencyPopup: false,
|
||||||
showCreditCardStatementDatePopup: false,
|
showCreditCardStatementDatePopup: false,
|
||||||
|
showCreditLimitSheet: false,
|
||||||
showBalanceSheet: false,
|
showBalanceSheet: false,
|
||||||
showBalanceDateTimeSheet: false,
|
showBalanceDateTimeSheet: false,
|
||||||
balanceDateTimeSheetMode: 'time'
|
balanceDateTimeSheetMode: 'time'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const originalBalance = ref<number>(0);
|
||||||
|
|
||||||
const accountContext = ref<AccountContext>(Object.assign({}, DEFAULT_ACCOUNT_CONTEXT));
|
const accountContext = ref<AccountContext>(Object.assign({}, DEFAULT_ACCOUNT_CONTEXT));
|
||||||
const subAccountContexts = ref<AccountContext[]>([]);
|
const subAccountContexts = ref<AccountContext[]>([]);
|
||||||
const subAccountToDelete = ref<Account | null>(null);
|
const subAccountToDelete = ref<Account | null>(null);
|
||||||
@@ -621,6 +663,13 @@ function formatAccountDisplayBalance(selectedAccount: Account): string {
|
|||||||
return formatAmountToLocalizedNumeralsWithCurrency(balance, selectedAccount.currency);
|
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 {
|
function formatAccountBalanceDate(account: Account): string {
|
||||||
if (!isDefined(account.balanceTime)) {
|
if (!isDefined(account.balanceTime)) {
|
||||||
return '';
|
return '';
|
||||||
@@ -652,6 +701,7 @@ function init(): void {
|
|||||||
accountId: editAccountId.value
|
accountId: editAccountId.value
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
setAccount(response);
|
setAccount(response);
|
||||||
|
originalBalance.value = response.balance;
|
||||||
subAccountContexts.value = [];
|
subAccountContexts.value = [];
|
||||||
|
|
||||||
for (let i = 0; i < subAccounts.value.length; i++) {
|
for (let i = 0; i < subAccounts.value.length; i++) {
|
||||||
@@ -684,22 +734,43 @@ function save(): void {
|
|||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
showLoading(() => submitting.value);
|
showLoading(() => submitting.value);
|
||||||
|
|
||||||
accountsStore.saveAccount({
|
const balanceChanged = !!editAccountId.value && account.value.balance !== originalBalance.value;
|
||||||
account: account.value,
|
const adjustPromise = balanceChanged
|
||||||
subAccounts: subAccounts.value,
|
? transactionsStore.adjustAccountBalance({ accountId: editAccountId.value!, targetBalance: account.value.balance, currentBalance: originalBalance.value })
|
||||||
isEdit: !!editAccountId.value,
|
: Promise.resolve(true);
|
||||||
clientSessionId: clientSessionId.value
|
|
||||||
}).then(() => {
|
|
||||||
submitting.value = false;
|
|
||||||
hideLoading();
|
|
||||||
|
|
||||||
if (!editAccountId.value) {
|
adjustPromise.then(() => {
|
||||||
showToast('You have added a new account');
|
accountsStore.saveAccount({
|
||||||
} else {
|
account: account.value,
|
||||||
showToast('You have saved this account');
|
subAccounts: subAccounts.value,
|
||||||
}
|
isEdit: !!editAccountId.value,
|
||||||
|
clientSessionId: clientSessionId.value
|
||||||
|
}).then(() => {
|
||||||
|
submitting.value = false;
|
||||||
|
hideLoading();
|
||||||
|
|
||||||
router.back();
|
if (!editAccountId.value) {
|
||||||
|
showToast('You have added a new account');
|
||||||
|
} else {
|
||||||
|
showToast('You have saved this account');
|
||||||
|
}
|
||||||
|
|
||||||
|
router.back();
|
||||||
|
}).catch(error => {
|
||||||
|
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 => {
|
}).catch(error => {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
<div class="nested-list-item-title">
|
<div class="nested-list-item-title">
|
||||||
<span>{{ account.name }}</span>
|
<span>{{ account.name }}</span>
|
||||||
<div class="item-footer" v-if="account.comment">{{ account.comment }}</div>
|
<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>
|
||||||
<div class="nested-list-item-after" v-if="account.type === AccountType.MultiSubAccounts.type">
|
<div class="nested-list-item-after" v-if="account.type === AccountType.MultiSubAccounts.type">
|
||||||
<span>{{ accountBalance(account) }}</span>
|
<span>{{ accountBalance(account) }}</span>
|
||||||
@@ -241,7 +242,7 @@ const props = defineProps<{
|
|||||||
f7router: Router.Router;
|
f7router: Router.Router;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { tt, getCurrentLanguageTextDirection } = useI18n();
|
const { tt, getCurrentLanguageTextDirection, formatAmountToLocalizedNumeralsWithCurrency } = useI18n();
|
||||||
const { showAlert, showToast, routeBackOnError } = useI18nUIComponents();
|
const { showAlert, showToast, routeBackOnError } = useI18nUIComponents();
|
||||||
|
|
||||||
const {
|
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 {
|
function hasAccount(accountCategory: AccountCategory, visibleOnly: boolean): boolean {
|
||||||
return accountsStore.hasAccount(accountCategory, visibleOnly);
|
return accountsStore.hasAccount(accountCategory, visibleOnly);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@
|
|||||||
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
|
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
|
||||||
secondary-hidden-field="hidden"
|
secondary-hidden-field="hidden"
|
||||||
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
|
: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]"
|
:items="allCategories[CategoryType.Expense]"
|
||||||
v-model:show="showCategorySheet"
|
v-model:show="showCategorySheet"
|
||||||
v-model="transaction.expenseCategoryId">
|
v-model="transaction.expenseCategoryId">
|
||||||
@@ -151,6 +152,7 @@
|
|||||||
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
|
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
|
||||||
secondary-hidden-field="hidden"
|
secondary-hidden-field="hidden"
|
||||||
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
|
: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]"
|
:items="allCategories[CategoryType.Income]"
|
||||||
v-model:show="showCategorySheet"
|
v-model:show="showCategorySheet"
|
||||||
v-model="transaction.incomeCategoryId">
|
v-model="transaction.incomeCategoryId">
|
||||||
@@ -183,6 +185,7 @@
|
|||||||
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
|
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
|
||||||
secondary-hidden-field="hidden"
|
secondary-hidden-field="hidden"
|
||||||
:enable-filter="true" :filter-placeholder="tt('Find category')" :filter-no-items-text="tt('No available category')"
|
: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]"
|
:items="allCategories[CategoryType.Transfer]"
|
||||||
v-model:show="showCategorySheet"
|
v-model:show="showCategorySheet"
|
||||||
v-model="transaction.transferCategoryId">
|
v-model="transaction.transferCategoryId">
|
||||||
@@ -195,6 +198,7 @@
|
|||||||
:class="{ 'disabled': !allVisibleAccounts.length || (mode === TransactionEditPageMode.Edit && transaction.type === TransactionType.ModifyBalance), 'readonly': mode === TransactionEditPageMode.View }"
|
:class="{ 'disabled': !allVisibleAccounts.length || (mode === TransactionEditPageMode.Edit && transaction.type === TransactionType.ModifyBalance), 'readonly': mode === TransactionEditPageMode.View }"
|
||||||
:header="tt(sourceAccountTitle)"
|
:header="tt(sourceAccountTitle)"
|
||||||
:title="sourceAccountName"
|
:title="sourceAccountName"
|
||||||
|
:footer="sourceAccountBalanceDisplay"
|
||||||
@click="showSourceAccountSheet = true"
|
@click="showSourceAccountSheet = true"
|
||||||
>
|
>
|
||||||
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
|
<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 }"
|
:class="{ 'disabled': !allVisibleAccounts.length, 'readonly': mode === TransactionEditPageMode.View }"
|
||||||
:header="tt('Destination Account')"
|
:header="tt('Destination Account')"
|
||||||
:title="destinationAccountName"
|
:title="destinationAccountName"
|
||||||
|
:footer="destinationAccountBalanceDisplay"
|
||||||
v-if="transaction.type === TransactionType.Transfer"
|
v-if="transaction.type === TransactionType.Transfer"
|
||||||
@click="showDestinationAccountSheet = true"
|
@click="showDestinationAccountSheet = true"
|
||||||
>
|
>
|
||||||
@@ -459,10 +464,12 @@
|
|||||||
<f7-actions-button @click="showTransactionPictures = true">{{ tt('Add Picture') }}</f7-actions-button>
|
<f7-actions-button @click="showTransactionPictures = true">{{ tt('Add Picture') }}</f7-actions-button>
|
||||||
</f7-actions-group>
|
</f7-actions-group>
|
||||||
<f7-actions-group v-if="pageTypeAndMode?.type === TransactionEditPageType.Transaction && mode === TransactionEditPageMode.View && transaction.type !== TransactionType.ModifyBalance">
|
<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(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(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(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 @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-group>
|
<f7-actions-group>
|
||||||
<f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
|
<f7-actions-button bold close>{{ tt('Cancel') }}</f7-actions-button>
|
||||||
@@ -521,6 +528,7 @@ import {
|
|||||||
import { useSettingsStore } from '@/stores/setting.ts';
|
import { useSettingsStore } from '@/stores/setting.ts';
|
||||||
import { useUserStore } from '@/stores/user.ts';
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
import { useAccountsStore } from '@/stores/account.ts';
|
import { useAccountsStore } from '@/stores/account.ts';
|
||||||
|
import type { Account } from '@/models/account.ts';
|
||||||
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
|
||||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||||
import { useTransactionsStore } from '@/stores/transaction.ts';
|
import { useTransactionsStore } from '@/stores/transaction.ts';
|
||||||
@@ -570,7 +578,8 @@ const {
|
|||||||
formatDateTimeToLongDate,
|
formatDateTimeToLongDate,
|
||||||
formatDateTimeToLongTime,
|
formatDateTimeToLongTime,
|
||||||
formatGregorianTextualYearMonthDayToLongDate,
|
formatGregorianTextualYearMonthDayToLongDate,
|
||||||
parseAmountFromLocalizedNumerals
|
parseAmountFromLocalizedNumerals,
|
||||||
|
formatAmountToLocalizedNumeralsWithCurrency
|
||||||
} = useI18n();
|
} = useI18n();
|
||||||
const { showAlert, showConfirm, showToast, routeBackOnError } = useI18nUIComponents();
|
const { showAlert, showConfirm, showToast, routeBackOnError } = useI18nUIComponents();
|
||||||
|
|
||||||
@@ -640,6 +649,7 @@ const pictureInput = useTemplateRef<HTMLInputElement>('pictureInput');
|
|||||||
const isSupportClipboard = !!navigator.clipboard;
|
const isSupportClipboard = !!navigator.clipboard;
|
||||||
|
|
||||||
const loadingError = ref<unknown | null>(null);
|
const loadingError = ref<unknown | null>(null);
|
||||||
|
const isFirstEntry = ref<boolean>(true);
|
||||||
const removingPictureId = ref<string | null>(null);
|
const removingPictureId = ref<string | null>(null);
|
||||||
const transactionDateTimeSheetMode = ref<string>('time');
|
const transactionDateTimeSheetMode = ref<string>('time');
|
||||||
const showTimeInDefaultTimezone = ref<boolean>(false);
|
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 sourceAmountClass = computed<Record<string, boolean>>(() => {
|
||||||
const classes: Record<string, boolean> = {
|
const classes: Record<string, boolean> = {
|
||||||
'readonly': mode.value === TransactionEditPageMode.View,
|
'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}`);
|
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 {
|
function onPageAfterIn(): void {
|
||||||
routeBackOnError(props.f7router, loadingError);
|
routeBackOnError(props.f7router, loadingError);
|
||||||
|
|
||||||
@@ -1326,6 +1383,21 @@ function onPageAfterIn(): void {
|
|||||||
&& !geoLocationStatus.value && !transaction.value.geoLocation) {
|
&& !geoLocationStatus.value && !transaction.value.geoLocation) {
|
||||||
updateGeoLocation(false);
|
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 {
|
function onPageBeforeOut(): void {
|
||||||
|
|||||||
@@ -69,6 +69,16 @@
|
|||||||
</f7-link>
|
</f7-link>
|
||||||
</f7-toolbar>
|
</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">
|
<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"
|
<transaction-calendar calendar-class="justify-content-center" week-day-name-type="short"
|
||||||
:readonly="loading" :is-dark-mode="isDarkMode"
|
:readonly="loading" :is-dark-mode="isDarkMode"
|
||||||
@@ -633,6 +643,7 @@ import {
|
|||||||
} from '@/core/datetime.ts';
|
} from '@/core/datetime.ts';
|
||||||
import { type NumeralSystem, AmountFilterType } from '@/core/numeral.ts';
|
import { type NumeralSystem, AmountFilterType } from '@/core/numeral.ts';
|
||||||
import { TransactionType } from '@/core/transaction.ts';
|
import { TransactionType } from '@/core/transaction.ts';
|
||||||
|
import { AccountType } from '@/core/account.ts';
|
||||||
import type { TransactionCategory } from '@/models/transaction_category.ts';
|
import type { TransactionCategory } from '@/models/transaction_category.ts';
|
||||||
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
|
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
|
||||||
|
|
||||||
@@ -670,7 +681,8 @@ const {
|
|||||||
tt,
|
tt,
|
||||||
getCurrentLanguageTextDirection,
|
getCurrentLanguageTextDirection,
|
||||||
getCurrentNumeralSystemType,
|
getCurrentNumeralSystemType,
|
||||||
getWeekdayShortName
|
getWeekdayShortName,
|
||||||
|
formatAmountToLocalizedNumeralsWithCurrency
|
||||||
} = useI18n();
|
} = useI18n();
|
||||||
|
|
||||||
const { showAlert, showToast, routeBackOnError } = useI18nUIComponents();
|
const { showAlert, showToast, routeBackOnError } = useI18nUIComponents();
|
||||||
@@ -748,6 +760,29 @@ const textDirection = computed<TextDirection>(() => getCurrentLanguageTextDirect
|
|||||||
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
|
const numeralSystem = computed<NumeralSystem>(() => getCurrentNumeralSystemType());
|
||||||
const isDarkMode = computed<boolean>(() => environmentsStore.framework7DarkMode || false);
|
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[]>(() => {
|
const transactions = computed<TransactionMonthList[]>(() => {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -4,21 +4,21 @@
|
|||||||
"copyright": "Copyright (c) 2014 Manuel Martínez-Almeida",
|
"copyright": "Copyright (c) 2014 Manuel Martínez-Almeida",
|
||||||
"url": "https://github.com/gin-gonic/gin",
|
"url": "https://github.com/gin-gonic/gin",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "Cache gin's middleware",
|
||||||
"copyright": "Copyright (c) 2016 Gin-Gonic",
|
"copyright": "Copyright (c) 2016 Gin-Gonic",
|
||||||
"url": "https://github.com/gin-contrib/cache",
|
"url": "https://github.com/gin-contrib/cache",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "GZIP gin's middleware",
|
||||||
"copyright": "Copyright (c) 2017 Gin-Gonic",
|
"copyright": "Copyright (c) 2017 Gin-Gonic",
|
||||||
"url": "https://github.com/gin-contrib/gzip",
|
"url": "https://github.com/gin-contrib/gzip",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "xorm",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"copyright": "Copyright (c) 2023 urfave/cli maintainers",
|
"copyright": "Copyright (c) 2023 urfave/cli maintainers",
|
||||||
"url": "https://github.com/urfave/cli",
|
"url": "https://github.com/urfave/cli",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "INI",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"copyright": "Copyright (c) 2015 Dean Karn",
|
"copyright": "Copyright (c) 2015 Dean Karn",
|
||||||
"url": "https://github.com/go-playground/validator",
|
"url": "https://github.com/go-playground/validator",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "jwt-go",
|
||||||
@@ -86,42 +86,42 @@
|
|||||||
"copyright": "Copyright (c) 2011-2013, 'pq' Contributors. Portions Copyright (c) 2011 Blake Mizerany",
|
"copyright": "Copyright (c) 2011-2013, 'pq' Contributors. Portions Copyright (c) 2011 Blake Mizerany",
|
||||||
"url": "https://github.com/lib/pq",
|
"url": "https://github.com/lib/pq",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "go-sqlite3",
|
||||||
"copyright": "Copyright (c) 2014 Yasuhiro Matsumoto",
|
"copyright": "Copyright (c) 2014 Yasuhiro Matsumoto",
|
||||||
"url": "https://github.com/mattn/go-sqlite3",
|
"url": "https://github.com/mattn/go-sqlite3",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "Go Cryptography",
|
||||||
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
|
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
|
||||||
"url": "https://golang.org/x/crypto",
|
"url": "https://golang.org/x/crypto",
|
||||||
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
|
"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",
|
"name": "Go Networking",
|
||||||
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
|
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
|
||||||
"url": "https://golang.org/x/net",
|
"url": "https://golang.org/x/net",
|
||||||
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
|
"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",
|
"name": "Go Text",
|
||||||
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
|
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
|
||||||
"url": "https://golang.org/x/text",
|
"url": "https://golang.org/x/text",
|
||||||
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
|
"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",
|
"name": "Go OAuth2",
|
||||||
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
|
"copyright": "Copyright (c) 2009 The Go Authors. All rights reserved.",
|
||||||
"url": "https://golang.org/x/oauth2",
|
"url": "https://golang.org/x/oauth2",
|
||||||
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
|
"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",
|
"name": "go-oidc",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
"copyright": "MinIO Cloud Storage, (C) 2014-2020 MinIO, Inc.",
|
"copyright": "MinIO Cloud Storage, (C) 2014-2020 MinIO, Inc.",
|
||||||
"url": "https://github.com/minio/minio-go",
|
"url": "https://github.com/minio/minio-go",
|
||||||
"license": "Apache License 2.0",
|
"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",
|
"name": "gocron",
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
"copyright": "Copyright (c) 2016-2025 The excelize Authors. Copyright (c) 2011-2017 Geoffrey J. Teale",
|
"copyright": "Copyright (c) 2016-2025 The excelize Authors. Copyright (c) 2011-2017 Geoffrey J. Teale",
|
||||||
"url": "https://github.com/qax-os/excelize",
|
"url": "https://github.com/qax-os/excelize",
|
||||||
"license": "BSD 3-Clause \"New\" or \"Revised\" License",
|
"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",
|
"name": "xls",
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
"copyright": "Copyright (c) 2018-present, Yuxi (Evan) You and Vue contributors",
|
"copyright": "Copyright (c) 2018-present, Yuxi (Evan) You and Vue contributors",
|
||||||
"url": "https://github.com/vuejs/core",
|
"url": "https://github.com/vuejs/core",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "Pinia",
|
||||||
@@ -217,21 +217,21 @@
|
|||||||
"copyright": "Copyright (c) 2019-present Eduardo San Martin Morote",
|
"copyright": "Copyright (c) 2019-present Eduardo San Martin Morote",
|
||||||
"url": "https://github.com/vuejs/router",
|
"url": "https://github.com/vuejs/router",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "vue-i18n",
|
||||||
"copyright": "Copyright (c) 2016 kazuya kawaguchi",
|
"copyright": "Copyright (c) 2016 kazuya kawaguchi",
|
||||||
"url": "https://github.com/intlify/vue-i18n-next",
|
"url": "https://github.com/intlify/vue-i18n-next",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "vuetify",
|
||||||
"copyright": "Copyright (c) 2016-now Vuetify, LLC",
|
"copyright": "Copyright (c) 2016-now Vuetify, LLC",
|
||||||
"url": "https://vuetifyjs.com",
|
"url": "https://vuetifyjs.com",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "register-service-worker",
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
"copyright": "Copyright (c) 2019 Vladimir Kharlampidi",
|
"copyright": "Copyright (c) 2019 Vladimir Kharlampidi",
|
||||||
"url": "https://swiperjs.com",
|
"url": "https://swiperjs.com",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "Skeleton Elements",
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
"copyright": "Copyright (c) 2014-present Matt Zabriskie & Collaborators",
|
"copyright": "Copyright (c) 2014-present Matt Zabriskie & Collaborators",
|
||||||
"url": "https://axios-http.com",
|
"url": "https://axios-http.com",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "Moment.js",
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
"copyright": "Copyright (c) JS Foundation and other contributors",
|
"copyright": "Copyright (c) JS Foundation and other contributors",
|
||||||
"url": "https://momentjs.com",
|
"url": "https://momentjs.com",
|
||||||
"license": "MIT License",
|
"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",
|
"name": "Leaflet",
|
||||||
|
|||||||
Reference in New Issue
Block a user