Compare commits
340 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6787d0591e | |||
| d78dada5ec | |||
| 74844b9a99 | |||
| a29ff0d553 | |||
| 6632dd64b3 | |||
| a8c912c4c2 | |||
| 2a02816127 | |||
| 66b950b4aa | |||
| 639bd9c5cd | |||
| 2bf8c0b501 | |||
| 847d5121aa | |||
| ab6e89594e | |||
| 0662427cec | |||
| d3b283f623 | |||
| 385d97ba15 | |||
| 09574f1c75 | |||
| 34247be52c | |||
| 56b1e1f565 | |||
| 7c6a3081ee | |||
| beeeb1c059 | |||
| 47f70098df | |||
| 4e6b708834 | |||
| a359b07ef3 | |||
| bb0524c559 | |||
| d70ea1987a | |||
| 001de8a1e0 | |||
| f045e8ffcd | |||
| f8be1222d6 | |||
| 8848fe8b33 | |||
| 32524dac56 | |||
| a50ecf4d9c | |||
| 98fda4e5d5 | |||
| f0c111f02a | |||
| bcc36e1533 | |||
| 872639fefa | |||
| e83b959930 | |||
| 3f8de39683 | |||
| 9430f57a0b | |||
| 703ceb44e4 | |||
| 3954db9b99 | |||
| 7abb5972eb | |||
| 377a4899b7 | |||
| d769e833e7 | |||
| 9786f96fe5 | |||
| bd2a672c12 | |||
| 96cb45dd45 | |||
| cf370c083b | |||
| 43e1780dc8 | |||
| 6aac810450 | |||
| f14c283a83 | |||
| e78b2cafb1 | |||
| a9eaf011cd | |||
| 7fca519fd9 | |||
| a9a37b0c97 | |||
| 8f55bd0df1 | |||
| 85e88949c4 | |||
| 80bcebbf66 | |||
| 7cae873830 | |||
| 7a1b27927f | |||
| 306da60752 | |||
| 4274b90b1e | |||
| 0b5721671d | |||
| 30575d15d0 | |||
| 0ca2f8b4a7 | |||
| 2e01e5530c | |||
| 13cc6a2cf0 | |||
| 35ba5dcc9f | |||
| ab58109e5e | |||
| 18a6d25ed6 | |||
| a0e3a269a0 | |||
| 1658d0758c | |||
| 6f0c59bba4 | |||
| 1e98a0df55 | |||
| fa77d3e837 | |||
| a21bc7aad7 | |||
| e665fac956 | |||
| e60c633e56 | |||
| a444526743 | |||
| b65a246fcb | |||
| c5e8a50033 | |||
| 6021c24da9 | |||
| 0e4cd10376 | |||
| f2c043a299 | |||
| 596787b998 | |||
| bb3a0c4444 | |||
| 624b9cb20b | |||
| 5a6c25d616 | |||
| eb178e7bed | |||
| 373d71c124 | |||
| 6618cfeceb | |||
| 58c1382570 | |||
| 721eb122bd | |||
| 95205d2f1d | |||
| b6efa91879 | |||
| 29d7ee09c8 | |||
| 68cb5bc523 | |||
| bd96b2398a | |||
| 1579882475 | |||
| b077b99806 | |||
| 9797e7e58f | |||
| 00f62fd608 | |||
| 833e767e6c | |||
| 3e7b3297aa | |||
| 8e170a69e8 | |||
| 00d6f5d473 | |||
| 3c363788d8 | |||
| cc920cff9a | |||
| 5d78d56f0c | |||
| b9b47c4428 | |||
| a5382e9fdd | |||
| 376f5b2650 | |||
| 61c5f75006 | |||
| 9a6148fe6e | |||
| ff6a558e96 | |||
| 3ad45bebb7 | |||
| 6b152bd778 | |||
| aacde2dfde | |||
| 6971eccb22 | |||
| a5e7c483ef | |||
| 319f97bf9e | |||
| da31a67c52 | |||
| af355e5b85 | |||
| ca9fe264b4 | |||
| c07e937702 | |||
| 371b88c6cd | |||
| 782bc11950 | |||
| 50c3fee7dc | |||
| 51c4e06e59 | |||
| 2a84f44f2c | |||
| 4878c7258d | |||
| e2a4e0cb3f | |||
| fd3457af84 | |||
| 8c0a9062a2 | |||
| 10d301aa3c | |||
| d7193847c5 | |||
| 8ea079679c | |||
| 95c7b498ff | |||
| 1156499b05 | |||
| e92aaffc94 | |||
| 8dc38912c9 | |||
| d228bf12bb | |||
| 7dff8a2ed5 | |||
| 814fe02949 | |||
| da702d6316 | |||
| fd909023f9 | |||
| c94c455b8b | |||
| d39b0ee077 | |||
| 99a2f40a4e | |||
| acaad355ed | |||
| ad4b351a32 | |||
| 2902fae1df | |||
| a0b9ca7fae | |||
| 05d8f8b9ab | |||
| d074a9d54a | |||
| d0274013cf | |||
| 1e0169a9b7 | |||
| c619d2ecad | |||
| a27a2556aa | |||
| 8207373a05 | |||
| 986fab9cbf | |||
| 2025551f3c | |||
| 3d934ab018 | |||
| 6a59ed0984 | |||
| 8fce3f2bcc | |||
| eca0574e41 | |||
| fa044f5972 | |||
| 6d758f338b | |||
| 28322bad5e | |||
| a9805b8fff | |||
| eb16b7fbb8 | |||
| 70428b6c96 | |||
| 85557c2879 | |||
| 5bf7f77520 | |||
| 3cdc7c947f | |||
| 84fc6b2ffb | |||
| e7612f6f0c | |||
| 58097331da | |||
| e053528abf | |||
| 568abb6e03 | |||
| 852c7899b5 | |||
| f4998da4cd | |||
| 9d9e6ef9bd | |||
| fb367174b6 | |||
| 87566010be | |||
| 2f7cdfd786 | |||
| 559e8259be | |||
| 929d3febb0 | |||
| 92df98c3fb | |||
| 019c993313 | |||
| 3c370b7ac7 | |||
| 1afd811aa8 | |||
| 0a999d56c7 | |||
| 4f6988d775 | |||
| 9f2bbe527e | |||
| 965be837a3 | |||
| f5f8b9a145 | |||
| f3b6c1266d | |||
| c675057ab1 | |||
| 20e95e35aa | |||
| f22666e756 | |||
| 3567ac170a | |||
| 260bd952d4 | |||
| e294f04b04 | |||
| 27fa5625be | |||
| 62623e6a23 | |||
| 9df436d31e | |||
| c03f74154b | |||
| 749bdfd164 | |||
| 6878d5260d | |||
| 4f21762533 | |||
| adebc96637 | |||
| 3a50e6d2de | |||
| b09b66adc3 | |||
| 6ef42a9303 | |||
| 922c338387 | |||
| dc4310c301 | |||
| 0b7fd647e6 | |||
| 081b270f04 | |||
| 29c09cb10a | |||
| cd2e6c1aae | |||
| d3e6756c22 | |||
| 7d31812055 | |||
| 8ce871e9bb | |||
| 30125f0faa | |||
| f4ea9a85f0 | |||
| 593e123610 | |||
| 4e8eddd868 | |||
| 041dbcb5c7 | |||
| 215a0163b6 | |||
| a38867ce01 | |||
| 9026c526f8 | |||
| c1f94a4499 | |||
| 2be329974e | |||
| abb26ac410 | |||
| c5f03165bc | |||
| 41452ac20b | |||
| a285707b53 | |||
| cc9a5eea36 | |||
| 89d5cc31af | |||
| f0e11e952c | |||
| 0f8431ff5c | |||
| 659b819011 | |||
| 6a62cfdef7 | |||
| f2ebd751d4 | |||
| 1414f54a12 | |||
| c3265c5bf6 | |||
| 13e322bb57 | |||
| 5cacfc8daf | |||
| 9bbe4d2dcf | |||
| b517409229 | |||
| 75a96e871a | |||
| 395f7dfd63 | |||
| b166f6ff56 | |||
| eb2b6d1002 | |||
| 6cb045453a | |||
| ffae9e81a7 | |||
| dc59d3954a | |||
| 7b26eb50bf | |||
| c0ab1ad793 | |||
| c73dcb51e4 | |||
| 8c7fc0fef9 | |||
| 76a2e24d06 | |||
| 3e6a054913 | |||
| 89b233e51b | |||
| 61f26e060e | |||
| 5649bb243d | |||
| ea90e97f92 | |||
| 3c624188d1 | |||
| 04f373e931 | |||
| b2e36a24fd | |||
| 8da3d2aa35 | |||
| 25c8b9baf8 | |||
| 1555052e1d | |||
| b1fbf91d6e | |||
| 5dfac0c085 | |||
| cb142a65f3 | |||
| b0a9b2366e | |||
| ed897d4105 | |||
| 2b71723ba1 | |||
| 60ba3b7977 | |||
| e0198da52c | |||
| 6365805715 | |||
| 5e7e3696bf | |||
| 166fae425d | |||
| f56bef40d8 | |||
| ad1eec7d47 | |||
| 5b241d2547 | |||
| 92a626fb21 | |||
| 5171f23c09 | |||
| 6dc0ebcac6 | |||
| 49f1f3c86b | |||
| 53ab441486 | |||
| 0e422b5a8f | |||
| 4f51480af9 | |||
| fb8fbbcf70 | |||
| 5256eff88d | |||
| 7c40157cba | |||
| 454e97c9f1 | |||
| 41d34af4c7 | |||
| a13f5bfb10 | |||
| da06fe4a7b | |||
| 061ea6aab4 | |||
| a19cc81391 | |||
| 871164b969 | |||
| 16fa77eb09 | |||
| a46399cbaf | |||
| a9e50d29d3 | |||
| e7d7f217a9 | |||
| 000c2b9ab0 | |||
| 37fdb161ea | |||
| 2d923bbdc9 | |||
| 07c55de024 | |||
| 30c463627a | |||
| 5eec635146 | |||
| 229d9c76c3 | |||
| 0119eadc14 | |||
| c1c656ab7e | |||
| e4a50bcd60 | |||
| d18e6df1c0 | |||
| af9aa726f4 | |||
| 27f8c90dae | |||
| b9a3c384d9 | |||
| eed7085756 | |||
| abb0c2ad16 | |||
| 9f7b40381c | |||
| ad9a390b58 | |||
| 5525635df1 | |||
| 863e0205ff | |||
| 2560a70e5e | |||
| b638a73e4d | |||
| 0a9cea4df3 | |||
| 15a98c3eac | |||
| 493c16087d | |||
| dd155a0f63 | |||
| 9ce1c8d397 | |||
| 3040435c06 | |||
| 685f93f05e | |||
| d465d9da1a | |||
| 50c2766014 | |||
| 4e1cbf13c6 |
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
@@ -10,11 +10,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
|
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
|
||||||
@@ -24,10 +24,12 @@ jobs:
|
|||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
image: tonistiigi/binfmt:qemu-v8.1.5
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Set up the environment
|
- name: Set up the environment
|
||||||
run: |
|
run: |
|
||||||
@@ -44,7 +46,7 @@ jobs:
|
|||||||
chmod +x docker/custom-frontend-pre-setup.sh
|
chmod +x docker/custom-frontend-pre-setup.sh
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
context: .
|
context: .
|
||||||
@@ -52,5 +54,6 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
RELEASE_BUILD=1
|
RELEASE_BUILD=1
|
||||||
|
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
|
${{ secrets.DOCKER_REPO }}/mayswind/ezbookkeeping
|
||||||
@@ -24,10 +24,12 @@ jobs:
|
|||||||
type=sha,format=short,prefix=SNAPSHOT-
|
type=sha,format=short,prefix=SNAPSHOT-
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
image: tonistiigi/binfmt:qemu-v8.1.5
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Set up the environment
|
- name: Set up the environment
|
||||||
run: |
|
run: |
|
||||||
@@ -44,7 +46,7 @@ jobs:
|
|||||||
chmod +x docker/custom-frontend-pre-setup.sh
|
chmod +x docker/custom-frontend-pre-setup.sh
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
|
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
|
||||||
@@ -24,19 +24,21 @@ jobs:
|
|||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
image: tonistiigi/binfmt:qemu-v8.1.5
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
context: .
|
context: .
|
||||||
@@ -48,5 +50,6 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
RELEASE_BUILD=1
|
RELEASE_BUILD=1
|
||||||
|
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
|
${{ secrets.DOCKER_USERNAME }}/ezbookkeeping
|
||||||
@@ -23,19 +23,21 @@ jobs:
|
|||||||
type=raw,value=latest-snapshot
|
type=raw,value=latest-snapshot
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
image: tonistiigi/binfmt:qemu-v8.1.5
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
context: .
|
context: .
|
||||||
@@ -46,6 +48,6 @@ jobs:
|
|||||||
linux/arm/v6
|
linux/arm/v6
|
||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -11,20 +11,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
build-args: |
|
build-args: |
|
||||||
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
SKIP_TESTS=${{ vars.SKIP_TESTS }}
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
# Build backend binary file
|
# Build backend binary file
|
||||||
FROM golang:1.23.4-alpine3.21 AS be-builder
|
FROM golang:1.24.0-alpine3.21 AS be-builder
|
||||||
ARG RELEASE_BUILD
|
ARG RELEASE_BUILD
|
||||||
ARG SKIP_TESTS
|
ARG SKIP_TESTS
|
||||||
ENV RELEASE_BUILD=$RELEASE_BUILD
|
ENV RELEASE_BUILD=$RELEASE_BUILD
|
||||||
@@ -11,7 +11,7 @@ RUN apk add git gcc g++ libc-dev
|
|||||||
RUN ./build.sh backend
|
RUN ./build.sh backend
|
||||||
|
|
||||||
# Build frontend files
|
# Build frontend files
|
||||||
FROM --platform=$BUILDPLATFORM node:20.18.1-alpine3.21 AS fe-builder
|
FROM --platform=$BUILDPLATFORM node:22.14.0-alpine3.21 AS fe-builder
|
||||||
ARG RELEASE_BUILD
|
ARG RELEASE_BUILD
|
||||||
ENV RELEASE_BUILD=$RELEASE_BUILD
|
ENV RELEASE_BUILD=$RELEASE_BUILD
|
||||||
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
WORKDIR /go/src/github.com/mayswind/ezbookkeeping
|
||||||
@@ -21,7 +21,7 @@ RUN apk add git
|
|||||||
RUN ./build.sh frontend
|
RUN ./build.sh frontend
|
||||||
|
|
||||||
# Package docker image
|
# Package docker image
|
||||||
FROM alpine:3.21.0
|
FROM alpine:3.21.3
|
||||||
LABEL maintainer="MaysWind <i@mayswind.net>"
|
LABEL maintainer="MaysWind <i@mayswind.net>"
|
||||||
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
|
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
|
||||||
RUN apk --no-cache add tzdata
|
RUN apk --no-cache add tzdata
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020-2024 MaysWind (i@mayswind.net)
|
Copyright (c) 2020-2025 MaysWind (i@mayswind.net)
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[](https://github.com/mayswind/ezbookkeeping/releases)
|
[](https://github.com/mayswind/ezbookkeeping/releases)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself. It can be deployed on almost all platforms, including Windows, macOS and Linux on x86, amd64 and ARM architectures. You can even deploy it on an raspberry device. It also supports many different databases, including sqlite and mysql. With docker, you can just deploy it via one command without complicated configuration.
|
ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself. It can be deployed on almost all platforms, including Windows, macOS and Linux on x86, amd64 and ARM architectures. You can even deploy it on an raspberry device. It also supports many different databases, including SQLite, MySQL and PostgreSQL. With docker, you can just deploy it via one command without complicated configuration.
|
||||||
|
|
||||||
Online Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.mayswind.net)
|
Online Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.mayswind.net)
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ Online Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-dem
|
|||||||
7. Multi-language support
|
7. Multi-language support
|
||||||
8. Two-factor authentication
|
8. Two-factor authentication
|
||||||
9. Application lock (PIN code / WebAuthn)
|
9. Application lock (PIN code / WebAuthn)
|
||||||
10. Data import & export
|
10. Data export & import (OFX, QFX, QIF, IIF, CSV, GnuCash, FireFly III, etc.)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
### Desktop Version
|
### Desktop Version
|
||||||
@@ -88,7 +88,7 @@ You can also build docker image, make sure you have [docker](https://www.docker.
|
|||||||
|
|
||||||
## Documents
|
## Documents
|
||||||
1. [English](http://ezbookkeeping.mayswind.net)
|
1. [English](http://ezbookkeeping.mayswind.net)
|
||||||
1. [简体中文 (Simplified Chinese)](http://ezbookkeeping.mayswind.net/zh_Hans)
|
1. [中文 (简体)](http://ezbookkeeping.mayswind.net/zh_Hans)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[MIT](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
|
[MIT](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
|
||||||
|
|||||||
+5
-1
@@ -792,7 +792,11 @@ func exportUserTransaction(c *core.CliContext) error {
|
|||||||
filePath := c.String("file")
|
filePath := c.String("file")
|
||||||
fileType := c.String("type")
|
fileType := c.String("type")
|
||||||
|
|
||||||
if fileType != "" && fileType != "csv" && fileType != "tsv" {
|
if fileType == "" {
|
||||||
|
fileType = "csv"
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileType != "csv" && fileType != "tsv" {
|
||||||
log.CliErrorf(c, "[user_data.exportUserTransaction] export file type is not supported")
|
log.CliErrorf(c, "[user_data.exportUserTransaction] export file type is not supported")
|
||||||
return errs.ErrNotSupported
|
return errs.ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,8 +116,13 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
router.StaticFile("favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
|
router.StaticFile("favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
|
||||||
router.StaticFile("touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
|
router.StaticFile("touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
|
||||||
router.StaticFile("manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
router.StaticFile("manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
|
||||||
|
router.StaticFile("sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
|
||||||
router.GET("/server_settings.js", bindCachedJs(api.ServerSettings.ServerSettingsJavascriptHandler, serverSettingsCacheStore))
|
router.GET("/server_settings.js", bindCachedJs(api.ServerSettings.ServerSettingsJavascriptHandler, serverSettingsCacheStore))
|
||||||
|
|
||||||
|
for i := 0; i < len(workboxFileNames); i++ {
|
||||||
|
router.StaticFile("/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
|
||||||
|
}
|
||||||
|
|
||||||
router.StaticFile("/mobile", filepath.Join(config.StaticRootPath, "mobile.html"))
|
router.StaticFile("/mobile", filepath.Join(config.StaticRootPath, "mobile.html"))
|
||||||
router.Static("/mobile/js", filepath.Join(config.StaticRootPath, "js"))
|
router.Static("/mobile/js", filepath.Join(config.StaticRootPath, "js"))
|
||||||
router.Static("/mobile/css", filepath.Join(config.StaticRootPath, "css"))
|
router.Static("/mobile/css", filepath.Join(config.StaticRootPath, "css"))
|
||||||
@@ -310,6 +315,7 @@ func startWebServer(c *core.CliContext) error {
|
|||||||
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
|
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
|
||||||
|
|
||||||
if config.EnableDataImport {
|
if config.EnableDataImport {
|
||||||
|
apiV1Route.POST("/transactions/parse_dsv_file.json", bindApi(api.Transactions.TransactionParseImportDsvFileDataHandler))
|
||||||
apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler))
|
apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler))
|
||||||
apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler))
|
apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,12 @@ email_verify_token_expired_time = 3600
|
|||||||
# Password reset token expired seconds (60 - 4294967295), default is 3600 (60 minutes)
|
# Password reset token expired seconds (60 - 4294967295), default is 3600 (60 minutes)
|
||||||
password_reset_token_expired_time = 3600
|
password_reset_token_expired_time = 3600
|
||||||
|
|
||||||
|
# Maximum count of password / token check failures (0 - 4294967295) per IP per minute (use the above duplicate checker), default is 5, set to 0 to disable
|
||||||
|
max_failures_per_ip_per_minute = 5
|
||||||
|
|
||||||
|
# Maximum count of password / token check failures (0 - 4294967295) per user per minute (use the above duplicate checker), default is 5, set to 0 to disable
|
||||||
|
max_failures_per_user_per_minute = 5
|
||||||
|
|
||||||
# Add X-Request-Id header to response to track user request or error, default is true
|
# Add X-Request-Id header to response to track user request or error, default is true
|
||||||
request_id_header = true
|
request_id_header = true
|
||||||
|
|
||||||
@@ -248,7 +254,7 @@ enable_tips_in_login_page = false
|
|||||||
|
|
||||||
# The custom tips displayed in login page, it supports multi-language configuration
|
# The custom tips displayed in login page, it supports multi-language configuration
|
||||||
# Add an underscore and a language tag after the setting key to configure the notification content in that language, the same below
|
# Add an underscore and a language tag after the setting key to configure the notification content in that language, the same below
|
||||||
# For example, login_page_tips_content_zh_hans means the notification content in Simplified Chinese
|
# For example, login_page_tips_content_zh_hans means the notification content in Chinese (Simplified)
|
||||||
login_page_tips_content =
|
login_page_tips_content =
|
||||||
|
|
||||||
[notification]
|
[notification]
|
||||||
@@ -257,7 +263,7 @@ enable_notification_after_register = false
|
|||||||
|
|
||||||
# The notification content displayed each time users register, it supports multi-language configuration
|
# The notification content displayed each time users register, it supports multi-language configuration
|
||||||
# Add an underscore and a language tag after the setting key to configure the notification content in that language, the same below
|
# Add an underscore and a language tag after the setting key to configure the notification content in that language, the same below
|
||||||
# For example, after_login_notification_content_zh_hans means the notification content in Simplified Chinese
|
# For example, after_login_notification_content_zh_hans means the notification content in Chinese (Simplified)
|
||||||
after_register_notification_content =
|
after_register_notification_content =
|
||||||
|
|
||||||
# Set to true to display custom notification in home page every time users login
|
# Set to true to display custom notification in home page every time users login
|
||||||
@@ -315,7 +321,7 @@ amap_application_key =
|
|||||||
# "external_proxy": use an external proxy to request amap api (amap application secret should be set by external proxy)
|
# "external_proxy": use an external proxy to request amap api (amap application secret should be set by external proxy)
|
||||||
# "plain_text": append amap application secret to frontend request directly (insecurity for public network)
|
# "plain_text": append amap application secret to frontend request directly (insecurity for public network)
|
||||||
# Please visit https://developer.amap.com/api/jsapi-v2/guide/abc/load for more information
|
# Please visit https://developer.amap.com/api/jsapi-v2/guide/abc/load for more information
|
||||||
amap_security_verification_method = plain_text
|
amap_security_verification_method = internal_proxy
|
||||||
|
|
||||||
# For "amap" map provider only, Amap JavaScript API application secret, this setting must be provided when "amap_security_verification_method" is set to "internal_proxy" or "plain_text", please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
|
# For "amap" map provider only, Amap JavaScript API application secret, this setting must be provided when "amap_security_verification_method" is set to "internal_proxy" or "plain_text", please visit https://lbs.amap.com/api/javascript-api/guide/abc/prepare for more information
|
||||||
amap_application_secret =
|
amap_application_secret =
|
||||||
|
|||||||
+27
-32
@@ -1,36 +1,31 @@
|
|||||||
import globals from 'globals';
|
import pluginVue from 'eslint-plugin-vue';
|
||||||
|
import vueTsEslintConfig from '@vue/eslint-config-typescript';
|
||||||
|
|
||||||
import path from 'node:path';
|
export default [
|
||||||
import { fileURLToPath } from 'node:url';
|
...pluginVue.configs['flat/essential'],
|
||||||
|
...vueTsEslintConfig(),
|
||||||
import js from '@eslint/js';
|
{
|
||||||
import { FlatCompat } from '@eslint/eslintrc';
|
languageOptions: {
|
||||||
import { includeIgnoreFile } from '@eslint/compat';
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
tsconfigRootDir: import.meta.dirname,
|
||||||
const __dirname = path.dirname(__filename);
|
}
|
||||||
const gitignorePath = path.resolve(__dirname, '.gitignore');
|
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: __dirname,
|
|
||||||
recommendedConfig: js.configs.recommended,
|
|
||||||
allConfig: js.configs.all
|
|
||||||
});
|
|
||||||
|
|
||||||
export default [...compat.extends('eslint:recommended', 'plugin:vue/vue3-essential'),
|
|
||||||
includeIgnoreFile(gitignorePath), {
|
|
||||||
languageOptions: {
|
|
||||||
globals: {
|
|
||||||
...globals.node,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
files: [
|
{
|
||||||
"**/*.{vue,js,jsx,cjs,mjs}"
|
ignores: [
|
||||||
],
|
'dist/**',
|
||||||
rules: {
|
'**/*.{js,jsx,cjs,mjs}'
|
||||||
'vue/no-use-v-if-with-v-for': 'off',
|
]
|
||||||
'vue/valid-v-slot': ['error', {
|
|
||||||
allowModifiers: true,
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
}];
|
{
|
||||||
|
files: [
|
||||||
|
'**/*.{vue,ts,tsx,mts,js,jsx,cjs,mjs}'
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'vue/valid-v-slot': ['error', {
|
||||||
|
allowModifiers: true
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
module github.com/mayswind/ezbookkeeping
|
module github.com/mayswind/ezbookkeeping
|
||||||
|
|
||||||
go 1.22
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/boombuler/barcode v1.0.2
|
github.com/boombuler/barcode v1.0.2
|
||||||
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.3.0
|
github.com/gin-contrib/cache v1.3.1
|
||||||
github.com/gin-contrib/gzip v1.0.1
|
github.com/gin-contrib/gzip v1.2.2
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-co-op/gocron/v2 v2.12.3
|
github.com/go-co-op/gocron/v2 v2.15.0
|
||||||
github.com/go-playground/validator/v10 v10.22.1
|
github.com/go-playground/validator/v10 v10.24.0
|
||||||
github.com/go-sql-driver/mysql v1.8.1
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
github.com/minio/minio-go/v7 v7.0.80
|
github.com/minio/minio-go/v7 v7.0.85
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/urfave/cli/v2 v2.27.4
|
github.com/urfave/cli/v2 v2.27.5
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.33.0
|
||||||
golang.org/x/net v0.30.0
|
golang.org/x/net v0.34.0
|
||||||
golang.org/x/text v0.19.0
|
golang.org/x/text v0.22.0
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
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
|
||||||
@@ -35,30 +35,30 @@ require (
|
|||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
|
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
|
||||||
github.com/buger/jsonparser v1.1.1 // indirect
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
github.com/bytedance/sonic v1.12.7 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic/loader v0.2.2 // 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
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
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.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // 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.3 // indirect
|
github.com/goccy/go-json v0.10.4 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/gomodule/redigo v1.8.9 // 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
|
||||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@@ -66,7 +66,7 @@ require (
|
|||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // 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
|
||||||
@@ -77,10 +77,10 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.13.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/sys v0.26.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
google.golang.org/protobuf v1.36.2 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMU
|
|||||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||||
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.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.2 h1:jxAJuN9fOot/cyz5Q6dUuMJF5OqQ6+5GfA8FjjQ0R4o=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.2/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
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=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||||
@@ -23,10 +24,9 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
|
|||||||
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -39,38 +39,38 @@ 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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/gin-contrib/cache v1.3.0 h1:wEEw38uvb4rTraQJVpd9ex4ZotXNlc0fSaSUsuPXS/w=
|
github.com/gin-contrib/cache v1.3.1 h1:EWjkOaLocs5fGt9feQaI7rt1GZbDyatFXEUh2/s3ZI8=
|
||||||
github.com/gin-contrib/cache v1.3.0/go.mod h1:EA63LrWGI5vwSI95TS5fgBrtxZ1tM2NKx+NrEeyEDcU=
|
github.com/gin-contrib/cache v1.3.1/go.mod h1:6Tme0p3QEF/Ck/KUcq7h/OAqZvUDjHRH1DtQbNgfIX0=
|
||||||
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
|
github.com/gin-contrib/gzip v1.2.2 h1:iUU/EYCM8ENfkjmZaVrxbjF/ZC267Iqv5S0MMCMEliI=
|
||||||
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
|
github.com/gin-contrib/gzip v1.2.2/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-co-op/gocron/v2 v2.12.3 h1:3JkKjkFoAPp/i0YE+sonlF5gi+xnBChwYh75nX16MaE=
|
github.com/go-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo=
|
||||||
github.com/go-co-op/gocron/v2 v2.12.3/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
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.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
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=
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
|
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
|
||||||
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
|
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -84,8 +84,8 @@ github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IX
|
|||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
@@ -104,8 +104,8 @@ github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv
|
|||||||
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/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.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk=
|
github.com/minio/minio-go/v7 v7.0.85 h1:9psTLS/NTvC3MWoyjhjXpwcKoNbkongaCSF3PNpSuXo=
|
||||||
github.com/minio/minio-go/v7 v7.0.80/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0=
|
github.com/minio/minio-go/v7 v7.0.85/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY=
|
||||||
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=
|
||||||
@@ -116,8 +116,8 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
|
|||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
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.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||||
@@ -142,8 +142,8 @@ 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
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=
|
||||||
@@ -152,34 +152,33 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
|
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||||
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
|
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||||
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 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
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=
|
||||||
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.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||||
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/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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
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/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.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
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=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
Generated
+2470
-1226
File diff suppressed because it is too large
Load Diff
+29
-23
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ezbookkeeping",
|
"name": "ezbookkeeping",
|
||||||
"version": "0.7.0",
|
"version": "0.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -15,52 +15,58 @@
|
|||||||
"serve": "cross-env NODE_ENV=development vite",
|
"serve": "cross-env NODE_ENV=development vite",
|
||||||
"build": "cross-env NODE_ENV=production vite build",
|
"build": "cross-env NODE_ENV=production vite build",
|
||||||
"serve:dist": "vite preview",
|
"serve:dist": "vite preview",
|
||||||
"lint": "eslint . --fix"
|
"lint": "tsc --noEmit && eslint . --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@vuepic/vue-datepicker": "^10.0.0",
|
"@vuepic/vue-datepicker": "^11.0.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.9",
|
||||||
"cbor-js": "^0.1.0",
|
"cbor-js": "^0.1.0",
|
||||||
"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": "^5.5.1",
|
"echarts": "^5.6.0",
|
||||||
"framework7": "^8.3.4",
|
"framework7": "^8.3.4",
|
||||||
"framework7-icons": "^5.0.5",
|
"framework7-icons": "^5.0.5",
|
||||||
"framework7-vue": "^8.3.4",
|
"framework7-vue": "^8.3.4",
|
||||||
"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.5.46",
|
"moment-timezone": "^0.5.47",
|
||||||
"pinia": "^2.2.5",
|
"pinia": "^2.3.1",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"skeleton-elements": "^4.0.1",
|
"skeleton-elements": "^4.0.1",
|
||||||
"swiper": "^10.2.0",
|
"swiper": "^10.2.0",
|
||||||
"ua-parser-js": "^1.0.39",
|
"ua-parser-js": "^1.0.39",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.13",
|
||||||
"vue-echarts": "^7.0.3",
|
"vue-echarts": "^7.0.3",
|
||||||
"vue-i18n": "^10.0.4",
|
"vue-i18n": "^11.1.1",
|
||||||
"vue-router": "^4.4.5",
|
"vue-router": "^4.5.0",
|
||||||
"vue3-perfect-scrollbar": "^2.0.0",
|
"vue3-perfect-scrollbar": "^2.0.0",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"vuetify": "^3.7.3"
|
"vuetify": "^3.7.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.2",
|
"@tsconfig/node22": "^22.0.0",
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@types/cbor-js": "^0.1.1",
|
||||||
"@eslint/js": "^9.14.0",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@types/git-rev-sync": "^2.0.2",
|
||||||
|
"@types/node": "^22.12.0",
|
||||||
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vue/eslint-config-typescript": "^14.3.0",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.20.0",
|
||||||
"eslint-plugin-vue": "^9.30.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"git-rev-sync": "^3.0.2",
|
"git-rev-sync": "^3.0.2",
|
||||||
"globals": "^15.11.0",
|
"postcss-preset-env": "^10.1.3",
|
||||||
"postcss-preset-env": "^10.0.9",
|
"sass": "^1.84.0",
|
||||||
"sass": "^1.80.6",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^5.4.10",
|
"vite": "^6.1.0",
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
"vite-plugin-pwa": "^0.21.1",
|
||||||
"vite-plugin-vuetify": "^2.0.4"
|
"vite-plugin-vuetify": "^2.1.0",
|
||||||
|
"vue-tsc": "^2.2.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ var (
|
|||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
},
|
},
|
||||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
container: duplicatechecker.Container,
|
container: duplicatechecker.Container,
|
||||||
},
|
},
|
||||||
accounts: services.Accounts,
|
accounts: services.Accounts,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
// AuthorizationsApi represents authorization api
|
// AuthorizationsApi represents authorization api
|
||||||
type AuthorizationsApi struct {
|
type AuthorizationsApi struct {
|
||||||
ApiUsingConfig
|
ApiUsingConfig
|
||||||
|
ApiUsingDuplicateChecker
|
||||||
ApiWithUserInfo
|
ApiWithUserInfo
|
||||||
users *services.UserService
|
users *services.UserService
|
||||||
tokens *services.TokenService
|
tokens *services.TokenService
|
||||||
@@ -27,6 +29,12 @@ var (
|
|||||||
ApiUsingConfig: ApiUsingConfig{
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
},
|
},
|
||||||
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
container: duplicatechecker.Container,
|
||||||
|
},
|
||||||
ApiWithUserInfo: ApiWithUserInfo{
|
ApiWithUserInfo: ApiWithUserInfo{
|
||||||
ApiUsingConfig: ApiUsingConfig{
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
@@ -51,7 +59,23 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err
|
|||||||
return nil, errs.ErrLoginNameOrPasswordInvalid
|
return nil, errs.ErrLoginNameOrPasswordInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.users.GetUserByUsernameOrEmailAndPassword(c, credential.LoginName, credential.Password)
|
err = a.CheckFailureCount(c, 0)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[authorizations.AuthorizeHandler] cannot login for user \"%s\", because %s", credential.LoginName, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, uid, err := a.users.GetUserByUsernameOrEmailAndPassword(c, credential.LoginName, credential.Password)
|
||||||
|
|
||||||
|
if errs.IsCustomError(err) {
|
||||||
|
failureCheckErr := a.CheckAndIncreaseFailureCount(c, uid)
|
||||||
|
|
||||||
|
if failureCheckErr != nil {
|
||||||
|
log.Warnf(c, "[authorizations.AuthorizeHandler] cannot login for user \"%s\", because %s", credential.LoginName, failureCheckErr.Error())
|
||||||
|
return nil, errs.Or(failureCheckErr, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
|
log.Warnf(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
|
||||||
@@ -133,6 +157,13 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any,
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
|
err = a.CheckFailureCount(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
|
||||||
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -142,6 +173,14 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any,
|
|||||||
|
|
||||||
if !totp.Validate(credential.Passcode, twoFactorSetting.Secret) {
|
if !totp.Validate(credential.Passcode, twoFactorSetting.Secret) {
|
||||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid)
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid)
|
||||||
|
|
||||||
|
err = a.CheckAndIncreaseFailureCount(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
|
||||||
return nil, errs.ErrPasscodeInvalid
|
return nil, errs.ErrPasscodeInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +235,13 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
|
err = a.CheckFailureCount(c, uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
|
||||||
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -226,6 +272,15 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC
|
|||||||
|
|
||||||
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(c, uid, credential.RecoveryCode, user.Salt)
|
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(c, uid, credential.RecoveryCode, user.Salt)
|
||||||
|
|
||||||
|
if errs.IsCustomError(err) {
|
||||||
|
failureCheckErr := a.CheckAndIncreaseFailureCount(c, uid)
|
||||||
|
|
||||||
|
if failureCheckErr != nil {
|
||||||
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] cannot auth for user \"uid:%d\", because %s", uid, failureCheckErr.Error())
|
||||||
|
return nil, errs.Or(failureCheckErr, errs.ErrFailureCountLimitReached)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
|
log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
return nil, errs.Or(err, errs.ErrTwoFactorRecoveryCodeNotExist)
|
return nil, errs.Or(err, errs.ErrTwoFactorRecoveryCodeNotExist)
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const internalTransactionPictureUrlFormat = "%spictures/%d.%s"
|
const internalTransactionPictureUrlFormat = "%spictures/%d.%s"
|
||||||
@@ -100,6 +104,7 @@ func (a *ApiUsingConfig) GetAfterOpenNotificationContent(userLanguage string, cl
|
|||||||
|
|
||||||
// ApiUsingDuplicateChecker represents an api that need to use duplicate checker
|
// ApiUsingDuplicateChecker represents an api that need to use duplicate checker
|
||||||
type ApiUsingDuplicateChecker struct {
|
type ApiUsingDuplicateChecker struct {
|
||||||
|
ApiUsingConfig
|
||||||
container *duplicatechecker.DuplicateCheckerContainer
|
container *duplicatechecker.DuplicateCheckerContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +118,67 @@ func (a *ApiUsingDuplicateChecker) SetSubmissionRemark(checkerType duplicatechec
|
|||||||
a.container.SetSubmissionRemark(checkerType, uid, identification, remark)
|
a.container.SetSubmissionRemark(checkerType, uid, identification, remark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckFailureCount returns whether the failure count of the specified IP and user has reached the limit and increases the failure count
|
||||||
|
func (a *ApiUsingDuplicateChecker) CheckFailureCount(c *core.WebContext, uid int64) error {
|
||||||
|
if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 {
|
||||||
|
clientIp := c.ClientIP()
|
||||||
|
ipFailureCount := a.container.GetFailureCount(clientIp)
|
||||||
|
|
||||||
|
if ipFailureCount >= a.CurrentConfig().MaxFailuresPerIpPerMinute {
|
||||||
|
log.Warnf(c, "[base.CheckFailureCount] operation failure via IP \"%s\", current failure count: %d reached the limit", clientIp, ipFailureCount)
|
||||||
|
return errs.ErrFailureCountLimitReached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 {
|
||||||
|
uidFailureCount := a.container.GetFailureCount(utils.Int64ToString(uid))
|
||||||
|
|
||||||
|
if uidFailureCount >= a.CurrentConfig().MaxFailuresPerUserPerMinute {
|
||||||
|
log.Warnf(c, "[base.CheckFailureCount] operation failure via uid \"%d\", current failure count: %d reached the limit", uid, uidFailureCount)
|
||||||
|
return errs.ErrFailureCountLimitReached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAndIncreaseFailureCount returns whether the failure count of the specified IP and user has reached the limit and increases the failure count
|
||||||
|
func (a *ApiUsingDuplicateChecker) CheckAndIncreaseFailureCount(c *core.WebContext, uid int64) error {
|
||||||
|
clientIp := c.ClientIP()
|
||||||
|
ipFailureCount := uint32(0)
|
||||||
|
uidFailureCount := uint32(0)
|
||||||
|
|
||||||
|
if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 {
|
||||||
|
ipFailureCount = a.container.GetFailureCount(clientIp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 {
|
||||||
|
uidFailureCount = a.container.GetFailureCount(utils.Int64ToString(uid))
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 && ipFailureCount < a.CurrentConfig().MaxFailuresPerIpPerMinute {
|
||||||
|
log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via IP \"%s\", previous failure count: %d", clientIp, ipFailureCount)
|
||||||
|
a.container.IncreaseFailureCount(clientIp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 && uidFailureCount < a.CurrentConfig().MaxFailuresPerUserPerMinute {
|
||||||
|
log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via uid \"%d\", previous failure count: %d", uid, uidFailureCount)
|
||||||
|
a.container.IncreaseFailureCount(utils.Int64ToString(uid))
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 && ipFailureCount >= a.CurrentConfig().MaxFailuresPerIpPerMinute {
|
||||||
|
log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via IP \"%s\", current failure count: %d reached the limit", clientIp, ipFailureCount)
|
||||||
|
return errs.ErrFailureCountLimitReached
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 && uidFailureCount >= a.CurrentConfig().MaxFailuresPerUserPerMinute {
|
||||||
|
log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via uid \"%d\", current failure count: %d reached the limit", uid, uidFailureCount)
|
||||||
|
return errs.ErrFailureCountLimitReached
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ApiUsingAvatarProvider represents an api that need to use avatar provider
|
// ApiUsingAvatarProvider represents an api that need to use avatar provider
|
||||||
type ApiUsingAvatarProvider struct {
|
type ApiUsingAvatarProvider struct {
|
||||||
container *avatars.AvatarProviderContainer
|
container *avatars.AvatarProviderContainer
|
||||||
|
|||||||
@@ -66,7 +66,12 @@ func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.WebContext) (any, *
|
|||||||
|
|
||||||
for i := 0; i < len(requests); i++ {
|
for i := 0; i < len(requests); i++ {
|
||||||
req := requests[i]
|
req := requests[i]
|
||||||
req.Header.Set("User-Agent", fmt.Sprintf("ezBookkeeping/%s ", settings.Version))
|
|
||||||
|
if len(req.Header.Values("User-Agent")) < 1 {
|
||||||
|
req.Header.Set("User-Agent", fmt.Sprintf("ezBookkeeping/%s", settings.Version))
|
||||||
|
} else if req.Header.Get("User-Agent") == "" {
|
||||||
|
req.Header.Del("User-Agent")
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestExchangeRatesApiLatestExchangeRateHandler_ReserveBankOfAustraliaDataSou
|
|||||||
|
|
||||||
assert.Equal(t, "AUD", exchangeRateResponse.BaseCurrency)
|
assert.Equal(t, "AUD", exchangeRateResponse.BaseCurrency)
|
||||||
|
|
||||||
supportedCurrencyCodes := []string{"CAD", "CNY", "EUR", "GBP", "HKD", "IDR", "INR", "JPY", "KRW",
|
supportedCurrencyCodes := []string{"CAD", "CHF", "CNY", "EUR", "GBP", "HKD", "IDR", "INR", "JPY", "KRW",
|
||||||
"MYR", "NZD", "PHP", "SGD", "THB", "TWD", "USD", "VND"}
|
"MYR", "NZD", "PHP", "SGD", "THB", "TWD", "USD", "VND"}
|
||||||
|
|
||||||
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ var (
|
|||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
},
|
},
|
||||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
container: duplicatechecker.Container,
|
container: duplicatechecker.Container,
|
||||||
},
|
},
|
||||||
categories: services.TransactionCategories,
|
categories: services.TransactionCategories,
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ var (
|
|||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
},
|
},
|
||||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
container: duplicatechecker.Container,
|
container: duplicatechecker.Container,
|
||||||
},
|
},
|
||||||
users: services.Users,
|
users: services.Users,
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ var (
|
|||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
},
|
},
|
||||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
container: duplicatechecker.Container,
|
container: duplicatechecker.Container,
|
||||||
},
|
},
|
||||||
templates: services.TransactionTemplates,
|
templates: services.TransactionTemplates,
|
||||||
@@ -156,7 +159,12 @@ func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.WebContext) (any
|
|||||||
}
|
}
|
||||||
|
|
||||||
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
serverUtcOffset := utils.GetServerTimezoneOffsetMinutes()
|
||||||
template := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1)
|
template, err := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transaction_templates.TemplateCreateHandler] failed to create new template for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" {
|
if a.CurrentConfig().EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" {
|
||||||
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId)
|
found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId)
|
||||||
@@ -260,6 +268,34 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any
|
|||||||
newTemplate.ScheduledFrequency = a.getOrderedFrequencyValues(*templateModifyReq.ScheduledFrequency)
|
newTemplate.ScheduledFrequency = a.getOrderedFrequencyValues(*templateModifyReq.ScheduledFrequency)
|
||||||
newTemplate.ScheduledAt = a.getUTCScheduledAt(*templateModifyReq.ScheduledTimezoneUtcOffset)
|
newTemplate.ScheduledAt = a.getUTCScheduledAt(*templateModifyReq.ScheduledTimezoneUtcOffset)
|
||||||
newTemplate.ScheduledTimezoneUtcOffset = *templateModifyReq.ScheduledTimezoneUtcOffset
|
newTemplate.ScheduledTimezoneUtcOffset = *templateModifyReq.ScheduledTimezoneUtcOffset
|
||||||
|
|
||||||
|
if templateModifyReq.ScheduledStartDate != nil {
|
||||||
|
startTime, err := utils.ParseFromLongDateFirstTime(*templateModifyReq.ScheduledStartDate, *templateModifyReq.ScheduledTimezoneUtcOffset)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transaction_templates.TemplateModifyHandler] failed to parse scheduled start date for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
startUnixTime := startTime.Unix()
|
||||||
|
newTemplate.ScheduledStartTime = &startUnixTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if templateModifyReq.ScheduledEndDate != nil {
|
||||||
|
endTime, err := utils.ParseFromLongDateLastTime(*templateModifyReq.ScheduledEndDate, *templateModifyReq.ScheduledTimezoneUtcOffset)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transaction_templates.TemplateModifyHandler] failed to parse scheduled end date for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
endUnixTime := endTime.Unix()
|
||||||
|
newTemplate.ScheduledEndTime = &endUnixTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if newTemplate.ScheduledStartTime != nil && newTemplate.ScheduledEndTime != nil && *newTemplate.ScheduledStartTime > *newTemplate.ScheduledEndTime {
|
||||||
|
return nil, errs.ErrScheduledTransactionTemplateStartDataLaterThanEndDate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if newTemplate.Name == template.Name &&
|
if newTemplate.Name == template.Name &&
|
||||||
@@ -277,6 +313,8 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any
|
|||||||
} else if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
} else if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE {
|
||||||
if newTemplate.ScheduledFrequencyType == template.ScheduledFrequencyType &&
|
if newTemplate.ScheduledFrequencyType == template.ScheduledFrequencyType &&
|
||||||
newTemplate.ScheduledFrequency == template.ScheduledFrequency &&
|
newTemplate.ScheduledFrequency == template.ScheduledFrequency &&
|
||||||
|
newTemplate.ScheduledStartTime == template.ScheduledStartTime &&
|
||||||
|
newTemplate.ScheduledEndTime == template.ScheduledEndTime &&
|
||||||
newTemplate.ScheduledAt == template.ScheduledAt &&
|
newTemplate.ScheduledAt == template.ScheduledAt &&
|
||||||
newTemplate.ScheduledTimezoneUtcOffset == template.ScheduledTimezoneUtcOffset {
|
newTemplate.ScheduledTimezoneUtcOffset == template.ScheduledTimezoneUtcOffset {
|
||||||
return nil, errs.ErrNothingWillBeUpdated
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
@@ -419,7 +457,7 @@ func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.WebContext) (any
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate {
|
func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) (*models.TransactionTemplate, error) {
|
||||||
template := &models.TransactionTemplate{
|
template := &models.TransactionTemplate{
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
TemplateType: templateCreateReq.TemplateType,
|
TemplateType: templateCreateReq.TemplateType,
|
||||||
@@ -441,9 +479,35 @@ func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCrea
|
|||||||
template.ScheduledFrequency = a.getOrderedFrequencyValues(*templateCreateReq.ScheduledFrequency)
|
template.ScheduledFrequency = a.getOrderedFrequencyValues(*templateCreateReq.ScheduledFrequency)
|
||||||
template.ScheduledAt = a.getUTCScheduledAt(*templateCreateReq.ScheduledTimezoneUtcOffset)
|
template.ScheduledAt = a.getUTCScheduledAt(*templateCreateReq.ScheduledTimezoneUtcOffset)
|
||||||
template.ScheduledTimezoneUtcOffset = *templateCreateReq.ScheduledTimezoneUtcOffset
|
template.ScheduledTimezoneUtcOffset = *templateCreateReq.ScheduledTimezoneUtcOffset
|
||||||
|
|
||||||
|
if templateCreateReq.ScheduledStartDate != nil {
|
||||||
|
startTime, err := utils.ParseFromLongDateFirstTime(*templateCreateReq.ScheduledStartDate, *templateCreateReq.ScheduledTimezoneUtcOffset)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
startUnixTime := startTime.Unix()
|
||||||
|
template.ScheduledStartTime = &startUnixTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if templateCreateReq.ScheduledEndDate != nil {
|
||||||
|
endTime, err := utils.ParseFromLongDateLastTime(*templateCreateReq.ScheduledEndDate, *templateCreateReq.ScheduledTimezoneUtcOffset)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
endUnixTime := endTime.Unix()
|
||||||
|
template.ScheduledEndTime = &endUnixTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.ScheduledStartTime != nil && template.ScheduledEndTime != nil && *template.ScheduledStartTime > *template.ScheduledEndTime {
|
||||||
|
return nil, errs.ErrScheduledTransactionTemplateStartDataLaterThanEndDate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return template
|
return template, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TransactionTemplatesApi) getUTCScheduledAt(scheduledTimezoneUtcOffset int16) int16 {
|
func (a *TransactionTemplatesApi) getUTCScheduledAt(scheduledTimezoneUtcOffset int16) int16 {
|
||||||
|
|||||||
+164
-3
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -8,6 +9,8 @@ import (
|
|||||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters"
|
"github.com/mayswind/ezbookkeeping/pkg/converters"
|
||||||
|
baseconverters "github.com/mayswind/ezbookkeeping/pkg/converters/base"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
@@ -40,6 +43,9 @@ var (
|
|||||||
container: settings.Container,
|
container: settings.Container,
|
||||||
},
|
},
|
||||||
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{
|
||||||
|
ApiUsingConfig: ApiUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
container: duplicatechecker.Container,
|
container: duplicatechecker.Container,
|
||||||
},
|
},
|
||||||
transactions: services.Transactions,
|
transactions: services.Transactions,
|
||||||
@@ -382,10 +388,10 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
|
|||||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
statisticTrendsResp := make(models.TransactionStatisticTrendsItemSlice, 0, len(allMonthlyTotalAmounts))
|
statisticTrendsResp := make(models.TransactionStatisticTrendsResponseItemSlice, 0, len(allMonthlyTotalAmounts))
|
||||||
|
|
||||||
for yearMonth, monthlyTotalAmounts := range allMonthlyTotalAmounts {
|
for yearMonth, monthlyTotalAmounts := range allMonthlyTotalAmounts {
|
||||||
monthlyStatisticResp := &models.TransactionStatisticTrendsItem{
|
monthlyStatisticResp := &models.TransactionStatisticTrendsResponseItem{
|
||||||
Year: yearMonth / 100,
|
Year: yearMonth / 100,
|
||||||
Month: yearMonth % 100,
|
Month: yearMonth % 100,
|
||||||
Items: make([]*models.TransactionStatisticResponseItem, len(monthlyTotalAmounts)),
|
Items: make([]*models.TransactionStatisticResponseItem, len(monthlyTotalAmounts)),
|
||||||
@@ -1030,6 +1036,83 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransactionParseImportDsvFileDataHandler returns the parsed file data by request parameters for current user
|
||||||
|
func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
form, err := c.MultipartForm()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.ErrParameterInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
fileTypes := form.Value["fileType"]
|
||||||
|
|
||||||
|
if len(fileTypes) < 1 || fileTypes[0] == "" {
|
||||||
|
return nil, errs.ErrImportFileTypeIsEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
fileType := fileTypes[0]
|
||||||
|
|
||||||
|
if !converters.IsCustomDelimiterSeparatedValuesFileType(fileType) {
|
||||||
|
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEncodings := form.Value["fileEncoding"]
|
||||||
|
|
||||||
|
if len(fileEncodings) < 1 || fileEncodings[0] == "" {
|
||||||
|
return nil, errs.ErrImportFileEncodingIsEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEncoding := fileEncodings[0]
|
||||||
|
dataParser, err := converters.CreateNewDelimiterSeparatedValuesDataParser(fileType, fileEncoding)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
importFiles := form.File["file"]
|
||||||
|
|
||||||
|
if len(importFiles) < 1 {
|
||||||
|
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] there is no import file in request for user \"uid:%d\"", uid)
|
||||||
|
return nil, errs.ErrNoFilesUpload
|
||||||
|
}
|
||||||
|
|
||||||
|
if importFiles[0].Size < 1 {
|
||||||
|
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] the size of import file in request is zero for user \"uid:%d\"", uid)
|
||||||
|
return nil, errs.ErrUploadedFileEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
if importFiles[0].Size > int64(a.CurrentConfig().MaxImportFileSize) {
|
||||||
|
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of import file for user \"uid:%d\"", importFiles[0].Size, a.CurrentConfig().MaxImportFileSize, uid)
|
||||||
|
return nil, errs.ErrExceedMaxUploadFileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
importFile, err := importFiles[0].Open()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
defer importFile.Close()
|
||||||
|
fileData, err := io.ReadAll(importFile)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
allLines, err := dataParser.ParseDsvFileLines(c, fileData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to parse import file data for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allLines, nil
|
||||||
|
}
|
||||||
|
|
||||||
// TransactionParseImportFileHandler returns the parsed transaction data by request parameters for current user
|
// TransactionParseImportFileHandler returns the parsed transaction data by request parameters for current user
|
||||||
func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext) (any, *errs.Error) {
|
func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext) (any, *errs.Error) {
|
||||||
uid := c.GetCurrentUid()
|
uid := c.GetCurrentUid()
|
||||||
@@ -1054,7 +1137,84 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileType := fileTypes[0]
|
fileType := fileTypes[0]
|
||||||
dataImporter, err := converters.GetTransactionDataImporter(fileType)
|
|
||||||
|
var dataImporter baseconverters.TransactionDataImporter
|
||||||
|
|
||||||
|
if converters.IsCustomDelimiterSeparatedValuesFileType(fileType) {
|
||||||
|
fileEncodings := form.Value["fileEncoding"]
|
||||||
|
|
||||||
|
if len(fileEncodings) < 1 || fileEncodings[0] == "" {
|
||||||
|
return nil, errs.ErrImportFileEncodingIsEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEncoding := fileEncodings[0]
|
||||||
|
|
||||||
|
columnMappings := form.Value["columnMapping"]
|
||||||
|
|
||||||
|
if len(columnMappings) < 1 || columnMappings[0] == "" {
|
||||||
|
return nil, errs.ErrImportFileColumnMappingInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
var columnIndexMapping = map[datatable.TransactionDataTableColumn]int{}
|
||||||
|
err = json.Unmarshal([]byte(columnMappings[0]), &columnIndexMapping)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse column mapping for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.ErrImportFileColumnMappingInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionTypeMappings := form.Value["transactionTypeMapping"]
|
||||||
|
|
||||||
|
if len(transactionTypeMappings) < 1 || transactionTypeMappings[0] == "" {
|
||||||
|
return nil, errs.ErrImportFileTransactionTypeMappingInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
var transactionTypeNameMapping = map[string]models.TransactionType{}
|
||||||
|
err = json.Unmarshal([]byte(transactionTypeMappings[0]), &transactionTypeNameMapping)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse transaction type mapping for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.ErrImportFileTransactionTypeMappingInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
hasHeaderLines := form.Value["hasHeaderLine"]
|
||||||
|
hasHeaderLine := false
|
||||||
|
|
||||||
|
if len(hasHeaderLines) > 0 {
|
||||||
|
hasHeaderLine = hasHeaderLines[0] == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
timeFormats := form.Value["timeFormat"]
|
||||||
|
|
||||||
|
if len(timeFormats) < 1 || timeFormats[0] == "" {
|
||||||
|
return nil, errs.ErrImportFileTransactionTimeFormatInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
timezoneFormats := form.Value["timezoneFormat"]
|
||||||
|
timezoneFormat := ""
|
||||||
|
|
||||||
|
if len(timezoneFormats) > 0 {
|
||||||
|
timezoneFormat = timezoneFormats[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
geoLocationSeparators := form.Value["geoSeparator"]
|
||||||
|
geoLocationSeparator := ""
|
||||||
|
|
||||||
|
if len(geoLocationSeparators) > 0 {
|
||||||
|
geoLocationSeparator = geoLocationSeparators[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionTagSeparators := form.Value["tagSeparator"]
|
||||||
|
transactionTagSeparator := ""
|
||||||
|
|
||||||
|
if len(transactionTagSeparators) > 0 {
|
||||||
|
transactionTagSeparator = transactionTagSeparators[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
dataImporter, err = converters.CreateNewDelimiterSeparatedValuesDataImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormats[0], timezoneFormat, geoLocationSeparator, transactionTagSeparator)
|
||||||
|
} else {
|
||||||
|
dataImporter, err = converters.GetTransactionDataImporter(fileType)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
|
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
|
||||||
@@ -1084,6 +1244,7 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
|
|||||||
return nil, errs.ErrOperationFailed
|
return nil, errs.ErrOperationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer importFile.Close()
|
||||||
fileData, err := io.ReadAll(importFile)
|
fileData, err := io.ReadAll(importFile)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
|
|||||||
geoLongitude := float64(0)
|
geoLongitude := float64(0)
|
||||||
geoLatitude := float64(0)
|
geoLatitude := float64(0)
|
||||||
|
|
||||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION) {
|
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION) && c.geoLocationSeparator != "" {
|
||||||
geoLocationItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), c.geoLocationSeparator)
|
geoLocationItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), c.geoLocationSeparator)
|
||||||
|
|
||||||
if len(geoLocationItems) == 2 {
|
if len(geoLocationItems) == 2 {
|
||||||
@@ -442,7 +442,13 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
|
|||||||
var tagNames []string
|
var tagNames []string
|
||||||
|
|
||||||
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_TAGS) {
|
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_TAGS) {
|
||||||
tagNameItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS), c.transactionTagSeparator)
|
var tagNameItems []string
|
||||||
|
|
||||||
|
if c.transactionTagSeparator != "" {
|
||||||
|
tagNameItems = strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS), c.transactionTagSeparator)
|
||||||
|
} else {
|
||||||
|
tagNameItems = append(tagNameItems, dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS))
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < len(tagNameItems); i++ {
|
for i := 0; i < len(tagNameItems); i++ {
|
||||||
tagName := tagNameItems[i]
|
tagName := tagNameItems[i]
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package dsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding"
|
||||||
|
"golang.org/x/text/encoding/charmap"
|
||||||
|
"golang.org/x/text/encoding/japanese"
|
||||||
|
"golang.org/x/text/encoding/korean"
|
||||||
|
"golang.org/x/text/encoding/simplifiedchinese"
|
||||||
|
"golang.org/x/text/encoding/traditionalchinese"
|
||||||
|
"golang.org/x/text/encoding/unicode"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/converters/base"
|
||||||
|
csvconverter "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var supportedFileTypeSeparators = map[string]rune{
|
||||||
|
"custom_csv": ',',
|
||||||
|
"custom_tsv": '\t',
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportedFileEncodings = map[string]encoding.Encoding{
|
||||||
|
"utf-8": unicode.UTF8, // UTF-8
|
||||||
|
"utf-8-bom": unicode.UTF8BOM, // UTF-8 with BOM
|
||||||
|
"utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), // UTF-16 Little Endian
|
||||||
|
"utf-16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), // UTF-16 Big Endian
|
||||||
|
"cp437": charmap.CodePage437, // OEM United States (CP-437)
|
||||||
|
"cp863": charmap.CodePage863, // OEM Canadian French (CP-863)
|
||||||
|
"cp037": charmap.CodePage037, // IBM EBCDIC US/Canada (CP-037)
|
||||||
|
"cp1047": charmap.CodePage1047, // IBM EBCDIC Open Systems (CP-1047)
|
||||||
|
"cp1140": charmap.CodePage1140, // IBM EBCDIC US/Canada with Euro (CP-1140)
|
||||||
|
"iso-8859-1": charmap.ISO8859_1, // Western European (ISO-8859-1)
|
||||||
|
"cp850": charmap.CodePage850, // Western European (CP-850)
|
||||||
|
"cp858": charmap.CodePage858, // Western European with Euro (CP-858)
|
||||||
|
"windows-1252": charmap.Windows1252, // Western European (Windows-1252)
|
||||||
|
"iso-8859-15": charmap.ISO8859_15, // Western European (ISO-8859-15)
|
||||||
|
"iso-8859-4": charmap.ISO8859_4, // North European (ISO-8859-4)
|
||||||
|
"iso-8859-10": charmap.ISO8859_10, // North European (ISO-8859-10)
|
||||||
|
"cp865": charmap.CodePage865, // North European (CP-865)
|
||||||
|
"iso-8859-2": charmap.ISO8859_2, // Central European (ISO-8859-2)
|
||||||
|
"cp852": charmap.CodePage852, // Central European (CP-852)
|
||||||
|
"windows-1250": charmap.Windows1250, // Central European (Windows-1250)
|
||||||
|
"iso-8859-14": charmap.ISO8859_14, // Celtic (ISO-8859-14)
|
||||||
|
"iso-8859-3": charmap.ISO8859_3, // South European (ISO-8859-3)
|
||||||
|
"cp860": charmap.CodePage860, // Portuguese (CP-860)
|
||||||
|
"iso-8859-7": charmap.ISO8859_7, // Greek (ISO-8859-7)
|
||||||
|
"windows-1253": charmap.Windows1253, // Greek (Windows-1253)
|
||||||
|
"iso-8859-9": charmap.ISO8859_9, // Turkish (ISO-8859-9)
|
||||||
|
"windows-1254": charmap.Windows1254, // Turkish (Windows-1254)
|
||||||
|
"iso-8859-13": charmap.ISO8859_13, // Baltic (ISO-8859-13)
|
||||||
|
"windows-1257": charmap.Windows1257, // Baltic (Windows-1257)
|
||||||
|
"iso-8859-16": charmap.ISO8859_16, // South-Eastern European (ISO-8859-16)
|
||||||
|
"iso-8859-5": charmap.ISO8859_5, // Cyrillic (ISO-8859-5)
|
||||||
|
"cp855": charmap.CodePage855, // Cyrillic (CP-855)
|
||||||
|
"cp866": charmap.CodePage866, // Cyrillic (CP-866)
|
||||||
|
"windows-1251": charmap.Windows1251, // Cyrillic (Windows-1251)
|
||||||
|
"koi8r": charmap.KOI8R, // Cyrillic (KOI8-R)
|
||||||
|
"koi8u": charmap.KOI8U, // Cyrillic (KOI8-U)
|
||||||
|
"iso-8859-6": charmap.ISO8859_6, // Arabic (ISO-8859-6)
|
||||||
|
"windows-1256": charmap.Windows1256, // Arabic (Windows-1256)
|
||||||
|
"iso-8859-8": charmap.ISO8859_8, // Hebrew (ISO-8859-8)
|
||||||
|
"cp862": charmap.CodePage862, // Hebrew (CP-862)
|
||||||
|
"windows-1255": charmap.Windows1255, // Hebrew (Windows-1255)
|
||||||
|
"windows-874": charmap.Windows874, // Thai (Windows-874)
|
||||||
|
"windows-1258": charmap.Windows1258, // Vietnamese (Windows-1258)
|
||||||
|
"gb18030": simplifiedchinese.GB18030, // Chinese (Simplified, GB18030)
|
||||||
|
"gbk": simplifiedchinese.GBK, // Chinese (Simplified, GBK)
|
||||||
|
"big5": traditionalchinese.Big5, // Chinese (Traditional, Big5)
|
||||||
|
"euc-kr": korean.EUCKR, // Korean (EUC-KR)
|
||||||
|
"euc-jp": japanese.EUCJP, // Japanese (EUC-JP)
|
||||||
|
"iso-2022-jp": japanese.ISO2022JP, // Japanese (ISO-2022-JP)
|
||||||
|
"shift_jis": japanese.ShiftJIS, // Japanese (Shift JIS)
|
||||||
|
}
|
||||||
|
|
||||||
|
var customTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||||
|
models.TRANSACTION_TYPE_MODIFY_BALANCE: utils.IntToString(int(models.TRANSACTION_TYPE_MODIFY_BALANCE)),
|
||||||
|
models.TRANSACTION_TYPE_INCOME: utils.IntToString(int(models.TRANSACTION_TYPE_INCOME)),
|
||||||
|
models.TRANSACTION_TYPE_EXPENSE: utils.IntToString(int(models.TRANSACTION_TYPE_EXPENSE)),
|
||||||
|
models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)),
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomTransactionDataDsvFileParser interface {
|
||||||
|
ParseDsvFileLines(ctx core.Context, data []byte) ([][]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// customTransactionDataDsvFileImporter defines the structure of custom dsv importer for transaction data
|
||||||
|
type customTransactionDataDsvFileImporter struct {
|
||||||
|
fileEncoding encoding.Encoding
|
||||||
|
separator rune
|
||||||
|
columnIndexMapping map[datatable.TransactionDataTableColumn]int
|
||||||
|
transactionTypeNameMapping map[string]models.TransactionType
|
||||||
|
hasHeaderLine bool
|
||||||
|
timeFormat string
|
||||||
|
timezoneFormat string
|
||||||
|
geoLocationSeparator string
|
||||||
|
transactionTagSeparator string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDsvFileLines returns the parsed file lines for specified the dsv file data
|
||||||
|
func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Context, data []byte) ([][]string, error) {
|
||||||
|
reader := transform.NewReader(bytes.NewReader(data), c.fileEncoding.NewDecoder())
|
||||||
|
csvReader := csv.NewReader(reader)
|
||||||
|
csvReader.Comma = c.separator
|
||||||
|
csvReader.FieldsPerRecord = -1
|
||||||
|
|
||||||
|
allLines := make([][]string, 0)
|
||||||
|
|
||||||
|
for {
|
||||||
|
items, err := csvReader.Read()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[custom_transaction_data_dsv_file_importer.ParseDsvFileLines] cannot parse dsv data, because %s", err.Error())
|
||||||
|
return nil, errs.ErrInvalidCSVFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) == 1 && items[0] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := range items {
|
||||||
|
items[index] = strings.Trim(items[index], " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
allLines = append(allLines, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allLines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseImportedData returns the imported data by parsing the custom transaction dsv data
|
||||||
|
func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||||
|
allLines, err := c.ParseDsvFileLines(ctx, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.hasHeaderLine {
|
||||||
|
allLines = append([][]string{{}}, allLines...)
|
||||||
|
}
|
||||||
|
|
||||||
|
dataTable := csvconverter.CreateNewCustomCsvImportedDataTable(allLines)
|
||||||
|
transactionDataTable := CreateNewCustomPlainTextDataTable(dataTable, c.columnIndexMapping, c.transactionTypeNameMapping, c.timeFormat, c.timezoneFormat)
|
||||||
|
dataTableImporter := datatable.CreateNewImporter(customTransactionTypeNameMapping, c.geoLocationSeparator, c.transactionTagSeparator)
|
||||||
|
|
||||||
|
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDelimiterSeparatedValuesFileType returns whether the file type is the delimiter-separated values file type
|
||||||
|
func IsDelimiterSeparatedValuesFileType(fileType string) bool {
|
||||||
|
_, exists := supportedFileTypeSeparators[fileType]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNewCustomTransactionDataDsvFileParser returns a new custom dsv parser for transaction data
|
||||||
|
func CreateNewCustomTransactionDataDsvFileParser(fileType string, fileEncoding string) (CustomTransactionDataDsvFileParser, error) {
|
||||||
|
separator, exists := supportedFileTypeSeparators[fileType]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, errs.ErrImportFileTypeNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
enc, exists := supportedFileEncodings[fileEncoding]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, errs.ErrImportFileEncodingNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
return &customTransactionDataDsvFileImporter{
|
||||||
|
fileEncoding: enc,
|
||||||
|
separator: separator,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNewCustomTransactionDataDsvFileImporter returns a new custom dsv importer for transaction data
|
||||||
|
func CreateNewCustomTransactionDataDsvFileImporter(fileType string, fileEncoding string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, geoLocationSeparator string, transactionTagSeparator string) (base.TransactionDataImporter, error) {
|
||||||
|
separator, exists := supportedFileTypeSeparators[fileType]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, errs.ErrImportFileTypeNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
enc, exists := supportedFileEncodings[fileEncoding]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, errs.ErrImportFileEncodingNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists = columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME]; !exists {
|
||||||
|
return nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists = columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE]; !exists {
|
||||||
|
return nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists = columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_AMOUNT]; !exists {
|
||||||
|
return nil, errs.ErrMissingRequiredFieldInHeaderRow
|
||||||
|
}
|
||||||
|
|
||||||
|
return &customTransactionDataDsvFileImporter{
|
||||||
|
fileEncoding: enc,
|
||||||
|
separator: separator,
|
||||||
|
columnIndexMapping: columnIndexMapping,
|
||||||
|
transactionTypeNameMapping: transactionTypeNameMapping,
|
||||||
|
hasHeaderLine: hasHeaderLine,
|
||||||
|
timeFormat: timeFormat,
|
||||||
|
timezoneFormat: timezoneFormat,
|
||||||
|
geoLocationSeparator: geoLocationSeparator,
|
||||||
|
transactionTagSeparator: transactionTagSeparator,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
|||||||
|
package dsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// customPlainTextDataTable defines the structure of custom plain text transaction data table
|
||||||
|
type customPlainTextDataTable struct {
|
||||||
|
innerDataTable datatable.ImportedDataTable
|
||||||
|
columnIndexMapping map[datatable.TransactionDataTableColumn]int
|
||||||
|
transactionTypeNameMapping map[string]models.TransactionType
|
||||||
|
timeFormat string
|
||||||
|
timezoneFormat string
|
||||||
|
timeFormatIncludeTimezone bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// customPlainTextDataRow defines the structure of custom plain text transaction data row
|
||||||
|
type customPlainTextDataRow struct {
|
||||||
|
transactionDataTable *customPlainTextDataTable
|
||||||
|
rowData map[datatable.TransactionDataTableColumn]string
|
||||||
|
isValid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// customPlainTextDataRowIterator defines the structure of custom plain text transaction data row iterator
|
||||||
|
type customPlainTextDataRowIterator struct {
|
||||||
|
transactionDataTable *customPlainTextDataTable
|
||||||
|
innerIterator datatable.ImportedDataRowIterator
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasColumn returns whether the data table has specified column
|
||||||
|
func (t *customPlainTextDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool {
|
||||||
|
// custom dsv file allows no sub category, account name and related account name column mapping, but data table converter needs these columns
|
||||||
|
if column == datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY ||
|
||||||
|
column == datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME ||
|
||||||
|
column == datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// timezone column will be added when original time format contains timezone
|
||||||
|
if t.timeFormatIncludeTimezone && column == datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, exists := t.columnIndexMapping[column]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionRowCount returns the total count of transaction data row
|
||||||
|
func (t *customPlainTextDataTable) TransactionRowCount() int {
|
||||||
|
return t.innerDataTable.DataRowCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionRowIterator returns the iterator of transaction data row
|
||||||
|
func (t *customPlainTextDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator {
|
||||||
|
return &customPlainTextDataRowIterator{
|
||||||
|
transactionDataTable: t,
|
||||||
|
innerIterator: t.innerDataTable.DataRowIterator(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether this row is valid data for importing
|
||||||
|
func (r *customPlainTextDataRow) IsValid() bool {
|
||||||
|
return r.isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetData returns the data in the specified column type
|
||||||
|
func (r *customPlainTextDataRow) GetData(column datatable.TransactionDataTableColumn) string {
|
||||||
|
return r.rowData[column]
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNext returns whether the iterator does not reach the end
|
||||||
|
func (t *customPlainTextDataRowIterator) HasNext() bool {
|
||||||
|
return t.innerIterator.HasNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next transaction data row
|
||||||
|
func (t *customPlainTextDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
|
||||||
|
importedRow := t.innerIterator.Next()
|
||||||
|
|
||||||
|
if importedRow == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData, isValid, err := t.parseTransaction(ctx, user, importedRow)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.Next] cannot parsing transaction in row \"%s\", because %s", t.innerIterator.CurrentRowId(), err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &customPlainTextDataRow{
|
||||||
|
transactionDataTable: t.transactionDataTable,
|
||||||
|
rowData: rowData,
|
||||||
|
isValid: isValid,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user *models.User, row datatable.ImportedDataRow) (map[datatable.TransactionDataTableColumn]string, bool, error) {
|
||||||
|
rowData := make(map[datatable.TransactionDataTableColumn]string, len(t.transactionDataTable.columnIndexMapping))
|
||||||
|
|
||||||
|
for column, columnIndex := range t.transactionDataTable.columnIndexMapping {
|
||||||
|
if columnIndex < 0 || columnIndex >= row.ColumnCount() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value := row.GetData(columnIndex)
|
||||||
|
rowData[column] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse transaction type
|
||||||
|
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] != "" {
|
||||||
|
transactionType, exists := t.transactionDataTable.transactionTypeNameMapping[rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE]]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
log.Warnf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] skip parsing this transaction, because transaction type \"%s\" mapping not defined", rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE])
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedTransactionType, exists := customTransactionTypeNameMapping[transactionType]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] cannot parsing transaction type \"%s\", because type \"%d\" is invalid", rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE], transactionType)
|
||||||
|
return nil, false, errs.ErrTransactionTypeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = mappedTransactionType
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse date time
|
||||||
|
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] != "" {
|
||||||
|
dateTime, err := time.Parse(t.transactionDataTable.timeFormat, rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, errs.ErrTransactionTimeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
|
||||||
|
|
||||||
|
if t.transactionDataTable.timeFormatIncludeTimezone {
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse timezone
|
||||||
|
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] != "" {
|
||||||
|
if t.transactionDataTable.timezoneFormat == "Z" || t.transactionDataTable.timezoneFormat == "" { // -HH:mm
|
||||||
|
// Do Nothing
|
||||||
|
} else if t.transactionDataTable.timezoneFormat == "ZZ" { // -HHmm
|
||||||
|
timezone := rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE]
|
||||||
|
|
||||||
|
if len(timezone) != 5 {
|
||||||
|
return nil, false, errs.ErrTransactionTimeZoneInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
timezone = timezone[:3] + ":" + timezone[3:]
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = timezone
|
||||||
|
} else {
|
||||||
|
return nil, false, errs.ErrImportFileTransactionTimezoneFormatInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use primary category if sub category is empty
|
||||||
|
if rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == "" && rowData[datatable.TRANSACTION_DATA_TABLE_CATEGORY] != "" {
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = rowData[datatable.TRANSACTION_DATA_TABLE_CATEGORY]
|
||||||
|
}
|
||||||
|
|
||||||
|
// trim trailing zero in decimal
|
||||||
|
if rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] != "" {
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
|
||||||
|
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] cannot parsing transaction amount \"%s\", because %s", rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT], err.Error())
|
||||||
|
return nil, false, errs.ErrAmountInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] != "" {
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
|
||||||
|
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] cannot parsing transaction related amount \"%s\", because %s", rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT], err.Error())
|
||||||
|
return nil, false, errs.ErrAmountInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY]; !exists {
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME]; !exists {
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME]; !exists {
|
||||||
|
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowData, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNewCustomPlainTextDataTable returns transaction data table from imported data table
|
||||||
|
func CreateNewCustomPlainTextDataTable(dataTable datatable.ImportedDataTable, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, timeFormat string, timezoneFormat string) *customPlainTextDataTable {
|
||||||
|
timeFormatIncludeTimezone := strings.Contains(timeFormat, "z") || strings.Contains(timeFormat, "Z")
|
||||||
|
|
||||||
|
return &customPlainTextDataTable{
|
||||||
|
innerDataTable: dataTable,
|
||||||
|
columnIndexMapping: columnIndexMapping,
|
||||||
|
transactionTypeNameMapping: transactionTypeNameMapping,
|
||||||
|
timeFormat: getDateTimeFormat(timeFormat),
|
||||||
|
timezoneFormat: timezoneFormat,
|
||||||
|
timeFormatIncludeTimezone: timeFormatIncludeTimezone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDateTimeFormat(format string) string {
|
||||||
|
// convert moment.js format to Go format
|
||||||
|
|
||||||
|
format = strings.ReplaceAll(format, "YYYY", "2006")
|
||||||
|
format = strings.ReplaceAll(format, "YY", "06")
|
||||||
|
|
||||||
|
format = strings.ReplaceAll(format, "MMMM", "January")
|
||||||
|
format = strings.ReplaceAll(format, "MMM", "Jan")
|
||||||
|
format = strings.ReplaceAll(format, "MM", "01")
|
||||||
|
format = strings.ReplaceAll(format, "M", "1")
|
||||||
|
|
||||||
|
format = strings.ReplaceAll(format, "DD", "02")
|
||||||
|
format = strings.ReplaceAll(format, "D", "2")
|
||||||
|
|
||||||
|
format = strings.ReplaceAll(format, "dddd", "Monday")
|
||||||
|
format = strings.ReplaceAll(format, "ddd", "Mon")
|
||||||
|
|
||||||
|
format = strings.ReplaceAll(format, "HH", "15")
|
||||||
|
format = strings.ReplaceAll(format, "H", "15")
|
||||||
|
|
||||||
|
format = strings.ReplaceAll(format, "hh", "03")
|
||||||
|
format = strings.ReplaceAll(format, "h", "3")
|
||||||
|
|
||||||
|
format = strings.ReplaceAll(format, "mm", "04")
|
||||||
|
format = strings.ReplaceAll(format, "m", "4")
|
||||||
|
|
||||||
|
format = strings.ReplaceAll(format, "ss", "05")
|
||||||
|
format = strings.ReplaceAll(format, "s", "5")
|
||||||
|
|
||||||
|
for i := 9; i >= 1; i-- {
|
||||||
|
format = strings.ReplaceAll(format, "."+strings.Repeat("S", i), "."+strings.Repeat("9", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
format = strings.ReplaceAll(format, "A", "PM")
|
||||||
|
format = strings.ReplaceAll(format, "a", "pm")
|
||||||
|
|
||||||
|
format = strings.ReplaceAll(format, "zz", "MST")
|
||||||
|
format = strings.ReplaceAll(format, "z", "MST")
|
||||||
|
|
||||||
|
if strings.Contains(format, "ZZ") {
|
||||||
|
format = strings.ReplaceAll(format, "ZZ", "Z0700")
|
||||||
|
} else if strings.Contains(format, "Z") {
|
||||||
|
format = strings.ReplaceAll(format, "Z", "Z07:00")
|
||||||
|
}
|
||||||
|
|
||||||
|
return format
|
||||||
|
}
|
||||||
+1
@@ -98,6 +98,7 @@ func (t *gnucashTransactionDataRowIterator) Next(ctx core.Context, user *models.
|
|||||||
rowItems, isValid, err := t.parseTransaction(ctx, user, data)
|
rowItems, isValid, err := t.parseTransaction(ctx, user, data)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[gnucash_transaction_table.Next] cannot parsing transaction in row#%d, because %s", t.currentIndex, err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,11 +129,26 @@ func (r *iifDataReader) read(ctx core.Context) ([]*iifAccountDataset, []*iifTran
|
|||||||
}
|
}
|
||||||
} else if lastLineSign == iifTransactionLineSignColumnName || lastLineSign == iifTransactionSplitLineSignColumnName {
|
} else if lastLineSign == iifTransactionLineSignColumnName || lastLineSign == iifTransactionSplitLineSignColumnName {
|
||||||
if items[0] == iifTransactionSplitLineSignColumnName {
|
if items[0] == iifTransactionSplitLineSignColumnName {
|
||||||
|
if currentTransactionData == nil {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] expected current transaction data is not nil, but read \"%s\"", items[0])
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
currentTransactionData.splitData = append(currentTransactionData.splitData, &iifTransactionSplitData{
|
currentTransactionData.splitData = append(currentTransactionData.splitData, &iifTransactionSplitData{
|
||||||
dataItems: items,
|
dataItems: items,
|
||||||
})
|
})
|
||||||
lastLineSign = items[0]
|
lastLineSign = items[0]
|
||||||
} else if items[0] == iifTransactionEndLineSignColumnName {
|
} else if items[0] == iifTransactionEndLineSignColumnName {
|
||||||
|
if currentTransactionData == nil {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] expected current transaction data is not nil, but read \"%s\"", items[0])
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(currentTransactionData.splitData) < 1 {
|
||||||
|
log.Errorf(ctx, "[iif_data_reader.read] expected reading transaction split line, but read \"%s\"", items[0])
|
||||||
|
return nil, nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
currentTransactionDataset.transactions = append(currentTransactionDataset.transactions, currentTransactionData)
|
currentTransactionDataset.transactions = append(currentTransactionDataset.transactions, currentTransactionData)
|
||||||
lastLineSign = ""
|
lastLineSign = ""
|
||||||
} else {
|
} else {
|
||||||
@@ -214,7 +229,7 @@ func (r *iifDataReader) readTransactionSampleLines(ctx core.Context, items []str
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(transactionEndSampleItems) < 1 || transactionEndSampleItems[0] != iifTransactionEndSampleLineSignColumnName {
|
if len(transactionEndSampleItems) < 1 || transactionEndSampleItems[0] != iifTransactionEndSampleLineSignColumnName {
|
||||||
log.Errorf(ctx, "[iif_data_reader.readTransactionSampleLines] expected reading transaction end sample line, but read \"%s\"", strings.Join(splitSampleItems, "\t"))
|
log.Errorf(ctx, "[iif_data_reader.readTransactionSampleLines] expected reading transaction end sample line, but read \"%s\"", strings.Join(transactionEndSampleItems, "\t"))
|
||||||
return nil, errs.ErrInvalidIIFFile
|
return nil, errs.ErrInvalidIIFFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseYearMonthDayFormatTime(t *
|
|||||||
assert.Equal(t, int64(1725408000), utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime))
|
assert.Equal(t, int64(1725408000), utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayFormatTime(t *testing.T) {
|
func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayYearFormatTime(t *testing.T) {
|
||||||
converter := IifTransactionDataFileImporter
|
converter := IifTransactionDataFileImporter
|
||||||
context := core.NewNullContext()
|
context := core.NewNullContext()
|
||||||
|
|
||||||
@@ -364,6 +364,37 @@ func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayFormatTime(t
|
|||||||
assert.Equal(t, int64(1725321600), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
assert.Equal(t, int64(1725321600), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_ParseShortMonthDayTwoDigitsYearFormatTime(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t9/01/24\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t9/01/24\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/2/24\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/2/24\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t9/3/24\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t9/3/24\tTest Account2\t-123.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 3, len(allNewTransactions))
|
||||||
|
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||||
|
assert.Equal(t, int64(1725235200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||||
|
assert.Equal(t, int64(1725321600), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||||
|
}
|
||||||
|
|
||||||
func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
|
func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T) {
|
||||||
converter := IifTransactionDataFileImporter
|
converter := IifTransactionDataFileImporter
|
||||||
context := core.NewNullContext()
|
context := core.NewNullContext()
|
||||||
@@ -377,8 +408,8 @@ func TestIIFTransactionDataFileParseImportedData_ParseInvalidTime(t *testing.T)
|
|||||||
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
"!ENDTRNS\t\t\t\n"+
|
"!ENDTRNS\t\t\t\n"+
|
||||||
"TRNS\t9/1/24\tTest Account\t123.45\n"+
|
"TRNS\t09-01-2024\tTest Account\t123.45\n"+
|
||||||
"SPL\t9/1/24\tTest Account2\t-123.45\n"+
|
"SPL\t09-01-2024\tTest Account2\t-123.45\n"+
|
||||||
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||||
|
|
||||||
@@ -486,7 +517,7 @@ func TestIIFTransactionDataFileParseImportedData_ParseDescription(t *testing.T)
|
|||||||
assert.Equal(t, "Test", allNewTransactions[0].Comment)
|
assert.Equal(t, "Test", allNewTransactions[0].Comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIIFTransactionDataFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) {
|
func TestIIFTransactionDataFileParseImportedData_ParseSplitTransaction(t *testing.T) {
|
||||||
converter := IifTransactionDataFileImporter
|
converter := IifTransactionDataFileImporter
|
||||||
context := core.NewNullContext()
|
context := core.NewNullContext()
|
||||||
|
|
||||||
@@ -495,11 +526,218 @@ func TestIIFTransactionDataFileParseImportedData_NotSupportedToParseSplitTransac
|
|||||||
DefaultCurrency: "CNY",
|
DefaultCurrency: "CNY",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!ACCNT\tNAME\tACCNTTYPE\n"+
|
||||||
|
"ACCNT\tTest Category\tINC\n"+
|
||||||
|
"ACCNT\tTest Category2\tEXP\n"+
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Category\t-23.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-100.00\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/02/2024\tTest Account\t-100.00\n"+
|
||||||
|
"SPL\t09/02/2024\tTest Category2\t30.00\n"+
|
||||||
|
"SPL\t09/02/2024\tTest Account3\t20.00\n"+
|
||||||
|
"SPL\t09/02/2024\tTest Account4\t50.00\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/03/2024\tTest Account\t100.00\n"+
|
||||||
|
"SPL\t09/03/2024\tTest Account2\t-100.00\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/04/2024\tTest Category\t-100.00\n"+
|
||||||
|
"SPL\t09/04/2024\tTest Account\t40.00\n"+
|
||||||
|
"SPL\t09/04/2024\tTest Account2\t60.00\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/05/2024\tTest Category2\t100.00\n"+
|
||||||
|
"SPL\t09/05/2024\tTest Account3\t-40.00\n"+
|
||||||
|
"SPL\t09/05/2024\tTest Account4\t-60.00\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 10, len(allNewTransactions))
|
||||||
|
assert.Equal(t, 4, len(allNewAccounts))
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type)
|
||||||
|
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||||
|
assert.Equal(t, int64(2345), allNewTransactions[0].Amount)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Category", allNewTransactions[0].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[1].Type)
|
||||||
|
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||||
|
assert.Equal(t, int64(10000), allNewTransactions[1].Amount)
|
||||||
|
assert.Equal(t, "Test Account2", allNewTransactions[1].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[1].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[2].Type)
|
||||||
|
assert.Equal(t, int64(1725235200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||||
|
assert.Equal(t, int64(3000), allNewTransactions[2].Amount)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Category2", allNewTransactions[2].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[3].Type)
|
||||||
|
assert.Equal(t, int64(1725235200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime))
|
||||||
|
assert.Equal(t, int64(2000), allNewTransactions[3].Amount)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[3].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Account3", allNewTransactions[3].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[3].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[4].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[4].Type)
|
||||||
|
assert.Equal(t, int64(1725235200), utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime))
|
||||||
|
assert.Equal(t, int64(5000), allNewTransactions[4].Amount)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[4].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Account4", allNewTransactions[4].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[4].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[5].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[5].Type)
|
||||||
|
assert.Equal(t, int64(1725321600), utils.GetUnixTimeFromTransactionTime(allNewTransactions[5].TransactionTime))
|
||||||
|
assert.Equal(t, int64(10000), allNewTransactions[5].Amount)
|
||||||
|
assert.Equal(t, "Test Account2", allNewTransactions[5].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[5].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[5].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[6].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[6].Type)
|
||||||
|
assert.Equal(t, int64(1725408000), utils.GetUnixTimeFromTransactionTime(allNewTransactions[6].TransactionTime))
|
||||||
|
assert.Equal(t, int64(4000), allNewTransactions[6].Amount)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[6].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Category", allNewTransactions[6].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[7].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[7].Type)
|
||||||
|
assert.Equal(t, int64(1725408000), utils.GetUnixTimeFromTransactionTime(allNewTransactions[7].TransactionTime))
|
||||||
|
assert.Equal(t, int64(6000), allNewTransactions[7].Amount)
|
||||||
|
assert.Equal(t, "Test Account2", allNewTransactions[7].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Category", allNewTransactions[7].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[8].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[8].Type)
|
||||||
|
assert.Equal(t, int64(1725494400), utils.GetUnixTimeFromTransactionTime(allNewTransactions[8].TransactionTime))
|
||||||
|
assert.Equal(t, int64(4000), allNewTransactions[8].Amount)
|
||||||
|
assert.Equal(t, "Test Account3", allNewTransactions[8].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Category2", allNewTransactions[8].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[9].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[9].Type)
|
||||||
|
assert.Equal(t, int64(1725494400), utils.GetUnixTimeFromTransactionTime(allNewTransactions[9].TransactionTime))
|
||||||
|
assert.Equal(t, int64(6000), allNewTransactions[9].Amount)
|
||||||
|
assert.Equal(t, "Test Account4", allNewTransactions[9].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Category2", allNewTransactions[9].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||||
|
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||||
|
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||||
|
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
|
||||||
|
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewAccounts[2].Uid)
|
||||||
|
assert.Equal(t, "Test Account3", allNewAccounts[2].Name)
|
||||||
|
assert.Equal(t, "CNY", allNewAccounts[2].Currency)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewAccounts[3].Uid)
|
||||||
|
assert.Equal(t, "Test Account4", allNewAccounts[3].Name)
|
||||||
|
assert.Equal(t, "CNY", allNewAccounts[3].Currency)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_ParseSplitTransactionDescription(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
|
||||||
|
"!ENDTRNS\t\t\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t\"Test\"\t123.45\t\"foo bar\t#test\"\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t\t-100.00\t\"foo\ttest#bar\"\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account3\t\t-23.45\t\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 2, len(allNewTransactions))
|
||||||
|
assert.Equal(t, "foo\ttest#bar", allNewTransactions[0].Comment)
|
||||||
|
assert.Equal(t, "foo bar\t#test", allNewTransactions[1].Comment)
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tNAME\tAMOUNT\tMEMO\n"+
|
||||||
|
"!ENDTRNS\t\t\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\tTest\t123.45\t\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t\t-100.00\t\"test\"\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account3\tfoo\t-12.34\t\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account4\t\t-11.11\t\n"+
|
||||||
|
"ENDTRNS\t\t\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 3, len(allNewTransactions))
|
||||||
|
assert.Equal(t, "test", allNewTransactions[0].Comment)
|
||||||
|
assert.Equal(t, "foo", allNewTransactions[1].Comment)
|
||||||
|
assert.Equal(t, "Test", allNewTransactions[2].Comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIIFTransactionDataFileParseImportedData_NotSupportedSplitTransaction(t *testing.T) {
|
||||||
|
converter := IifTransactionDataFileImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opening balance transaction
|
||||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tTRNSTYPE\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\t\n"+
|
||||||
|
"TRNS\tBEGINBALCHECK\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\tBEGINBALCHECK\t09/01/2024\tTest Account2\t-100.00\n"+
|
||||||
|
"SPL\tBEGINBALCHECK\t09/01/2024\tTest Account3\t-23.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
|
||||||
|
|
||||||
|
// Transaction with invalid amount
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123 45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-100.00\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account3\t-23.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||||
|
|
||||||
|
// Transaction split data with invalid amount
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
"!ENDTRNS\t\t\t\n"+
|
"!ENDTRNS\t\t\t\n"+
|
||||||
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
"TRNS\t09/01/2024\tTest Account\t123.45\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account2\t-100 00\n"+
|
||||||
|
"SPL\t09/01/2024\tTest Account3\t-23.45\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||||
|
|
||||||
|
// Transaction amount not equal to sum of split data amount
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"TRNS\t09/01/2024\tTest Account\t123.00\n"+
|
||||||
"SPL\t09/01/2024\tTest Account2\t-100.00\n"+
|
"SPL\t09/01/2024\tTest Account2\t-100.00\n"+
|
||||||
"SPL\t09/01/2024\tTest Account3\t-23.45\n"+
|
"SPL\t09/01/2024\tTest Account3\t-23.45\n"+
|
||||||
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
@@ -515,7 +753,7 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T)
|
|||||||
DefaultCurrency: "CNY",
|
DefaultCurrency: "CNY",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Missing Transaction Line
|
//Missing Transaction Line
|
||||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
@@ -524,6 +762,14 @@ func TestIIFTransactionDataFileParseImportedData_InvalidDataLines(t *testing.T)
|
|||||||
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
|
// Missing Transaction And Split Line
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!SPL\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
"!ENDTRNS\t\t\t\n"+
|
||||||
|
"ENDTRNS\t\t\t\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidIIFFile.Message)
|
||||||
|
|
||||||
// Missing Split Line
|
// Missing Split Line
|
||||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
"!TRNS\tDATE\tACCNT\tAMOUNT\n"+
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package iif
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
@@ -59,6 +60,7 @@ type iifTransactionDataRowIterator struct {
|
|||||||
dataTable *iifTransactionDataTable
|
dataTable *iifTransactionDataTable
|
||||||
currentDatasetIndex int
|
currentDatasetIndex int
|
||||||
currentIndexInDataset int
|
currentIndexInDataset int
|
||||||
|
currentSplitDataIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasColumn returns whether the transaction data table has specified column
|
// HasColumn returns whether the transaction data table has specified column
|
||||||
@@ -72,8 +74,15 @@ func (t *iifTransactionDataTable) TransactionRowCount() int {
|
|||||||
totalDataRowCount := 0
|
totalDataRowCount := 0
|
||||||
|
|
||||||
for i := 0; i < len(t.transactionDatasets); i++ {
|
for i := 0; i < len(t.transactionDatasets); i++ {
|
||||||
transactions := t.transactionDatasets[i]
|
datasets := t.transactionDatasets[i]
|
||||||
totalDataRowCount += len(transactions.transactions)
|
|
||||||
|
for j := 0; j < len(datasets.transactions); j++ {
|
||||||
|
transaction := datasets.transactions[j]
|
||||||
|
|
||||||
|
if transaction.splitData != nil {
|
||||||
|
totalDataRowCount += len(transaction.splitData)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalDataRowCount
|
return totalDataRowCount
|
||||||
@@ -84,7 +93,8 @@ func (t *iifTransactionDataTable) TransactionRowIterator() datatable.Transaction
|
|||||||
return &iifTransactionDataRowIterator{
|
return &iifTransactionDataRowIterator{
|
||||||
dataTable: t,
|
dataTable: t,
|
||||||
currentDatasetIndex: 0,
|
currentDatasetIndex: 0,
|
||||||
currentIndexInDataset: -1,
|
currentIndexInDataset: 0,
|
||||||
|
currentSplitDataIndex: -1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +126,9 @@ func (t *iifTransactionDataRowIterator) HasNext() bool {
|
|||||||
|
|
||||||
if t.currentIndexInDataset+1 < len(currentDataset.transactions) {
|
if t.currentIndexInDataset+1 < len(currentDataset.transactions) {
|
||||||
return true
|
return true
|
||||||
|
} else if t.currentIndexInDataset < len(currentDataset.transactions) &&
|
||||||
|
t.currentSplitDataIndex+1 < len(currentDataset.transactions[t.currentIndexInDataset].splitData) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := t.currentDatasetIndex + 1; i < len(allDatasets); i++ {
|
for i := t.currentDatasetIndex + 1; i < len(allDatasets); i++ {
|
||||||
@@ -134,20 +147,29 @@ func (t *iifTransactionDataRowIterator) HasNext() bool {
|
|||||||
// Next returns the next imported data row
|
// Next returns the next imported data row
|
||||||
func (t *iifTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
|
func (t *iifTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
|
||||||
allDatasets := t.dataTable.transactionDatasets
|
allDatasets := t.dataTable.transactionDatasets
|
||||||
currentIndexInDataset := t.currentIndexInDataset
|
|
||||||
|
|
||||||
for i := t.currentDatasetIndex; i < len(allDatasets); i++ {
|
for i := t.currentDatasetIndex; i < len(allDatasets); i++ {
|
||||||
|
foundNextRow := false
|
||||||
dataset := allDatasets[i]
|
dataset := allDatasets[i]
|
||||||
|
|
||||||
if currentIndexInDataset+1 < len(dataset.transactions) {
|
for j := t.currentIndexInDataset; j < len(dataset.transactions); j++ {
|
||||||
|
if t.currentSplitDataIndex+1 < len(dataset.transactions[j].splitData) {
|
||||||
|
t.currentSplitDataIndex++
|
||||||
|
foundNextRow = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
t.currentIndexInDataset++
|
t.currentIndexInDataset++
|
||||||
currentIndexInDataset = t.currentIndexInDataset
|
t.currentSplitDataIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if foundNextRow {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
t.currentDatasetIndex++
|
t.currentDatasetIndex++
|
||||||
t.currentIndexInDataset = -1
|
t.currentIndexInDataset = 0
|
||||||
currentIndexInDataset = -1
|
t.currentSplitDataIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.currentDatasetIndex >= len(allDatasets) {
|
if t.currentDatasetIndex >= len(allDatasets) {
|
||||||
@@ -161,9 +183,28 @@ func (t *iifTransactionDataRowIterator) Next(ctx core.Context, user *models.User
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := currentDataset.transactions[t.currentIndexInDataset]
|
data := currentDataset.transactions[t.currentIndexInDataset]
|
||||||
rowItems, err := t.parseTransaction(ctx, user, currentDataset, data)
|
|
||||||
|
if len(data.splitData) < 1 {
|
||||||
|
log.Errorf(ctx, "[iif_transaction_data_table.Next] cannot parsing transaction in row#%d (dataset#%d), because split data is empty", t.currentIndexInDataset, t.currentDatasetIndex)
|
||||||
|
return nil, errs.ErrInvalidIIFFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.currentSplitDataIndex >= len(data.splitData) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.splitData) > 1 {
|
||||||
|
_, err := t.isSplitTransactionSupported(ctx, currentDataset, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rowItems, err := t.parseTransaction(ctx, user, currentDataset, data, t.currentSplitDataIndex)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[iif_transaction_data_table.Next] cannot parsing transaction in row#%d-split#%d (dataset#%d), because %s", t.currentIndexInDataset, t.currentSplitDataIndex, t.currentDatasetIndex, err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,13 +214,7 @@ func (t *iifTransactionDataRowIterator) Next(ctx core.Context, user *models.User
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user *models.User, dataset *iifTransactionDataset, transactionData *iifTransactionData) (map[datatable.TransactionDataTableColumn]string, error) {
|
func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user *models.User, dataset *iifTransactionDataset, transactionData *iifTransactionData, splitDataIndex int) (map[datatable.TransactionDataTableColumn]string, error) {
|
||||||
if len(transactionData.splitData) < 1 {
|
|
||||||
return nil, errs.ErrInvalidIIFFile
|
|
||||||
} else if len(transactionData.splitData) > 1 {
|
|
||||||
return nil, errs.ErrNotSupportedSplitTransactions
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
data := make(map[datatable.TransactionDataTableColumn]string, len(iifTransactionSupportedColumns))
|
data := make(map[datatable.TransactionDataTableColumn]string, len(iifTransactionSupportedColumns))
|
||||||
@@ -189,18 +224,18 @@ func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionType, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionTypeColumnName)
|
transactionType, _ := dataset.getSplitDataItemValue(transactionData.splitData[splitDataIndex], iifTransactionTypeColumnName)
|
||||||
accountName1, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAccountNameColumnName)
|
mainAccountName, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAccountNameColumnName)
|
||||||
accountName2, _ := dataset.getSplitDataItemValue(transactionData.splitData[0], iifTransactionAccountNameColumnName)
|
splitAccountName, _ := dataset.getSplitDataItemValue(transactionData.splitData[splitDataIndex], iifTransactionAccountNameColumnName)
|
||||||
amount1, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAmountColumnName)
|
mainAmount, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAmountColumnName)
|
||||||
amount2, _ := dataset.getSplitDataItemValue(transactionData.splitData[0], iifTransactionAmountColumnName)
|
splitAmount, _ := dataset.getSplitDataItemValue(transactionData.splitData[splitDataIndex], iifTransactionAmountColumnName)
|
||||||
amountNum1, err := utils.ParseAmount(strings.ReplaceAll(amount1, ",", ""))
|
mainAmountNum, err := parseAmount(mainAmount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.ErrAmountInvalid
|
return nil, errs.ErrAmountInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
amountNum2, err := utils.ParseAmount(strings.ReplaceAll(amount2, ",", ""))
|
splitAmountNum, err := parseAmount(splitAmount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.ErrAmountInvalid
|
return nil, errs.ErrAmountInvalid
|
||||||
@@ -208,24 +243,35 @@ func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user
|
|||||||
|
|
||||||
if transactionType == iifTransactionTypeBeginningBalance { // balance modification
|
if transactionType == iifTransactionTypeBeginningBalance { // balance modification
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName1
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = mainAccountName
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum1)
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(mainAmountNum)
|
||||||
} else if t.dataTable.incomeAccountNames[accountName1] || t.dataTable.incomeAccountNames[accountName2] { // income
|
} else if (t.dataTable.incomeAccountNames[mainAccountName] && !t.dataTable.incomeAccountNames[splitAccountName] && !t.dataTable.expenseAccountNames[splitAccountName]) ||
|
||||||
|
(t.dataTable.incomeAccountNames[splitAccountName] && !t.dataTable.incomeAccountNames[mainAccountName] && !t.dataTable.expenseAccountNames[mainAccountName]) { // income
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
|
||||||
categoryName := ""
|
categoryName := ""
|
||||||
accountName := ""
|
accountName := ""
|
||||||
amountNum := int64(0)
|
amountNum := int64(0)
|
||||||
|
|
||||||
if t.dataTable.incomeAccountNames[accountName1] && !t.dataTable.incomeAccountNames[accountName2] {
|
if t.dataTable.incomeAccountNames[mainAccountName] && !t.dataTable.incomeAccountNames[splitAccountName] {
|
||||||
categoryName = accountName1
|
categoryName = mainAccountName
|
||||||
accountName = accountName2
|
accountName = splitAccountName
|
||||||
amountNum = amountNum2
|
|
||||||
} else if t.dataTable.incomeAccountNames[accountName2] && !t.dataTable.incomeAccountNames[accountName1] {
|
if len(transactionData.splitData) > 1 {
|
||||||
categoryName = accountName2
|
amountNum = splitAmountNum
|
||||||
accountName = accountName1
|
} else {
|
||||||
amountNum = amountNum1
|
amountNum = -mainAmountNum
|
||||||
|
}
|
||||||
|
} else if t.dataTable.incomeAccountNames[splitAccountName] && !t.dataTable.incomeAccountNames[mainAccountName] {
|
||||||
|
categoryName = splitAccountName
|
||||||
|
accountName = mainAccountName
|
||||||
|
|
||||||
|
if len(transactionData.splitData) > 1 {
|
||||||
|
amountNum = -splitAmountNum
|
||||||
|
} else {
|
||||||
|
amountNum = mainAmountNum
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Errorf(ctx, "[iif_transaction_data_table.parseTransaction] cannot parse transaction, because two accounts \"%s\" and \"%s\" are all income account", accountName1, accountName2)
|
log.Errorf(ctx, "[iif_transaction_data_table.parseTransaction] cannot parse transaction, because main account \"%s\" and split account \"%s\" are all income account", mainAccountName, splitAccountName)
|
||||||
return nil, errs.ErrInvalidIIFFile
|
return nil, errs.ErrInvalidIIFFile
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,22 +286,33 @@ func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user
|
|||||||
|
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum)
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum)
|
||||||
} else if t.dataTable.expenseAccountNames[accountName1] || t.dataTable.expenseAccountNames[accountName2] { // expense
|
} else if (t.dataTable.expenseAccountNames[mainAccountName] && !t.dataTable.expenseAccountNames[splitAccountName] && !t.dataTable.incomeAccountNames[splitAccountName]) ||
|
||||||
|
(t.dataTable.expenseAccountNames[splitAccountName] && !t.dataTable.expenseAccountNames[mainAccountName] && !t.dataTable.incomeAccountNames[mainAccountName]) { // expense
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
|
||||||
categoryName := ""
|
categoryName := ""
|
||||||
accountName := ""
|
accountName := ""
|
||||||
amountNum := int64(0)
|
amountNum := int64(0)
|
||||||
|
|
||||||
if t.dataTable.expenseAccountNames[accountName1] && !t.dataTable.expenseAccountNames[accountName2] {
|
if t.dataTable.expenseAccountNames[mainAccountName] && !t.dataTable.expenseAccountNames[splitAccountName] {
|
||||||
categoryName = accountName1
|
categoryName = mainAccountName
|
||||||
accountName = accountName2
|
accountName = splitAccountName
|
||||||
amountNum = amountNum2
|
|
||||||
} else if t.dataTable.expenseAccountNames[accountName2] && !t.dataTable.expenseAccountNames[accountName1] {
|
if len(transactionData.splitData) > 1 {
|
||||||
categoryName = accountName2
|
amountNum = -splitAmountNum
|
||||||
accountName = accountName1
|
} else {
|
||||||
amountNum = amountNum1
|
amountNum = mainAmountNum
|
||||||
|
}
|
||||||
|
} else if t.dataTable.expenseAccountNames[splitAccountName] && !t.dataTable.expenseAccountNames[mainAccountName] {
|
||||||
|
categoryName = splitAccountName
|
||||||
|
accountName = mainAccountName
|
||||||
|
|
||||||
|
if len(transactionData.splitData) > 1 {
|
||||||
|
amountNum = splitAmountNum
|
||||||
|
} else {
|
||||||
|
amountNum = -mainAmountNum
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Errorf(ctx, "[iif_transaction_data_table.parseTransaction] cannot parse transaction, because two accounts \"%s\" and \"%s\" are all expense account", accountName1, accountName2)
|
log.Errorf(ctx, "[iif_transaction_data_table.parseTransaction] cannot parse transaction, because main account \"%s\" and split account \"%s\" are all expense account", mainAccountName, splitAccountName)
|
||||||
return nil, errs.ErrInvalidIIFFile
|
return nil, errs.ErrInvalidIIFFile
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,26 +326,57 @@ func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user
|
|||||||
}
|
}
|
||||||
|
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum)
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum)
|
||||||
} else {
|
} else {
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = iifTransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
|
||||||
|
amountNum := int64(0)
|
||||||
|
relatedAmountNum := int64(0)
|
||||||
|
mainAccountTransferToSplitAccount := false
|
||||||
|
|
||||||
if amountNum1 >= 0 {
|
if len(transactionData.splitData) > 1 {
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName2
|
amountNum = splitAmountNum
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum2)
|
relatedAmountNum = splitAmountNum
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = accountName1
|
mainAccountTransferToSplitAccount = amountNum >= 0
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(amountNum1)
|
} else {
|
||||||
} else if amountNum2 >= 0 {
|
if mainAmountNum >= 0 {
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = accountName1
|
amountNum = splitAmountNum
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum1)
|
relatedAmountNum = mainAmountNum
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = accountName2
|
mainAccountTransferToSplitAccount = false
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(amountNum2)
|
} else if splitAmountNum >= 0 {
|
||||||
|
amountNum = mainAmountNum
|
||||||
|
relatedAmountNum = splitAmountNum
|
||||||
|
mainAccountTransferToSplitAccount = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mainAccountTransferToSplitAccount {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = mainAccountName
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = splitAccountName
|
||||||
|
} else {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = splitAccountName
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = mainAccountName
|
||||||
|
}
|
||||||
|
|
||||||
|
if amountNum >= 0 {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amountNum)
|
||||||
|
} else {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amountNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
if relatedAmountNum >= 0 {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(relatedAmountNum)
|
||||||
|
} else {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(-relatedAmountNum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if memo, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionMemoColumnName); memo != "" {
|
if splitMemo, _ := dataset.getSplitDataItemValue(transactionData.splitData[splitDataIndex], iifTransactionMemoColumnName); splitMemo != "" {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = splitMemo
|
||||||
|
} else if memo, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionMemoColumnName); memo != "" {
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = memo
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = memo
|
||||||
|
} else if splitName, _ := dataset.getSplitDataItemValue(transactionData.splitData[splitDataIndex], iifTransactionNameColumnName); splitName != "" {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = splitName
|
||||||
} else if name, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionNameColumnName); name != "" {
|
} else if name, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionNameColumnName); name != "" {
|
||||||
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = name
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = name
|
||||||
} else {
|
} else {
|
||||||
@@ -298,6 +386,49 @@ func (t *iifTransactionDataRowIterator) parseTransaction(ctx core.Context, user
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *iifTransactionDataRowIterator) isSplitTransactionSupported(ctx core.Context, dataset *iifTransactionDataset, transactionData *iifTransactionData) (bool, error) {
|
||||||
|
supportSplitTransactions := true
|
||||||
|
transactionType, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionTypeColumnName)
|
||||||
|
|
||||||
|
if transactionType == iifTransactionTypeBeginningBalance { // balance modification
|
||||||
|
supportSplitTransactions = false
|
||||||
|
log.Errorf(ctx, "[iif_transaction_data_table.isSplitTransactionSupported] cannot parse split balance modification transaction#%d (dataset#%d)", t.currentIndexInDataset, t.currentDatasetIndex)
|
||||||
|
} else {
|
||||||
|
transactionAmountStr, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionAmountColumnName)
|
||||||
|
transactionAmount, err := parseAmount(transactionAmountStr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[iif_transaction_data_table.isSplitTransactionSupported] cannot parsing transaction in row#%d (dataset#%d), because transaction amount \"%s\" is invalid", t.currentIndexInDataset, t.currentDatasetIndex, transactionAmountStr)
|
||||||
|
return false, errs.ErrAmountInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
splitTotalAmount := int64(0)
|
||||||
|
|
||||||
|
for i := 0; i < len(transactionData.splitData); i++ {
|
||||||
|
splitAmountStr, _ := dataset.getSplitDataItemValue(transactionData.splitData[i], iifTransactionAmountColumnName)
|
||||||
|
splitAmount, err := parseAmount(splitAmountStr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[iif_transaction_data_table.isSplitTransactionSupported] cannot parsing transaction in row#%d-split#%d (dataset#%d), because split amount \"%s\" is invalid", t.currentIndexInDataset, i, t.currentDatasetIndex, splitAmountStr)
|
||||||
|
return false, errs.ErrAmountInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
splitTotalAmount += splitAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
if splitTotalAmount != -transactionAmount {
|
||||||
|
supportSplitTransactions = false
|
||||||
|
log.Errorf(ctx, "[iif_transaction_data_table.isSplitTransactionSupported] cannot parse split transaction#%d (dataset#%d), because the sum amount of each split data \"%d\" not equal to the transaction amount \"%d\"", t.currentIndexInDataset, t.currentDatasetIndex, splitTotalAmount, -transactionAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(transactionData.splitData) > 1 && !supportSplitTransactions {
|
||||||
|
return false, errs.ErrNotSupportedSplitTransactions
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t *iifTransactionDataRowIterator) parseTransactionTime(dataset *iifTransactionDataset, transactionData *iifTransactionData) (string, error) {
|
func (t *iifTransactionDataRowIterator) parseTransactionTime(dataset *iifTransactionDataset, transactionData *iifTransactionData) (string, error) {
|
||||||
date, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionDateColumnName)
|
date, _ := dataset.getTransactionDataItemValue(transactionData, iifTransactionDateColumnName)
|
||||||
dateParts := strings.Split(date, "/")
|
dateParts := strings.Split(date, "/")
|
||||||
@@ -316,6 +447,10 @@ func (t *iifTransactionDataRowIterator) parseTransactionTime(dataset *iifTransac
|
|||||||
day = dateParts[2]
|
day = dateParts[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(year) == 2 {
|
||||||
|
year = utils.IntToString(time.Now().Year()/100) + year
|
||||||
|
}
|
||||||
|
|
||||||
if len(month) < 2 {
|
if len(month) < 2 {
|
||||||
month = "0" + month
|
month = "0" + month
|
||||||
}
|
}
|
||||||
@@ -390,3 +525,7 @@ func getIncomeAndExpenseAccountNameMap(accountDatasets []*iifAccountDataset) (in
|
|||||||
|
|
||||||
return incomeAccountNames, expenseAccountNames
|
return incomeAccountNames, expenseAccountNames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAmount(amount string) (int64, error) {
|
||||||
|
return utils.ParseAmount(strings.ReplaceAll(amount, ",", ""))
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ofxUnicodeEncoding = "unicode"
|
const ofx1USAsciiEncoding = "usascii"
|
||||||
const ofxUSAsciiEncoding = "usascii"
|
const ofx1UnicodeEncoding = "unicode"
|
||||||
|
const ofx1UTF8Encoding = "utf8" // non-standard ofx 1.x encoding, used by some banks (https://github.com/mayswind/ezbookkeeping/issues/48)
|
||||||
const ofx1SGMLDataFormat = "OFXSGML"
|
const ofx1SGMLDataFormat = "OFXSGML"
|
||||||
|
|
||||||
var ofx2HeaderPattern = regexp.MustCompile("<\\?OFX( +[A-Z]+=\"[^=]*\")* *\\?>")
|
var ofx2HeaderPattern = regexp.MustCompile("<\\?OFX( +[A-Z]+=\"[^=]*\")* *\\?>")
|
||||||
@@ -231,7 +232,7 @@ func readOFX1FileHeader(ctx core.Context, data []byte) (fileHeader *ofxFileHeade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileEncoding == ofxUSAsciiEncoding {
|
if fileEncoding == ofx1USAsciiEncoding {
|
||||||
if utils.IsStringOnlyContainsDigits(fileCharset) {
|
if utils.IsStringOnlyContainsDigits(fileCharset) {
|
||||||
fileCharset = "cp" + fileCharset
|
fileCharset = "cp" + fileCharset
|
||||||
}
|
}
|
||||||
@@ -245,12 +246,18 @@ func readOFX1FileHeader(ctx core.Context, data []byte) (fileHeader *ofxFileHeade
|
|||||||
if enc == nil {
|
if enc == nil {
|
||||||
enc = charmap.Windows1252
|
enc = charmap.Windows1252
|
||||||
}
|
}
|
||||||
} else if fileEncoding == ofxUnicodeEncoding {
|
} else if fileEncoding == ofx1UnicodeEncoding {
|
||||||
enc, _ = charset.Lookup(ofxUnicodeEncoding)
|
enc, _ = charset.Lookup(ofx1UnicodeEncoding)
|
||||||
|
|
||||||
if enc == nil {
|
if enc == nil {
|
||||||
enc = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
|
enc = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
|
||||||
}
|
}
|
||||||
|
} else if fileEncoding == ofx1UTF8Encoding {
|
||||||
|
enc, _ = charset.Lookup(ofx1UTF8Encoding)
|
||||||
|
|
||||||
|
if enc == nil {
|
||||||
|
enc = unicode.UTF8
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Errorf(ctx, "[ofx_data_reader.readOFX1FileHeader] cannot parse ofx 1.x file, because encoding \"%s\" is unknown", fileEncoding)
|
log.Errorf(ctx, "[ofx_data_reader.readOFX1FileHeader] cannot parse ofx 1.x file, because encoding \"%s\" is unknown", fileEncoding)
|
||||||
return nil, nil, "", nil, errs.ErrInvalidOFXFile
|
return nil, nil, "", nil, errs.ErrInvalidOFXFile
|
||||||
|
|||||||
+3
-1
@@ -105,6 +105,7 @@ func (t *ofxTransactionDataRowIterator) Next(ctx core.Context, user *models.User
|
|||||||
rowItems, err := t.parseTransaction(ctx, user, data)
|
rowItems, err := t.parseTransaction(ctx, user, data)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[ofx_transaction_table.Next] cannot parsing transaction in row#%d, because %s", t.currentIndex, err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,9 +152,10 @@ func (t *ofxTransactionDataRowIterator) parseTransaction(ctx core.Context, user
|
|||||||
return nil, errs.ErrAmountInvalid
|
return nil, errs.ErrAmountInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
amount, err := utils.ParseAmount(strings.ReplaceAll(ofxTransaction.Amount, ",", ".")) // ofx supports decimal point or comma to indicate the start of the fractional amount
|
amount, err := utils.ParseAmount(utils.TrimTrailingZerosInDecimal(strings.ReplaceAll(ofxTransaction.Amount, ",", "."))) // ofx supports decimal point or comma to indicate the start of the fractional amount
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[ofx_transaction_table.parseTransaction] cannot parsing transaction amount \"%s\", because %s", ofxTransaction.Amount, err.Error())
|
||||||
return nil, errs.ErrAmountInvalid
|
return nil, errs.ErrAmountInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +105,7 @@ func (t *qifTransactionDataRowIterator) Next(ctx core.Context, user *models.User
|
|||||||
rowItems, err := t.parseTransaction(ctx, user, data)
|
rowItems, err := t.parseTransaction(ctx, user, data)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(ctx, "[qif_transaction_data_table.Next] cannot parsing transaction in row#%d, because %s", t.currentIndex, err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package converters
|
|||||||
import (
|
import (
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/alipay"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/alipay"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/base"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/base"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/default"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/default"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/converters/dsv"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/feidee"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/feidee"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/fireflyIII"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/fireflyIII"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/gnucash"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/gnucash"
|
||||||
@@ -12,6 +14,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/qif"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/qif"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/wechat"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/wechat"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetTransactionDataExporter returns the transaction data exporter according to the file type
|
// GetTransactionDataExporter returns the transaction data exporter according to the file type
|
||||||
@@ -61,3 +64,18 @@ func GetTransactionDataImporter(fileType string) (base.TransactionDataImporter,
|
|||||||
return nil, errs.ErrImportFileTypeNotSupported
|
return nil, errs.ErrImportFileTypeNotSupported
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCustomDelimiterSeparatedValuesFileType returns whether the file type is the delimiter-separated values file type
|
||||||
|
func IsCustomDelimiterSeparatedValuesFileType(fileType string) bool {
|
||||||
|
return dsv.IsDelimiterSeparatedValuesFileType(fileType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNewDelimiterSeparatedValuesDataParser returns a new delimiter-separated values data parser according to the file type and encoding
|
||||||
|
func CreateNewDelimiterSeparatedValuesDataParser(fileType string, fileEncoding string) (dsv.CustomTransactionDataDsvFileParser, error) {
|
||||||
|
return dsv.CreateNewCustomTransactionDataDsvFileParser(fileType, fileEncoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNewDelimiterSeparatedValuesDataImporter returns a new delimiter-separated values data importer according to the file type and encoding
|
||||||
|
func CreateNewDelimiterSeparatedValuesDataImporter(fileType string, fileEncoding string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, geoLocationSeparator string, transactionTagSeparator string) (base.TransactionDataImporter, error) {
|
||||||
|
return dsv.CreateNewCustomTransactionDataDsvFileImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormat, timezoneFormat, geoLocationSeparator, transactionTagSeparator)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const (
|
|||||||
DECIMAL_SEPARATOR_DEFAULT DecimalSeparator = 0
|
DECIMAL_SEPARATOR_DEFAULT DecimalSeparator = 0
|
||||||
DECIMAL_SEPARATOR_DOT DecimalSeparator = 1
|
DECIMAL_SEPARATOR_DOT DecimalSeparator = 1
|
||||||
DECIMAL_SEPARATOR_COMMA DecimalSeparator = 2
|
DECIMAL_SEPARATOR_COMMA DecimalSeparator = 2
|
||||||
DECIMAL_SEPARATOR_SPACE DecimalSeparator = 3
|
|
||||||
DECIMAL_SEPARATOR_INVALID DecimalSeparator = 255
|
DECIMAL_SEPARATOR_INVALID DecimalSeparator = 255
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,8 +24,6 @@ func (f DecimalSeparator) String() string {
|
|||||||
return "Dot"
|
return "Dot"
|
||||||
case DECIMAL_SEPARATOR_COMMA:
|
case DECIMAL_SEPARATOR_COMMA:
|
||||||
return "Comma"
|
return "Comma"
|
||||||
case DECIMAL_SEPARATOR_SPACE:
|
|
||||||
return "Space"
|
|
||||||
case DECIMAL_SEPARATOR_INVALID:
|
case DECIMAL_SEPARATOR_INVALID:
|
||||||
return "Invalid"
|
return "Invalid"
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -15,22 +15,22 @@ type GocronLoggerAdapter struct {
|
|||||||
|
|
||||||
// Debug logs debug log
|
// Debug logs debug log
|
||||||
func (logger GocronLoggerAdapter) Debug(msg string, args ...any) {
|
func (logger GocronLoggerAdapter) Debug(msg string, args ...any) {
|
||||||
log.Debugf(core.NewNullContext(), logger.getFinalLog(msg, args...))
|
log.Debugf(core.NewNullContext(), "%s", logger.getFinalLog(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs info log
|
// Info logs info log
|
||||||
func (logger GocronLoggerAdapter) Info(msg string, args ...any) {
|
func (logger GocronLoggerAdapter) Info(msg string, args ...any) {
|
||||||
log.Infof(core.NewNullContext(), logger.getFinalLog(msg, args...))
|
log.Infof(core.NewNullContext(), "%s", logger.getFinalLog(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs warn log
|
// Warn logs warn log
|
||||||
func (logger GocronLoggerAdapter) Warn(msg string, args ...any) {
|
func (logger GocronLoggerAdapter) Warn(msg string, args ...any) {
|
||||||
log.Warnf(core.NewNullContext(), logger.getFinalLog(msg, args...))
|
log.Warnf(core.NewNullContext(), "%s", logger.getFinalLog(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs error log
|
// Error logs error log
|
||||||
func (logger GocronLoggerAdapter) Error(msg string, args ...any) {
|
func (logger GocronLoggerAdapter) Error(msg string, args ...any) {
|
||||||
log.Errorf(core.NewNullContext(), logger.getFinalLog(msg, args...))
|
log.Errorf(core.NewNullContext(), "%s", logger.getFinalLog(msg, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logger GocronLoggerAdapter) getFinalLog(msg string, args ...any) string {
|
func (logger GocronLoggerAdapter) getFinalLog(msg string, args ...any) string {
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import (
|
|||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database represents a database instance
|
// Database represents a database instance
|
||||||
type Database struct {
|
type Database struct {
|
||||||
engineGroup *xorm.EngineGroup
|
databaseType string
|
||||||
|
engineGroup *xorm.EngineGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSession starts a new session with the specified context
|
// NewSession starts a new session with the specified context
|
||||||
@@ -41,3 +43,23 @@ func (db *Database) DoTransaction(c core.Context, fn func(sess *xorm.Session) er
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSavePoint sets a save point in the current transaction for Postgres
|
||||||
|
func (db *Database) SetSavePoint(sess *xorm.Session, savePointName string) error {
|
||||||
|
if db.databaseType == settings.PostgresDbType {
|
||||||
|
_, err := sess.Exec("SAVEPOINT " + savePointName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RollbackToSavePoint rolls back to the specified save point in the current transaction for Postgres
|
||||||
|
func (db *Database) RollbackToSavePoint(sess *xorm.Session, savePointName string) error {
|
||||||
|
if db.databaseType == settings.PostgresDbType {
|
||||||
|
_, err := sess.Exec("ROLLBACK TO SAVEPOINT " + savePointName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ func initializeDatabase(dbConfig *settings.DatabaseConfig) (*Database, error) {
|
|||||||
engineGroup.SetConnMaxLifetime(time.Duration(dbConfig.ConnectionMaxLifeTime) * time.Second)
|
engineGroup.SetConnMaxLifetime(time.Duration(dbConfig.ConnectionMaxLifeTime) * time.Second)
|
||||||
|
|
||||||
return &Database{
|
return &Database{
|
||||||
engineGroup: engineGroup,
|
databaseType: dbConfig.DatabaseType,
|
||||||
|
engineGroup: engineGroup,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ type DuplicateChecker interface {
|
|||||||
SetSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string, remark string)
|
SetSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string, remark string)
|
||||||
GetOrSetCronJobRunningInfo(jobName string, runningInfo string, runningInterval time.Duration) (bool, string)
|
GetOrSetCronJobRunningInfo(jobName string, runningInfo string, runningInterval time.Duration) (bool, string)
|
||||||
RemoveCronJobRunningInfo(jobName string)
|
RemoveCronJobRunningInfo(jobName string)
|
||||||
|
GetFailureCount(failureKey string) uint32
|
||||||
|
IncreaseFailureCount(failureKey string) uint32
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,3 +48,13 @@ func (c *DuplicateCheckerContainer) GetOrSetCronJobRunningInfo(jobName string, r
|
|||||||
func (c *DuplicateCheckerContainer) RemoveCronJobRunningInfo(jobName string) {
|
func (c *DuplicateCheckerContainer) RemoveCronJobRunningInfo(jobName string) {
|
||||||
c.Current.RemoveCronJobRunningInfo(jobName)
|
c.Current.RemoveCronJobRunningInfo(jobName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFailureCount returns the failure count of the specified failure key
|
||||||
|
func (c *DuplicateCheckerContainer) GetFailureCount(failureKey string) uint32 {
|
||||||
|
return c.Current.GetFailureCount(failureKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncreaseFailureCount increases the failure count of the specified failure key
|
||||||
|
func (c *DuplicateCheckerContainer) IncreaseFailureCount(failureKey string) uint32 {
|
||||||
|
return c.Current.IncreaseFailureCount(failureKey)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ const (
|
|||||||
DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4
|
DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4
|
||||||
DUPLICATE_CHECKER_TYPE_NEW_PICTURE DuplicateCheckerType = 5
|
DUPLICATE_CHECKER_TYPE_NEW_PICTURE DuplicateCheckerType = 5
|
||||||
DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS DuplicateCheckerType = 6
|
DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS DuplicateCheckerType = 6
|
||||||
|
DUPLICATE_CHECKER_TYPE_FAILURE_CHECK DuplicateCheckerType = 255
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -69,6 +69,34 @@ func (c *InMemoryDuplicateChecker) RemoveCronJobRunningInfo(jobName string) {
|
|||||||
c.cache.Delete(c.getCacheKey(DUPLICATE_CHECKER_TYPE_BACKGROUND_CRON_JOB, 0, jobName))
|
c.cache.Delete(c.getCacheKey(DUPLICATE_CHECKER_TYPE_BACKGROUND_CRON_JOB, 0, jobName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFailureCount returns the failure count of the specified failure key
|
||||||
|
func (c *InMemoryDuplicateChecker) GetFailureCount(failureKey string) uint32 {
|
||||||
|
existedFailureCount, found := c.cache.Get(c.getCacheKey(DUPLICATE_CHECKER_TYPE_FAILURE_CHECK, 0, failureKey))
|
||||||
|
|
||||||
|
if found {
|
||||||
|
return existedFailureCount.(uint32)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncreaseFailureCount increases the failure count of the specified failure key
|
||||||
|
func (c *InMemoryDuplicateChecker) IncreaseFailureCount(failureKey string) uint32 {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
cacheKey := c.getCacheKey(DUPLICATE_CHECKER_TYPE_FAILURE_CHECK, 0, failureKey)
|
||||||
|
_, found := c.cache.Get(cacheKey)
|
||||||
|
|
||||||
|
if found {
|
||||||
|
failureCount, _ := c.cache.IncrementUint32(cacheKey, uint32(1))
|
||||||
|
return failureCount
|
||||||
|
} else {
|
||||||
|
c.cache.Set(cacheKey, uint32(1), 1*time.Minute)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *InMemoryDuplicateChecker) getCacheKey(checkerType DuplicateCheckerType, uid int64, identification string) string {
|
func (c *InMemoryDuplicateChecker) getCacheKey(checkerType DuplicateCheckerType, uid int64, identification string) string {
|
||||||
return fmt.Sprintf("%d|%d|%s", checkerType, uid, identification)
|
return fmt.Sprintf("%d|%d|%s", checkerType, uid, identification)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,3 +155,77 @@ func TestGetOrSetRunningInfoConcurrent(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, uint32(999), setRunningInfoCount.Load())
|
assert.Equal(t, uint32(999), setRunningInfoCount.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetFailureCount(t *testing.T) {
|
||||||
|
checker, _ := NewInMemoryDuplicateChecker(&settings.Config{
|
||||||
|
DuplicateSubmissionsIntervalDuration: time.Second,
|
||||||
|
InMemoryDuplicateCheckerCleanupIntervalDuration: time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
|
failureKey := "127.0.0.1"
|
||||||
|
|
||||||
|
failureCount := checker.GetFailureCount(failureKey)
|
||||||
|
assert.Equal(t, uint32(0), failureCount)
|
||||||
|
|
||||||
|
failureCount = checker.IncreaseFailureCount(failureKey)
|
||||||
|
assert.Equal(t, uint32(1), failureCount)
|
||||||
|
|
||||||
|
failureCount = checker.GetFailureCount(failureKey)
|
||||||
|
assert.Equal(t, uint32(1), failureCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIncreaseFailureCount(t *testing.T) {
|
||||||
|
checker, _ := NewInMemoryDuplicateChecker(&settings.Config{
|
||||||
|
DuplicateSubmissionsIntervalDuration: time.Second,
|
||||||
|
InMemoryDuplicateCheckerCleanupIntervalDuration: time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
|
failureKey := "127.0.0.1"
|
||||||
|
|
||||||
|
failureCount := checker.IncreaseFailureCount(failureKey)
|
||||||
|
assert.Equal(t, uint32(1), failureCount)
|
||||||
|
|
||||||
|
failureCount = checker.GetFailureCount(failureKey)
|
||||||
|
assert.Equal(t, uint32(1), failureCount)
|
||||||
|
|
||||||
|
failureCount = checker.IncreaseFailureCount(failureKey)
|
||||||
|
assert.Equal(t, uint32(2), failureCount)
|
||||||
|
|
||||||
|
failureCount = checker.GetFailureCount(failureKey)
|
||||||
|
assert.Equal(t, uint32(2), failureCount)
|
||||||
|
|
||||||
|
failureCount = checker.IncreaseFailureCount(failureKey)
|
||||||
|
assert.Equal(t, uint32(3), failureCount)
|
||||||
|
|
||||||
|
failureCount = checker.GetFailureCount(failureKey)
|
||||||
|
assert.Equal(t, uint32(3), failureCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIncreaseFailureCountConcurrent(t *testing.T) {
|
||||||
|
checker, _ := NewInMemoryDuplicateChecker(&settings.Config{
|
||||||
|
DuplicateSubmissionsIntervalDuration: time.Second,
|
||||||
|
InMemoryDuplicateCheckerCleanupIntervalDuration: time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
|
failureKey := "127.0.0.1"
|
||||||
|
|
||||||
|
concurrentCount := 10
|
||||||
|
var waitGroup sync.WaitGroup
|
||||||
|
|
||||||
|
for routineIndex := 0; routineIndex < concurrentCount; routineIndex++ {
|
||||||
|
waitGroup.Add(1)
|
||||||
|
|
||||||
|
go func(currentRoutineIndex int) {
|
||||||
|
for cycle := 0; cycle < 10; cycle++ {
|
||||||
|
checker.IncreaseFailureCount(failureKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitGroup.Done()
|
||||||
|
}(routineIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitGroup.Wait()
|
||||||
|
|
||||||
|
failureCount := checker.GetFailureCount(failureKey)
|
||||||
|
assert.Equal(t, uint32(100), failureCount)
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ var (
|
|||||||
ErrNoFilesUpload = NewNormalError(NormalSubcategoryGlobal, 15, http.StatusBadRequest, "no files uploaded")
|
ErrNoFilesUpload = NewNormalError(NormalSubcategoryGlobal, 15, http.StatusBadRequest, "no files uploaded")
|
||||||
ErrUploadedFileEmpty = NewNormalError(NormalSubcategoryGlobal, 16, http.StatusBadRequest, "uploaded file is empty")
|
ErrUploadedFileEmpty = NewNormalError(NormalSubcategoryGlobal, 16, http.StatusBadRequest, "uploaded file is empty")
|
||||||
ErrExceedMaxUploadFileSize = NewNormalError(NormalSubcategoryGlobal, 17, http.StatusBadRequest, "uploaded file size exceeds the maximum allowed size")
|
ErrExceedMaxUploadFileSize = NewNormalError(NormalSubcategoryGlobal, 17, http.StatusBadRequest, "uploaded file size exceeds the maximum allowed size")
|
||||||
|
ErrFailureCountLimitReached = NewNormalError(NormalSubcategoryGlobal, 18, http.StatusBadRequest, "failure count exceeded maximum limit")
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetParameterInvalidMessage returns specific error message for invalid parameter error
|
// GetParameterInvalidMessage returns specific error message for invalid parameter error
|
||||||
|
|||||||
@@ -35,4 +35,10 @@ var (
|
|||||||
ErrCannotAddTransactionBeforeBalanceModificationTransaction = NewSystemError(NormalSubcategoryTransaction, 28, http.StatusBadRequest, "cannot add transaction before balance modification transaction")
|
ErrCannotAddTransactionBeforeBalanceModificationTransaction = NewSystemError(NormalSubcategoryTransaction, 28, http.StatusBadRequest, "cannot add transaction before balance modification transaction")
|
||||||
ErrBalanceModificationTransactionCannotModifyTime = NewSystemError(NormalSubcategoryTransaction, 29, http.StatusBadRequest, "balance modification transaction cannot modify transaction time")
|
ErrBalanceModificationTransactionCannotModifyTime = NewSystemError(NormalSubcategoryTransaction, 29, http.StatusBadRequest, "balance modification transaction cannot modify transaction time")
|
||||||
ErrTransferTransactionAmountCannotBeLessThanZero = NewNormalError(NormalSubcategoryTransaction, 30, http.StatusBadRequest, "transfer transaction amount cannot be less than zero")
|
ErrTransferTransactionAmountCannotBeLessThanZero = NewNormalError(NormalSubcategoryTransaction, 30, http.StatusBadRequest, "transfer transaction amount cannot be less than zero")
|
||||||
|
ErrImportFileEncodingIsEmpty = NewSystemError(NormalSubcategoryTransaction, 31, http.StatusBadRequest, "import file encoding is empty")
|
||||||
|
ErrImportFileEncodingNotSupported = NewSystemError(NormalSubcategoryTransaction, 32, http.StatusBadRequest, "import file encoding not supported")
|
||||||
|
ErrImportFileColumnMappingInvalid = NewSystemError(NormalSubcategoryTransaction, 33, http.StatusBadRequest, "column mapping invalid")
|
||||||
|
ErrImportFileTransactionTypeMappingInvalid = NewSystemError(NormalSubcategoryTransaction, 34, http.StatusBadRequest, "transaction type mapping invalid")
|
||||||
|
ErrImportFileTransactionTimeFormatInvalid = NewSystemError(NormalSubcategoryTransaction, 35, http.StatusBadRequest, "transaction time format invalid")
|
||||||
|
ErrImportFileTransactionTimezoneFormatInvalid = NewSystemError(NormalSubcategoryTransaction, 36, http.StatusBadRequest, "transaction time zone format invalid")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import "net/http"
|
|||||||
|
|
||||||
// Error codes related to transaction templates
|
// Error codes related to transaction templates
|
||||||
var (
|
var (
|
||||||
ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid")
|
ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid")
|
||||||
ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found")
|
ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found")
|
||||||
ErrTransactionTemplateTypeInvalid = NewNormalError(NormalSubcategoryTemplate, 2, http.StatusBadRequest, "transaction template type is invalid")
|
ErrTransactionTemplateTypeInvalid = NewNormalError(NormalSubcategoryTemplate, 2, http.StatusBadRequest, "transaction template type is invalid")
|
||||||
ErrScheduledTransactionNotEnabled = NewNormalError(NormalSubcategoryTemplate, 3, http.StatusBadRequest, "scheduled transaction is not enabled")
|
ErrScheduledTransactionNotEnabled = NewNormalError(NormalSubcategoryTemplate, 3, http.StatusBadRequest, "scheduled transaction is not enabled")
|
||||||
ErrScheduledTransactionFrequencyInvalid = NewNormalError(NormalSubcategoryTemplate, 4, http.StatusBadRequest, "scheduled transaction frequency is invalid")
|
ErrScheduledTransactionFrequencyInvalid = NewNormalError(NormalSubcategoryTemplate, 4, http.StatusBadRequest, "scheduled transaction frequency is invalid")
|
||||||
ErrTransactionTemplateHasTooManyTags = NewNormalError(NormalSubcategoryTemplate, 5, http.StatusBadRequest, "transaction template has too many tags")
|
ErrTransactionTemplateHasTooManyTags = NewNormalError(NormalSubcategoryTemplate, 5, http.StatusBadRequest, "transaction template has too many tags")
|
||||||
|
ErrScheduledTransactionTemplateStartDataLaterThanEndDate = NewNormalError(NormalSubcategoryTemplate, 6, http.StatusBadRequest, "scheduled transaction start date is later than end time")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ func (e *InternationalMonetaryFundDataSource) BuildRequests() ([]*http.Request,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "") // Do not set custom user agent
|
||||||
|
|
||||||
return []*http.Request{req}, nil
|
return []*http.Request{req}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,21 @@ var DefaultLanguage = en
|
|||||||
// AllLanguages represents all the supported language
|
// AllLanguages represents all the supported language
|
||||||
// To add new languages, please refer to https://ezbookkeeping.mayswind.net/translating
|
// To add new languages, please refer to https://ezbookkeeping.mayswind.net/translating
|
||||||
var AllLanguages = map[string]*LocaleInfo{
|
var AllLanguages = map[string]*LocaleInfo{
|
||||||
|
"de": {
|
||||||
|
Content: de,
|
||||||
|
},
|
||||||
"en": {
|
"en": {
|
||||||
Content: en,
|
Content: en,
|
||||||
},
|
},
|
||||||
|
"es": {
|
||||||
|
Content: es,
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
Content: ja,
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
Content: ru,
|
||||||
|
},
|
||||||
"vi": {
|
"vi": {
|
||||||
Content: vi,
|
Content: vi,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package locales
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
var de = &LocaleTextItems{
|
||||||
|
DefaultTypes: &DefaultTypes{
|
||||||
|
DecimalSeparator: core.DECIMAL_SEPARATOR_COMMA,
|
||||||
|
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_DOT,
|
||||||
|
},
|
||||||
|
DataConverterTextItems: &DataConverterTextItems{
|
||||||
|
Alipay: "Alipay",
|
||||||
|
WeChatWallet: "Wallet",
|
||||||
|
},
|
||||||
|
VerifyEmailTextItems: &VerifyEmailTextItems{
|
||||||
|
Title: "E-Mail verifizieren",
|
||||||
|
SalutationFormat: "Hallo %s,",
|
||||||
|
DescriptionAboveBtn: "Bitte klicken Sie auf den untenstehenden Link, um Ihre E-Mail-Adresse zu bestätigen.",
|
||||||
|
VerifyEmail: "E-Mail verifizieren",
|
||||||
|
DescriptionBelowBtnFormat: "Wenn Sie kein %s Konto erstellt haben, ignorieren Sie bitte diese E-Mail. Wenn Sie den obigen Link nicht anklicken können, kopieren Sie bitte die obige URL und fügen Sie sie in Ihren Browser ein. Der Verifizierungslink wird nach %v Minuten ablaufen.",
|
||||||
|
},
|
||||||
|
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
|
||||||
|
Title: "Passwort zurücksetzen",
|
||||||
|
SalutationFormat: "Hallo %s,",
|
||||||
|
DescriptionAboveBtn: "Wir haben kürzlich eine Anfrage zum Zurücksetzen Ihres Passworts erhalten. Sie können auf den untenstehenden Link klicken, um Ihr Passwort zurückzusetzen.",
|
||||||
|
ResetPassword: "Passwort zurücksetzen",
|
||||||
|
DescriptionBelowBtnFormat: "Wenn Sie nicht angefordert haben, Ihr Passwort zurückzusetzen, ignorieren Sie bitte diese E-Mail. Wenn Sie den obigen Link nicht anklicken können, kopieren Sie bitte die obige URL und fügen Sie sie in Ihren Browser ein. Der Link zum Zurücksetzen des Passworts wird nach %v Minuten ablaufen.",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package locales
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
var es = &LocaleTextItems{
|
||||||
|
DefaultTypes: &DefaultTypes{
|
||||||
|
DecimalSeparator: core.DECIMAL_SEPARATOR_DOT,
|
||||||
|
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA,
|
||||||
|
},
|
||||||
|
DataConverterTextItems: &DataConverterTextItems{
|
||||||
|
Alipay: "Alipay",
|
||||||
|
WeChatWallet: "Wallet",
|
||||||
|
},
|
||||||
|
VerifyEmailTextItems: &VerifyEmailTextItems{
|
||||||
|
Title: "Verifique su cuenta de correo",
|
||||||
|
SalutationFormat: "Hola %s,",
|
||||||
|
DescriptionAboveBtn: "Por favor, haga click en el siguiente enlace para confirmar su cuenta de correo.",
|
||||||
|
VerifyEmail: "Verificar correo",
|
||||||
|
DescriptionBelowBtnFormat: "Si no registró una cuenta de %s, simplemente haga caso omiso a este correo. Si no puede hacer click en el link de verificación, copie la url arriba mostrada y péguela en su navegador. El enlace de verificación de correo expira pasados %v minutos.",
|
||||||
|
},
|
||||||
|
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
|
||||||
|
Title: "Restablezca su Contraseña",
|
||||||
|
SalutationFormat: "Hola %s,",
|
||||||
|
DescriptionAboveBtn: "Hemos recibido una solicitud para restablecer su contraseña. Puede hacer click en el siguiente link para restablecer su contraseña.",
|
||||||
|
ResetPassword: "Restablecer Contraseña",
|
||||||
|
DescriptionBelowBtnFormat: "Si no solicitó un restablecimiento de contraseña, simplemente descarte este correo. Si no puede hacer click en el link anterior, copie la url arriba mostrada y péguela en su navegadror. El enlace de restablecimiento de contraseña expira pasados %v minutos.",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package locales
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ja = &LocaleTextItems{
|
||||||
|
DefaultTypes: &DefaultTypes{
|
||||||
|
DecimalSeparator: core.DECIMAL_SEPARATOR_DOT,
|
||||||
|
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA,
|
||||||
|
},
|
||||||
|
DataConverterTextItems: &DataConverterTextItems{
|
||||||
|
Alipay: "Alipay",
|
||||||
|
WeChatWallet: "Wallet",
|
||||||
|
},
|
||||||
|
VerifyEmailTextItems: &VerifyEmailTextItems{
|
||||||
|
Title: "メールの確認",
|
||||||
|
SalutationFormat: "こんにちは%s,",
|
||||||
|
DescriptionAboveBtn: "次のリンクをクリックしてメールアドレスを確認してください。",
|
||||||
|
VerifyEmail: "メールを確認",
|
||||||
|
DescriptionBelowBtnFormat: "%sアカウントに登録していない場合はこのメールを無視してください。上記のリンクをクリックできない場合は上記のURLをコピーしてブラウザに貼り付けてください。メールの確認リンクは%v分後に期限切れになります。",
|
||||||
|
},
|
||||||
|
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
|
||||||
|
Title: "パスワードのリセット",
|
||||||
|
SalutationFormat: "こんにちは%s,",
|
||||||
|
DescriptionAboveBtn: "パスワードリセットのリクエストを受け取りました。次のリンクをクリックしてパスワードをリセットしてください。",
|
||||||
|
ResetPassword: "パスワードをリセット",
|
||||||
|
DescriptionBelowBtnFormat: "パスワードのリセットをリクエストしていない場合はこのメールを無視してください。上記のリンクをクリックできない場合は、上記のURLをコピーしてブラウザに貼り付けてください。パスワードリセットのリンクは%v分後に期限切れになります。",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package locales
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ru = &LocaleTextItems{
|
||||||
|
DefaultTypes: &DefaultTypes{
|
||||||
|
DecimalSeparator: core.DECIMAL_SEPARATOR_DOT,
|
||||||
|
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_COMMA,
|
||||||
|
},
|
||||||
|
DataConverterTextItems: &DataConverterTextItems{
|
||||||
|
Alipay: "Alipay",
|
||||||
|
WeChatWallet: "Wallet",
|
||||||
|
},
|
||||||
|
VerifyEmailTextItems: &VerifyEmailTextItems{
|
||||||
|
Title: "Подтвердите электронную почту",
|
||||||
|
SalutationFormat: "Здравствуйте %s,",
|
||||||
|
DescriptionAboveBtn: "Нажмите на ссылку ниже, чтобы подтвердить свой адрес электронной почты.",
|
||||||
|
VerifyEmail: "Подтвердить электронную почту",
|
||||||
|
DescriptionBelowBtnFormat: "Если вы не регистрировали учетную запись %s, просто проигнорируйте это письмо. Если вы не можете нажать на ссылку выше, скопируйте указанный выше URL и вставьте его в свой браузер. Срок действия ссылки для проверки электронной почты истечет через %v минут.",
|
||||||
|
},
|
||||||
|
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
|
||||||
|
Title: "Сброс пароля",
|
||||||
|
SalutationFormat: "Здравствуйте %s,",
|
||||||
|
DescriptionAboveBtn: "Недавно мы получили запрос на сброс вашего пароля. Нажмите на ссылку ниже, чтобы сбросить свой пароль.",
|
||||||
|
ResetPassword: "Сбросить пароль",
|
||||||
|
DescriptionBelowBtnFormat: "Если вы не запрашивали сброс пароля, просто проигнорируйте это письмо. Если вы не можете нажать на ссылку выше, скопируйте указанный выше URL и вставьте его в браузер. Ссылка для сброса пароля истечет через %v минут.",
|
||||||
|
},
|
||||||
|
}
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
var vi = &LocaleTextItems{
|
var vi = &LocaleTextItems{
|
||||||
DefaultTypes: &DefaultTypes{
|
DefaultTypes: &DefaultTypes{
|
||||||
DecimalSeparator: core.DECIMAL_SEPARATOR_COMMA,
|
DecimalSeparator: core.DECIMAL_SEPARATOR_COMMA,
|
||||||
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_DOT,
|
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_DOT,
|
||||||
},
|
},
|
||||||
DataConverterTextItems: &DataConverterTextItems{
|
DataConverterTextItems: &DataConverterTextItems{
|
||||||
|
|||||||
+1
-1
@@ -141,7 +141,7 @@ func SetLoggerConfiguration(config *settings.Config, isDisableBootLog bool) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DebugfWithRequestId logs debug log with custom format
|
// Debugf logs debug log with custom format
|
||||||
func Debugf(c core.Context, format string, args ...any) {
|
func Debugf(c core.Context, format string, args ...any) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
defaultLogger.Debug(getFinalLog(format, args...))
|
defaultLogger.Debug(getFinalLog(format, args...))
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ type Account struct {
|
|||||||
Category AccountCategory `xorm:"NOT NULL"`
|
Category AccountCategory `xorm:"NOT NULL"`
|
||||||
Type AccountType `xorm:"NOT NULL"`
|
Type AccountType `xorm:"NOT NULL"`
|
||||||
ParentAccountId int64 `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
|
ParentAccountId int64 `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
|
||||||
Name string `xorm:"VARCHAR(32) NOT NULL"`
|
Name string `xorm:"VARCHAR(64) NOT NULL"`
|
||||||
DisplayOrder int32 `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
|
DisplayOrder int32 `xorm:"INDEX(IDX_account_uid_deleted_parent_account_id_order) NOT NULL"`
|
||||||
Icon int64 `xorm:"NOT NULL"`
|
Icon int64 `xorm:"NOT NULL"`
|
||||||
Color string `xorm:"VARCHAR(6) NOT NULL"`
|
Color string `xorm:"VARCHAR(6) NOT NULL"`
|
||||||
@@ -85,7 +85,7 @@ type AccountExtend struct {
|
|||||||
|
|
||||||
// AccountCreateRequest represents all parameters of account creation request
|
// AccountCreateRequest represents all parameters of account creation request
|
||||||
type AccountCreateRequest struct {
|
type AccountCreateRequest struct {
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||||
Category AccountCategory `json:"category" binding:"required"`
|
Category AccountCategory `json:"category" binding:"required"`
|
||||||
Type AccountType `json:"type" binding:"required"`
|
Type AccountType `json:"type" binding:"required"`
|
||||||
Icon int64 `json:"icon,string" binding:"required,min=1"`
|
Icon int64 `json:"icon,string" binding:"required,min=1"`
|
||||||
@@ -102,7 +102,7 @@ type AccountCreateRequest struct {
|
|||||||
// AccountModifyRequest represents all parameters of account modification request
|
// AccountModifyRequest represents all parameters of account modification request
|
||||||
type AccountModifyRequest struct {
|
type AccountModifyRequest struct {
|
||||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||||
Category AccountCategory `json:"category" binding:"required"`
|
Category AccountCategory `json:"category" binding:"required"`
|
||||||
Icon int64 `json:"icon,string" binding:"min=1"`
|
Icon int64 `json:"icon,string" binding:"min=1"`
|
||||||
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
|
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
|
||||||
|
|||||||
@@ -311,8 +311,8 @@ type TransactionStatisticResponseItem struct {
|
|||||||
TotalAmount int64 `json:"amount"`
|
TotalAmount int64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionStatisticTrendsItem represents the data within each statistic interval
|
// TransactionStatisticTrendsResponseItem represents the data within each statistic interval
|
||||||
type TransactionStatisticTrendsItem struct {
|
type TransactionStatisticTrendsResponseItem struct {
|
||||||
Year int32 `json:"year"`
|
Year int32 `json:"year"`
|
||||||
Month int32 `json:"month"`
|
Month int32 `json:"month"`
|
||||||
Items []*TransactionStatisticResponseItem `json:"items"`
|
Items []*TransactionStatisticResponseItem `json:"items"`
|
||||||
@@ -493,21 +493,21 @@ func (s TransactionInfoResponseSlice) Less(i, j int) bool {
|
|||||||
return s[i].Id > s[j].Id
|
return s[i].Id > s[j].Id
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionStatisticTrendsItemSlice represents the slice data structure of TransactionStatisticTrendsItem
|
// TransactionStatisticTrendsResponseItemSlice represents the slice data structure of TransactionStatisticTrendsResponseItem
|
||||||
type TransactionStatisticTrendsItemSlice []*TransactionStatisticTrendsItem
|
type TransactionStatisticTrendsResponseItemSlice []*TransactionStatisticTrendsResponseItem
|
||||||
|
|
||||||
// Len returns the count of items
|
// Len returns the count of items
|
||||||
func (s TransactionStatisticTrendsItemSlice) Len() int {
|
func (s TransactionStatisticTrendsResponseItemSlice) Len() int {
|
||||||
return len(s)
|
return len(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swap swaps two items
|
// Swap swaps two items
|
||||||
func (s TransactionStatisticTrendsItemSlice) Swap(i, j int) {
|
func (s TransactionStatisticTrendsResponseItemSlice) Swap(i, j int) {
|
||||||
s[i], s[j] = s[j], s[i]
|
s[i], s[j] = s[j], s[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Less reports whether the first item is less than the second one
|
// Less reports whether the first item is less than the second one
|
||||||
func (s TransactionStatisticTrendsItemSlice) Less(i, j int) bool {
|
func (s TransactionStatisticTrendsResponseItemSlice) Less(i, j int) bool {
|
||||||
if s[i].Year != s[j].Year {
|
if s[i].Year != s[j].Year {
|
||||||
return s[i].Year < s[j].Year
|
return s[i].Year < s[j].Year
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type TransactionCategory struct {
|
|||||||
Deleted bool `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
|
Deleted bool `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
|
||||||
Type TransactionCategoryType `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
|
Type TransactionCategoryType `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
|
||||||
ParentCategoryId int64 `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
|
ParentCategoryId int64 `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
|
||||||
Name string `xorm:"VARCHAR(32) NOT NULL"`
|
Name string `xorm:"VARCHAR(64) NOT NULL"`
|
||||||
DisplayOrder int32 `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
|
DisplayOrder int32 `xorm:"INDEX(IDX_category_uid_deleted_type_parent_category_id_order) NOT NULL"`
|
||||||
Icon int64 `xorm:"NOT NULL"`
|
Icon int64 `xorm:"NOT NULL"`
|
||||||
Color string `xorm:"VARCHAR(6) NOT NULL"`
|
Color string `xorm:"VARCHAR(6) NOT NULL"`
|
||||||
@@ -44,7 +44,7 @@ type TransactionCategoryGetRequest struct {
|
|||||||
|
|
||||||
// TransactionCategoryCreateRequest represents all parameters of single transaction category creation request
|
// TransactionCategoryCreateRequest represents all parameters of single transaction category creation request
|
||||||
type TransactionCategoryCreateRequest struct {
|
type TransactionCategoryCreateRequest struct {
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||||
Type TransactionCategoryType `json:"type" binding:"required"`
|
Type TransactionCategoryType `json:"type" binding:"required"`
|
||||||
ParentId int64 `json:"parentId,string" binding:"min=0"`
|
ParentId int64 `json:"parentId,string" binding:"min=0"`
|
||||||
Icon int64 `json:"icon,string" binding:"min=1"`
|
Icon int64 `json:"icon,string" binding:"min=1"`
|
||||||
@@ -60,7 +60,7 @@ type TransactionCategoryCreateBatchRequest struct {
|
|||||||
|
|
||||||
// TransactionCategoryCreateWithSubCategories represents all parameters of multi transaction categories creation request
|
// TransactionCategoryCreateWithSubCategories represents all parameters of multi transaction categories creation request
|
||||||
type TransactionCategoryCreateWithSubCategories struct {
|
type TransactionCategoryCreateWithSubCategories struct {
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||||
Type TransactionCategoryType `json:"type" binding:"required"`
|
Type TransactionCategoryType `json:"type" binding:"required"`
|
||||||
Icon int64 `json:"icon,string" binding:"min=1"`
|
Icon int64 `json:"icon,string" binding:"min=1"`
|
||||||
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
|
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
|
||||||
@@ -71,7 +71,7 @@ type TransactionCategoryCreateWithSubCategories struct {
|
|||||||
// TransactionCategoryModifyRequest represents all parameters of transaction category modification request
|
// TransactionCategoryModifyRequest represents all parameters of transaction category modification request
|
||||||
type TransactionCategoryModifyRequest struct {
|
type TransactionCategoryModifyRequest struct {
|
||||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||||
ParentId int64 `json:"parentId,string" binding:"min=0"`
|
ParentId int64 `json:"parentId,string" binding:"min=0"`
|
||||||
Icon int64 `json:"icon,string" binding:"min=1"`
|
Icon int64 `json:"icon,string" binding:"min=1"`
|
||||||
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
|
Color string `json:"color" binding:"required,len=6,validHexRGBColor"`
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ type TransactionTag struct {
|
|||||||
TagId int64 `xorm:"PK"`
|
TagId int64 `xorm:"PK"`
|
||||||
Uid int64 `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
|
Uid int64 `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
|
||||||
Deleted bool `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
|
Deleted bool `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
|
||||||
Name string `xorm:"VARCHAR(32) NOT NULL"`
|
Name string `xorm:"VARCHAR(64) NOT NULL"`
|
||||||
DisplayOrder int32 `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
|
DisplayOrder int32 `xorm:"INDEX(IDX_tag_uid_deleted_order) NOT NULL"`
|
||||||
Hidden bool `xorm:"NOT NULL"`
|
Hidden bool `xorm:"NOT NULL"`
|
||||||
CreatedUnixTime int64
|
CreatedUnixTime int64
|
||||||
@@ -20,13 +20,13 @@ type TransactionTagGetRequest struct {
|
|||||||
|
|
||||||
// TransactionTagCreateRequest represents all parameters of transaction tag creation request
|
// TransactionTagCreateRequest represents all parameters of transaction tag creation request
|
||||||
type TransactionTagCreateRequest struct {
|
type TransactionTagCreateRequest struct {
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionTagModifyRequest represents all parameters of transaction tag modification request
|
// TransactionTagModifyRequest represents all parameters of transaction tag modification request
|
||||||
type TransactionTagModifyRequest struct {
|
type TransactionTagModifyRequest struct {
|
||||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionTagHideRequest represents all parameters of transaction tag hiding request
|
// TransactionTagHideRequest represents all parameters of transaction tag hiding request
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
)
|
)
|
||||||
@@ -29,15 +30,17 @@ const (
|
|||||||
type TransactionTemplate struct {
|
type TransactionTemplate struct {
|
||||||
TemplateId int64 `xorm:"PK"`
|
TemplateId int64 `xorm:"PK"`
|
||||||
Uid int64 `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) NOT NULL"`
|
Uid int64 `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) NOT NULL"`
|
||||||
Deleted bool `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at) NOT NULL"`
|
Deleted bool `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time) NOT NULL"`
|
||||||
TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at) NOT NULL"`
|
TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time) NOT NULL"`
|
||||||
Name string `xorm:"VARCHAR(32) NOT NULL"`
|
Name string `xorm:"VARCHAR(64) NOT NULL"`
|
||||||
Type TransactionType `xorm:"NOT NULL"`
|
Type TransactionType `xorm:"NOT NULL"`
|
||||||
CategoryId int64 `xorm:"NOT NULL"`
|
CategoryId int64 `xorm:"NOT NULL"`
|
||||||
AccountId int64 `xorm:"NOT NULL"`
|
AccountId int64 `xorm:"NOT NULL"`
|
||||||
ScheduledFrequencyType TransactionScheduleFrequencyType `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at)"`
|
ScheduledFrequencyType TransactionScheduleFrequencyType `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time)"`
|
||||||
ScheduledFrequency string `xorm:"VARCHAR(100)"`
|
ScheduledFrequency string `xorm:"VARCHAR(100)"`
|
||||||
ScheduledAt int16 `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at)"`
|
ScheduledStartTime *int64 `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time)"`
|
||||||
|
ScheduledEndTime *int64 `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time)"`
|
||||||
|
ScheduledAt int16 `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time)"`
|
||||||
ScheduledTimezoneUtcOffset int16
|
ScheduledTimezoneUtcOffset int16
|
||||||
TagIds string `xorm:"VARCHAR(255) NOT NULL"`
|
TagIds string `xorm:"VARCHAR(255) NOT NULL"`
|
||||||
Amount int64 `xorm:"NOT NULL"`
|
Amount int64 `xorm:"NOT NULL"`
|
||||||
@@ -65,7 +68,7 @@ type TransactionTemplateGetRequest struct {
|
|||||||
// TransactionTemplateCreateRequest represents all parameters of transaction template creation request
|
// TransactionTemplateCreateRequest represents all parameters of transaction template creation request
|
||||||
type TransactionTemplateCreateRequest struct {
|
type TransactionTemplateCreateRequest struct {
|
||||||
TemplateType TransactionTemplateType `json:"templateType"`
|
TemplateType TransactionTemplateType `json:"templateType"`
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||||
Type TransactionType `json:"type" binding:"required"`
|
Type TransactionType `json:"type" binding:"required"`
|
||||||
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
|
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
|
||||||
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
|
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
|
||||||
@@ -77,6 +80,8 @@ type TransactionTemplateCreateRequest struct {
|
|||||||
Comment string `json:"comment" binding:"max=255"`
|
Comment string `json:"comment" binding:"max=255"`
|
||||||
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType" binding:"omitempty"`
|
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType" binding:"omitempty"`
|
||||||
ScheduledFrequency *string `json:"scheduledFrequency" binding:"omitempty"`
|
ScheduledFrequency *string `json:"scheduledFrequency" binding:"omitempty"`
|
||||||
|
ScheduledStartDate *string `json:"scheduledStartDate" binding:"omitempty"`
|
||||||
|
ScheduledEndDate *string `json:"scheduledEndDate" binding:"omitempty"`
|
||||||
ScheduledTimezoneUtcOffset *int16 `json:"utcOffset" binding:"omitempty,min=-720,max=840"`
|
ScheduledTimezoneUtcOffset *int16 `json:"utcOffset" binding:"omitempty,min=-720,max=840"`
|
||||||
ClientSessionId string `json:"clientSessionId"`
|
ClientSessionId string `json:"clientSessionId"`
|
||||||
}
|
}
|
||||||
@@ -84,13 +89,13 @@ type TransactionTemplateCreateRequest struct {
|
|||||||
// TransactionTemplateModifyNameRequest represents all parameters of transaction template name modification request
|
// TransactionTemplateModifyNameRequest represents all parameters of transaction template name modification request
|
||||||
type TransactionTemplateModifyNameRequest struct {
|
type TransactionTemplateModifyNameRequest struct {
|
||||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransactionTemplateModifyRequest represents all parameters of transaction template modification request
|
// TransactionTemplateModifyRequest represents all parameters of transaction template modification request
|
||||||
type TransactionTemplateModifyRequest struct {
|
type TransactionTemplateModifyRequest struct {
|
||||||
Id int64 `json:"id,string" binding:"required,min=1"`
|
Id int64 `json:"id,string" binding:"required,min=1"`
|
||||||
Name string `json:"name" binding:"required,notBlank,max=32"`
|
Name string `json:"name" binding:"required,notBlank,max=64"`
|
||||||
Type TransactionType `json:"type" binding:"required"`
|
Type TransactionType `json:"type" binding:"required"`
|
||||||
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
|
CategoryId int64 `json:"categoryId,string" binding:"required,min=1"`
|
||||||
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
|
SourceAccountId int64 `json:"sourceAccountId,string" binding:"required,min=1"`
|
||||||
@@ -102,6 +107,8 @@ type TransactionTemplateModifyRequest struct {
|
|||||||
Comment string `json:"comment" binding:"max=255"`
|
Comment string `json:"comment" binding:"max=255"`
|
||||||
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType" binding:"omitempty"`
|
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType" binding:"omitempty"`
|
||||||
ScheduledFrequency *string `json:"scheduledFrequency" binding:"omitempty"`
|
ScheduledFrequency *string `json:"scheduledFrequency" binding:"omitempty"`
|
||||||
|
ScheduledStartDate *string `json:"scheduledStartDate" binding:"omitempty"`
|
||||||
|
ScheduledEndDate *string `json:"scheduledEndDate" binding:"omitempty"`
|
||||||
ScheduledTimezoneUtcOffset *int16 `json:"utcOffset" binding:"omitempty,min=-720,max=840"`
|
ScheduledTimezoneUtcOffset *int16 `json:"utcOffset" binding:"omitempty,min=-720,max=840"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +140,8 @@ type TransactionTemplateInfoResponse struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType,omitempty"`
|
ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType,omitempty"`
|
||||||
ScheduledFrequency *string `json:"scheduledFrequency,omitempty"`
|
ScheduledFrequency *string `json:"scheduledFrequency,omitempty"`
|
||||||
|
ScheduledStartDate *string `json:"scheduledStartDate" binding:"omitempty"`
|
||||||
|
ScheduledEndDate *string `json:"scheduledEndDate" binding:"omitempty"`
|
||||||
ScheduledAt *int16 `json:"scheduledAt,omitempty"`
|
ScheduledAt *int16 `json:"scheduledAt,omitempty"`
|
||||||
DisplayOrder int32 `json:"displayOrder"`
|
DisplayOrder int32 `json:"displayOrder"`
|
||||||
Hidden bool `json:"hidden"`
|
Hidden bool `json:"hidden"`
|
||||||
@@ -171,6 +180,18 @@ func (t *TransactionTemplate) ToTransactionTemplateInfoResponse(serverUtcOffset
|
|||||||
response.ScheduledFrequencyType = &t.ScheduledFrequencyType
|
response.ScheduledFrequencyType = &t.ScheduledFrequencyType
|
||||||
response.ScheduledFrequency = &t.ScheduledFrequency
|
response.ScheduledFrequency = &t.ScheduledFrequency
|
||||||
response.ScheduledAt = &t.ScheduledAt
|
response.ScheduledAt = &t.ScheduledAt
|
||||||
|
|
||||||
|
templateTimeZone := time.FixedZone("Template Timezone", int(t.ScheduledTimezoneUtcOffset)*60)
|
||||||
|
|
||||||
|
if t.ScheduledStartTime != nil {
|
||||||
|
startDate := utils.FormatUnixTimeToLongDate(*t.ScheduledStartTime, templateTimeZone)
|
||||||
|
response.ScheduledStartDate = &startDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.ScheduledEndTime != nil {
|
||||||
|
endDate := utils.FormatUnixTimeToLongDate(*t.ScheduledEndTime, templateTimeZone)
|
||||||
|
response.ScheduledEndDate = &endDate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -127,25 +127,25 @@ func TestTransactionInfoResponseSliceLess(t *testing.T) {
|
|||||||
assert.Equal(t, int64(4), transactionRespSlice[4].Id)
|
assert.Equal(t, int64(4), transactionRespSlice[4].Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransactionStatisticTrendsItemSliceLess(t *testing.T) {
|
func TestTransactionStatisticTrendsResponseItemSliceLess(t *testing.T) {
|
||||||
var transactionTrendsSlice TransactionStatisticTrendsItemSlice
|
var transactionTrendsSlice TransactionStatisticTrendsResponseItemSlice
|
||||||
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticTrendsItem{
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticTrendsResponseItem{
|
||||||
Year: 2024,
|
Year: 2024,
|
||||||
Month: 9,
|
Month: 9,
|
||||||
})
|
})
|
||||||
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticTrendsItem{
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticTrendsResponseItem{
|
||||||
Year: 2022,
|
Year: 2022,
|
||||||
Month: 10,
|
Month: 10,
|
||||||
})
|
})
|
||||||
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticTrendsItem{
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticTrendsResponseItem{
|
||||||
Year: 2023,
|
Year: 2023,
|
||||||
Month: 1,
|
Month: 1,
|
||||||
})
|
})
|
||||||
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticTrendsItem{
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticTrendsResponseItem{
|
||||||
Year: 2022,
|
Year: 2022,
|
||||||
Month: 2,
|
Month: 2,
|
||||||
})
|
})
|
||||||
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticTrendsItem{
|
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticTrendsResponseItem{
|
||||||
Year: 2024,
|
Year: 2024,
|
||||||
Month: 1,
|
Month: 1,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
||||||
@@ -270,7 +271,9 @@ func (s *AccountService) CreateAccounts(c core.Context, mainAccount *models.Acco
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.UserDataDB(mainAccount.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
userDataDb := s.UserDataDB(mainAccount.Uid)
|
||||||
|
|
||||||
|
return userDataDb.DoTransaction(c, func(sess *xorm.Session) error {
|
||||||
for i := 0; i < len(allAccounts); i++ {
|
for i := 0; i < len(allAccounts); i++ {
|
||||||
account := allAccounts[i]
|
account := allAccounts[i]
|
||||||
_, err := sess.Insert(account)
|
_, err := sess.Insert(account)
|
||||||
@@ -282,9 +285,25 @@ func (s *AccountService) CreateAccounts(c core.Context, mainAccount *models.Acco
|
|||||||
|
|
||||||
for i := 0; i < len(allInitTransactions); i++ {
|
for i := 0; i < len(allInitTransactions); i++ {
|
||||||
transaction := allInitTransactions[i]
|
transaction := allInitTransactions[i]
|
||||||
|
|
||||||
|
insertTransactionSavePointName := "insert_transaction"
|
||||||
|
err := userDataDb.SetSavePoint(sess, insertTransactionSavePointName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[accounts.CreateAccounts] failed to set save point \"%s\", because %s", insertTransactionSavePointName, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
createdRows, err := sess.Insert(transaction)
|
createdRows, err := sess.Insert(transaction)
|
||||||
|
|
||||||
if err != nil || createdRows < 1 { // maybe another transaction has same time
|
if err != nil || createdRows < 1 { // maybe another transaction has same time
|
||||||
|
err = userDataDb.RollbackToSavePoint(sess, insertTransactionSavePointName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[accounts.CreateAccounts] failed to rollback to save point \"%s\", because %s", insertTransactionSavePointName, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
sameSecondLatestTransaction := &models.Transaction{}
|
sameSecondLatestTransaction := &models.Transaction{}
|
||||||
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
||||||
maxTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
maxTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func (s *TransactionTemplateService) ModifyTemplate(c core.Context, template *mo
|
|||||||
template.UpdatedUnixTime = time.Now().Unix()
|
template.UpdatedUnixTime = time.Now().Unix()
|
||||||
|
|
||||||
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
||||||
updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "scheduled_frequency_type", "scheduled_frequency", "scheduled_at", "scheduled_timezone_utc_offset", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
|
updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "scheduled_frequency_type", "scheduled_frequency", "scheduled_start_time", "scheduled_end_time", "scheduled_at", "scheduled_timezone_utc_offset", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -252,8 +252,10 @@ func (s *TransactionService) CreateTransaction(c core.Context, transaction *mode
|
|||||||
UpdatedUnixTime: now,
|
UpdatedUnixTime: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.UserDataDB(transaction.Uid).DoTransaction(c, func(sess *xorm.Session) error {
|
userDataDb := s.UserDataDB(transaction.Uid)
|
||||||
return s.doCreateTransaction(sess, transaction, transactionTagIndexes, tagIds, pictureIds, pictureUpdateModel)
|
|
||||||
|
return userDataDb.DoTransaction(c, func(sess *xorm.Session) error {
|
||||||
|
return s.doCreateTransaction(c, userDataDb, sess, transaction, transactionTagIndexes, tagIds, pictureIds, pictureUpdateModel)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,12 +357,14 @@ func (s *TransactionService) BatchCreateTransactions(c core.Context, uid int64,
|
|||||||
allTransactionTagIds[transaction.TransactionId] = uniqueTagIds
|
allTransactionTagIds[transaction.TransactionId] = uniqueTagIds
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
|
userDataDb := s.UserDataDB(uid)
|
||||||
|
|
||||||
|
return userDataDb.DoTransaction(c, func(sess *xorm.Session) error {
|
||||||
for i := 0; i < len(transactions); i++ {
|
for i := 0; i < len(transactions); i++ {
|
||||||
transaction := transactions[i]
|
transaction := transactions[i]
|
||||||
transactionTagIndexes := allTransactionTagIndexes[transaction.TransactionId]
|
transactionTagIndexes := allTransactionTagIndexes[transaction.TransactionId]
|
||||||
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
|
transactionTagIds := allTransactionTagIds[transaction.TransactionId]
|
||||||
err := s.doCreateTransaction(sess, transaction, transactionTagIndexes, transactionTagIds, nil, nil)
|
err := s.doCreateTransaction(c, userDataDb, sess, transaction, transactionTagIndexes, transactionTagIds, nil, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
transactionUnixTime := utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime)
|
transactionUnixTime := utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime)
|
||||||
@@ -394,7 +398,7 @@ func (s *TransactionService) CreateScheduledTransactions(c core.Context, current
|
|||||||
|
|
||||||
for i := 0; i < s.UserDataDBCount(); i++ {
|
for i := 0; i < s.UserDataDBCount(); i++ {
|
||||||
var templates []*models.TransactionTemplate
|
var templates []*models.TransactionTemplate
|
||||||
err := s.UserDataDBByIndex(i).NewSession(c).Where("deleted=? AND template_type=? AND (scheduled_frequency_type=? OR scheduled_frequency_type=?) AND scheduled_at>=? AND scheduled_at<?", false, models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_WEEKLY, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_MONTHLY, minScheduledAt, maxScheduledAt).Find(&templates)
|
err := s.UserDataDBByIndex(i).NewSession(c).Where("deleted=? AND template_type=? AND (scheduled_frequency_type=? OR scheduled_frequency_type=?) AND (scheduled_start_time IS NULL OR scheduled_start_time<=?) AND (scheduled_end_time IS NULL OR scheduled_end_time>=?) AND scheduled_at>=? AND scheduled_at<?", false, models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_WEEKLY, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_MONTHLY, startTime.Unix(), startTime.Unix(), minScheduledAt, maxScheduledAt).Find(&templates)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -453,6 +457,18 @@ func (s *TransactionService) CreateScheduledTransactions(c core.Context, current
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if template.ScheduledStartTime != nil && *template.ScheduledStartTime > transactionUnixTime {
|
||||||
|
skipCount++
|
||||||
|
log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" does not need to create transaction, now is earlier than the start time %d", template.TemplateId, *template.ScheduledStartTime)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.ScheduledEndTime != nil && *template.ScheduledEndTime < transactionUnixTime {
|
||||||
|
skipCount++
|
||||||
|
log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" does not need to create transaction, now is later than the end time %d", template.TemplateId, *template.ScheduledEndTime)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var transactionDbType models.TransactionDbType
|
var transactionDbType models.TransactionDbType
|
||||||
|
|
||||||
if template.Type == models.TRANSACTION_TYPE_EXPENSE {
|
if template.Type == models.TRANSACTION_TYPE_EXPENSE {
|
||||||
@@ -546,6 +562,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
has, err := sess.ID(transaction.TransactionId).Where("uid=? AND deleted=?", transaction.Uid, false).Get(oldTransaction)
|
has, err := sess.ID(transaction.TransactionId).Where("uid=? AND deleted=?", transaction.Uid, false).Get(oldTransaction)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to get current transaction, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return errs.ErrTransactionNotFound
|
return errs.ErrTransactionNotFound
|
||||||
@@ -568,6 +585,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
sourceAccount, destinationAccount, err := s.getAccountModels(sess, transaction)
|
sourceAccount, destinationAccount, err := s.getAccountModels(sess, transaction)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to get account, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,6 +610,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
oldSourceAccount, oldDestinationAccount, err := s.getOldAccountModels(sess, transaction, oldTransaction, sourceAccount, destinationAccount)
|
oldSourceAccount, oldDestinationAccount, err := s.getOldAccountModels(sess, transaction, oldTransaction, sourceAccount, destinationAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to get old account, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,6 +644,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
has, err = sess.Where("uid=? AND deleted=? AND transaction_time>=? AND transaction_time<=?", transaction.Uid, false, minTransactionTime, maxTransactionTime).OrderBy("transaction_time desc").Limit(1).Get(sameSecondLatestTransaction)
|
has, err = sess.Where("uid=? AND deleted=? AND transaction_time>=? AND transaction_time<=?", transaction.Uid, false, minTransactionTime, maxTransactionTime).OrderBy("transaction_time desc").Limit(1).Get(sameSecondLatestTransaction)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to get trasaction time, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,6 +726,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to get whether other transactions exist, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if otherTransactionExists {
|
} else if otherTransactionExists {
|
||||||
return errs.ErrCannotAddTransactionBeforeBalanceModificationTransaction
|
return errs.ErrCannotAddTransactionBeforeBalanceModificationTransaction
|
||||||
@@ -716,6 +737,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(transaction.TransactionId).Cols(updateCols...).Where("uid=? AND deleted=?", transaction.Uid, false).Update(transaction)
|
updatedRows, err := sess.ID(transaction.TransactionId).Cols(updateCols...).Where("uid=? AND deleted=?", transaction.Uid, false).Update(transaction)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update transaction, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrTransactionNotFound
|
return errs.ErrTransactionNotFound
|
||||||
@@ -732,6 +754,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(relatedTransaction.TransactionId).Cols(relatedUpdateCols...).Where("uid=? AND deleted=?", relatedTransaction.Uid, false).Update(relatedTransaction)
|
updatedRows, err := sess.ID(relatedTransaction.TransactionId).Cols(relatedUpdateCols...).Where("uid=? AND deleted=?", relatedTransaction.Uid, false).Update(relatedTransaction)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update related transaction, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -748,6 +771,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
deletedRows, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, transaction.TransactionId).In("tag_id", removeTagIds).Update(tagIndexUpdateModel)
|
deletedRows, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, transaction.TransactionId).In("tag_id", removeTagIds).Update(tagIndexUpdateModel)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to remove old transaction tag index, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if deletedRows < 1 {
|
} else if deletedRows < 1 {
|
||||||
return errs.ErrTransactionTagNotFound
|
return errs.ErrTransactionTagNotFound
|
||||||
@@ -762,6 +786,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
_, err := sess.Insert(transactionTagIndex)
|
_, err := sess.Insert(transactionTagIndex)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to add new transaction tag index, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -773,6 +798,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
_, err := sess.Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, transaction.TransactionId).Update(tagIndexUpdateModel)
|
_, err := sess.Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, transaction.TransactionId).Update(tagIndexUpdateModel)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update transaction tag index, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -787,6 +813,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
deletedRows, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, transaction.TransactionId).In("picture_id", removePictureIds).Update(pictureUpdateModel)
|
deletedRows, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, transaction.TransactionId).In("picture_id", removePictureIds).Update(pictureUpdateModel)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to remove old transaction picture info, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if deletedRows < 1 {
|
} else if deletedRows < 1 {
|
||||||
return errs.ErrTransactionPictureNotFound
|
return errs.ErrTransactionPictureNotFound
|
||||||
@@ -802,6 +829,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
_, err = sess.Cols("transaction_id", "updated_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, models.TransactionPictureNewPictureTransactionId).In("picture_id", addPictureIds).Update(pictureUpdateModel)
|
_, err = sess.Cols("transaction_id", "updated_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, models.TransactionPictureNewPictureTransactionId).In("picture_id", addPictureIds).Update(pictureUpdateModel)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update new transaction picture info, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -817,6 +845,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)+(%d)", oldTransaction.RelatedAccountAmount, transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)+(%d)", oldTransaction.RelatedAccountAmount, transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -837,6 +866,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(oldSourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)+(%d)", oldTransaction.Amount, oldAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", oldSourceAccount.Uid, false).Update(oldSourceAccount)
|
updatedRows, err := sess.ID(oldSourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)+(%d)", oldTransaction.Amount, oldAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", oldSourceAccount.Uid, false).Update(oldSourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -848,6 +878,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", newAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", newAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -868,6 +899,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(oldSourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)-(%d)", oldTransaction.Amount, oldAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", oldSourceAccount.Uid, false).Update(oldSourceAccount)
|
updatedRows, err := sess.ID(oldSourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)-(%d)", oldTransaction.Amount, oldAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", oldSourceAccount.Uid, false).Update(oldSourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -879,6 +911,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", newAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", newAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -899,6 +932,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(oldSourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)-(%d)", oldTransaction.Amount, oldSourceAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", oldSourceAccount.Uid, false).Update(oldSourceAccount)
|
updatedRows, err := sess.ID(oldSourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)-(%d)", oldTransaction.Amount, oldSourceAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", oldSourceAccount.Uid, false).Update(oldSourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -910,6 +944,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", newSourceAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", newSourceAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -930,6 +965,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(oldDestinationAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)+(%d)", oldTransaction.RelatedAccountAmount, oldDestinationAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", oldDestinationAccount.Uid, false).Update(oldDestinationAccount)
|
updatedRows, err := sess.ID(oldDestinationAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)+(%d)", oldTransaction.RelatedAccountAmount, oldDestinationAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", oldDestinationAccount.Uid, false).Update(oldDestinationAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -941,6 +977,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
|
|||||||
updatedRows, err := sess.ID(destinationAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", newDestinationAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", destinationAccount.Uid, false).Update(destinationAccount)
|
updatedRows, err := sess.ID(destinationAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", newDestinationAccountNewAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", destinationAccount.Uid, false).Update(destinationAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.ModifyTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -1541,7 +1578,7 @@ func (s *TransactionService) GetTransactionIds(transactions []*models.Transactio
|
|||||||
return transactionIds
|
return transactionIds
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction *models.Transaction, transactionTagIndexes []*models.TransactionTagIndex, tagIds []int64, pictureIds []int64, pictureUpdateModel *models.TransactionPictureInfo) error {
|
func (s *TransactionService) doCreateTransaction(c core.Context, database *datastore.Database, sess *xorm.Session, transaction *models.Transaction, transactionTagIndexes []*models.TransactionTagIndex, tagIds []int64, pictureIds []int64, pictureUpdateModel *models.TransactionPictureInfo) error {
|
||||||
// Get and verify source and destination account
|
// Get and verify source and destination account
|
||||||
sourceAccount, destinationAccount, err := s.getAccountModels(sess, transaction)
|
sourceAccount, destinationAccount, err := s.getAccountModels(sess, transaction)
|
||||||
|
|
||||||
@@ -1593,6 +1630,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
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{})
|
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 {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to get whether other transactions exist, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if otherTransactionExists {
|
} else if otherTransactionExists {
|
||||||
return errs.ErrBalanceModificationTransactionCannotAddWhenNotEmpty
|
return errs.ErrBalanceModificationTransactionCannotAddWhenNotEmpty
|
||||||
@@ -1610,6 +1648,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to get whether other transactions exist, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if otherTransactionExists {
|
} else if otherTransactionExists {
|
||||||
return errs.ErrCannotAddTransactionBeforeBalanceModificationTransaction
|
return errs.ErrCannotAddTransactionBeforeBalanceModificationTransaction
|
||||||
@@ -1623,9 +1662,30 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
relatedTransaction = s.GetRelatedTransferTransaction(transaction)
|
relatedTransaction = s.GetRelatedTransferTransaction(transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertTransactionSavePointName := "insert_transaction"
|
||||||
|
err = database.SetSavePoint(sess, insertTransactionSavePointName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to set save point \"%s\", because %s", insertTransactionSavePointName, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
createdRows, err := sess.Insert(transaction)
|
createdRows, err := sess.Insert(transaction)
|
||||||
|
|
||||||
if err != nil || createdRows < 1 { // maybe another transaction has same time
|
if err != nil || createdRows < 1 { // maybe another transaction has same time
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[transactions.doCreateTransaction] cannot create trasaction, because %s, regenerate transaction time value", err.Error())
|
||||||
|
} else {
|
||||||
|
log.Warnf(c, "[transactions.doCreateTransaction] cannot create trasaction, regenerate transaction time value")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = database.RollbackToSavePoint(sess, insertTransactionSavePointName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to rollback to save point \"%s\", because %s", insertTransactionSavePointName, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
sameSecondLatestTransaction := &models.Transaction{}
|
sameSecondLatestTransaction := &models.Transaction{}
|
||||||
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
||||||
maxTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
maxTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime))
|
||||||
@@ -1633,6 +1693,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
has, err := sess.Where("uid=? AND transaction_time>=? AND transaction_time<=?", transaction.Uid, minTransactionTime, maxTransactionTime).OrderBy("transaction_time desc").Limit(1).Get(sameSecondLatestTransaction)
|
has, err := sess.Where("uid=? AND transaction_time>=? AND transaction_time<=?", transaction.Uid, minTransactionTime, maxTransactionTime).OrderBy("transaction_time desc").Limit(1).Get(sameSecondLatestTransaction)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to get trasaction time, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -1644,6 +1705,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
createdRows, err := sess.Insert(transaction)
|
createdRows, err := sess.Insert(transaction)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to add transaction again, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if createdRows < 1 {
|
} else if createdRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -1660,6 +1722,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
createdRows, err := sess.Insert(relatedTransaction)
|
createdRows, err := sess.Insert(relatedTransaction)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to add related transaction, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if createdRows < 1 {
|
} else if createdRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -1677,6 +1740,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
_, err := sess.Insert(transactionTagIndex)
|
_, err := sess.Insert(transactionTagIndex)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to add transaction tag index, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1687,6 +1751,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
_, err = sess.Cols("transaction_id", "updated_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, models.TransactionPictureNewPictureTransactionId).In("picture_id", pictureIds).Update(pictureUpdateModel)
|
_, err = sess.Cols("transaction_id", "updated_unix_time").Where("uid=? AND deleted=? AND transaction_id=?", transaction.Uid, false, models.TransactionPictureNewPictureTransactionId).In("picture_id", pictureIds).Update(pictureUpdateModel)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to update transaction picture info, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1697,6 +1762,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -1706,6 +1772,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -1715,6 +1782,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedRows < 1 {
|
} else if updatedRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -1724,6 +1792,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
updatedSourceRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
updatedSourceRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)", transaction.Amount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedSourceRows < 1 {
|
} else if updatedSourceRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -1733,6 +1802,7 @@ func (s *TransactionService) doCreateTransaction(sess *xorm.Session, transaction
|
|||||||
updatedDestinationRows, err := sess.ID(destinationAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", destinationAccount.Uid, false).Update(destinationAccount)
|
updatedDestinationRows, err := sess.ID(destinationAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance+(%d)", transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", destinationAccount.Uid, false).Update(destinationAccount)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf(c, "[transactions.doCreateTransaction] failed to update account balance, because %s", err.Error())
|
||||||
return err
|
return err
|
||||||
} else if updatedDestinationRows < 1 {
|
} else if updatedDestinationRows < 1 {
|
||||||
return errs.ErrDatabaseOperationFailed
|
return errs.ErrDatabaseOperationFailed
|
||||||
@@ -1913,7 +1983,7 @@ func (s *TransactionService) appendFilterTagIdsConditionToQuery(sess *xorm.Sessi
|
|||||||
|
|
||||||
if noTags {
|
if noTags {
|
||||||
subQuery := builder.Select("transaction_id").From("transaction_tag_index").Where(subQueryCondition)
|
subQuery := builder.Select("transaction_id").From("transaction_tag_index").Where(subQueryCondition)
|
||||||
sess.NotIn("transaction_id", subQuery)
|
sess.NotIn("transaction_id", subQuery).NotIn("related_id", subQuery)
|
||||||
return sess
|
return sess
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1929,9 +1999,9 @@ func (s *TransactionService) appendFilterTagIdsConditionToQuery(sess *xorm.Sessi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tagFilterType == models.TRANSACTION_TAG_FILTER_HAS_ANY || tagFilterType == models.TRANSACTION_TAG_FILTER_HAS_ALL {
|
if tagFilterType == models.TRANSACTION_TAG_FILTER_HAS_ANY || tagFilterType == models.TRANSACTION_TAG_FILTER_HAS_ALL {
|
||||||
sess.In("transaction_id", subQuery)
|
sess.And(builder.Or(builder.In("transaction_id", subQuery), builder.In("related_id", subQuery)))
|
||||||
} else if tagFilterType == models.TRANSACTION_TAG_FILTER_NOT_HAS_ANY || tagFilterType == models.TRANSACTION_TAG_FILTER_NOT_HAS_ALL {
|
} else if tagFilterType == models.TRANSACTION_TAG_FILTER_NOT_HAS_ANY || tagFilterType == models.TRANSACTION_TAG_FILTER_NOT_HAS_ALL {
|
||||||
sess.NotIn("transaction_id", subQuery)
|
sess.NotIn("transaction_id", subQuery).NotIn("related_id", subQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sess
|
return sess
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetUserByUsernameOrEmailAndPassword returns the user model according to login name and password
|
// GetUserByUsernameOrEmailAndPassword returns the user model according to login name and password
|
||||||
func (s *UserService) GetUserByUsernameOrEmailAndPassword(c core.Context, loginname string, password string) (*models.User, error) {
|
func (s *UserService) GetUserByUsernameOrEmailAndPassword(c core.Context, loginname string, password string) (*models.User, int64, error) {
|
||||||
var user *models.User
|
var user *models.User
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -71,14 +71,18 @@ func (s *UserService) GetUserByUsernameOrEmailAndPassword(c core.Context, loginn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
return nil, 0, errs.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.IsPasswordEqualsUserPassword(password, user) {
|
if !s.IsPasswordEqualsUserPassword(password, user) {
|
||||||
return nil, errs.ErrUserPasswordWrong
|
return nil, user.Uid, errs.ErrUserPasswordWrong
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, user.Uid, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserById returns the user model according to user uid
|
// GetUserById returns the user model according to user uid
|
||||||
@@ -301,7 +305,7 @@ func (s *UserService) UpdateUser(c core.Context, user *models.User, modifyUserLa
|
|||||||
updateCols = append(updateCols, "short_time_format")
|
updateCols = append(updateCols, "short_time_format")
|
||||||
}
|
}
|
||||||
|
|
||||||
if core.DECIMAL_SEPARATOR_DEFAULT <= user.DecimalSeparator && user.DecimalSeparator <= core.DECIMAL_SEPARATOR_SPACE {
|
if core.DECIMAL_SEPARATOR_DEFAULT <= user.DecimalSeparator && user.DecimalSeparator <= core.DECIMAL_SEPARATOR_COMMA {
|
||||||
updateCols = append(updateCols, "decimal_separator")
|
updateCols = append(updateCols, "decimal_separator")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ const (
|
|||||||
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
|
defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes
|
||||||
defaultEmailVerifyTokenExpiredTime uint32 = 3600 // 60 minutes
|
defaultEmailVerifyTokenExpiredTime uint32 = 3600 // 60 minutes
|
||||||
defaultPasswordResetTokenExpiredTime uint32 = 3600 // 60 minutes
|
defaultPasswordResetTokenExpiredTime uint32 = 3600 // 60 minutes
|
||||||
|
defaultMaxFailuresPerIpPerMinute uint32 = 5
|
||||||
|
defaultMaxFailuresPerUserPerMinute uint32 = 5
|
||||||
|
|
||||||
defaultTransactionPictureFileMaxSize uint32 = 10485760 // 10MB
|
defaultTransactionPictureFileMaxSize uint32 = 10485760 // 10MB
|
||||||
defaultUserAvatarFileMaxSize uint32 = 1048576 // 1MB
|
defaultUserAvatarFileMaxSize uint32 = 1048576 // 1MB
|
||||||
@@ -286,6 +288,8 @@ type Config struct {
|
|||||||
EmailVerifyTokenExpiredTimeDuration time.Duration
|
EmailVerifyTokenExpiredTimeDuration time.Duration
|
||||||
PasswordResetTokenExpiredTime uint32
|
PasswordResetTokenExpiredTime uint32
|
||||||
PasswordResetTokenExpiredTimeDuration time.Duration
|
PasswordResetTokenExpiredTimeDuration time.Duration
|
||||||
|
MaxFailuresPerIpPerMinute uint32
|
||||||
|
MaxFailuresPerUserPerMinute uint32
|
||||||
EnableRequestIdHeader bool
|
EnableRequestIdHeader bool
|
||||||
|
|
||||||
// User
|
// User
|
||||||
@@ -768,6 +772,9 @@ func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName
|
|||||||
|
|
||||||
config.PasswordResetTokenExpiredTimeDuration = time.Duration(config.PasswordResetTokenExpiredTime) * time.Second
|
config.PasswordResetTokenExpiredTimeDuration = time.Duration(config.PasswordResetTokenExpiredTime) * time.Second
|
||||||
|
|
||||||
|
config.MaxFailuresPerIpPerMinute = getConfigItemUint32Value(configFile, sectionName, "max_failures_per_ip_per_minute", defaultMaxFailuresPerIpPerMinute)
|
||||||
|
config.MaxFailuresPerUserPerMinute = getConfigItemUint32Value(configFile, sectionName, "max_failures_per_user_per_minute", defaultMaxFailuresPerUserPerMinute)
|
||||||
|
|
||||||
config.EnableRequestIdHeader = getConfigItemBoolValue(configFile, sectionName, "request_id_header", true)
|
config.EnableRequestIdHeader = getConfigItemBoolValue(configFile, sectionName, "request_id_header", true)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
longDateFormat = "2006-01-02"
|
||||||
longDateTimeFormat = "2006-01-02 15:04:05"
|
longDateTimeFormat = "2006-01-02 15:04:05"
|
||||||
longDateTimeWithTimezoneFormat = "2006-01-02 15:04:05Z07:00"
|
longDateTimeWithTimezoneFormat = "2006-01-02 15:04:05Z07:00"
|
||||||
longDateTimeWithTimezoneFormat2 = "2006-01-02 15:04:05 Z0700"
|
longDateTimeWithTimezoneFormat2 = "2006-01-02 15:04:05 Z0700"
|
||||||
@@ -42,6 +43,17 @@ func ParseNumericYearMonth(yearMonth string) (int32, int32, error) {
|
|||||||
return year, month, nil
|
return year, month, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormatUnixTimeToLongDate returns a textual representation of the unix time formatted by long date time format
|
||||||
|
func FormatUnixTimeToLongDate(unixTime int64, timezone *time.Location) string {
|
||||||
|
t := parseFromUnixTime(unixTime)
|
||||||
|
|
||||||
|
if timezone != nil {
|
||||||
|
t = t.In(timezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Format(longDateFormat)
|
||||||
|
}
|
||||||
|
|
||||||
// FormatUnixTimeToLongDateTime returns a textual representation of the unix time formatted by long date time format
|
// FormatUnixTimeToLongDateTime returns a textual representation of the unix time formatted by long date time format
|
||||||
func FormatUnixTimeToLongDateTime(unixTime int64, timezone *time.Location) string {
|
func FormatUnixTimeToLongDateTime(unixTime int64, timezone *time.Location) string {
|
||||||
t := parseFromUnixTime(unixTime)
|
t := parseFromUnixTime(unixTime)
|
||||||
@@ -119,6 +131,24 @@ func GetMaxUnixTimeWithSameLocalDateTime(unixTime int64, currentUtcOffset int16)
|
|||||||
return unixTime + int64(currentUtcOffset)*60 - westernmostTimezoneUtcOffset*60
|
return unixTime + int64(currentUtcOffset)*60 - westernmostTimezoneUtcOffset*60
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseFromLongDateFirstTime parses a formatted string in long date format
|
||||||
|
func ParseFromLongDateFirstTime(t string, utcOffset int16) (time.Time, error) {
|
||||||
|
timezone := time.FixedZone("Timezone", int(utcOffset)*60)
|
||||||
|
return time.ParseInLocation(longDateFormat, t, timezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFromLongDateLastTime parses a formatted string in long date format
|
||||||
|
func ParseFromLongDateLastTime(t string, utcOffset int16) (time.Time, error) {
|
||||||
|
timezone := time.FixedZone("Timezone", int(utcOffset)*60)
|
||||||
|
lastTime, err := time.ParseInLocation(longDateFormat, t, timezone)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return lastTime, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastTime.Add(24 * time.Hour).Add(-1 * time.Nanosecond), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ParseFromLongDateTimeToMinUnixTime parses a formatted string in long date time format to minimal unix time (the westernmost timezone)
|
// ParseFromLongDateTimeToMinUnixTime parses a formatted string in long date time format to minimal unix time (the westernmost timezone)
|
||||||
func ParseFromLongDateTimeToMinUnixTime(t string) (time.Time, error) {
|
func ParseFromLongDateTimeToMinUnixTime(t string) (time.Time, error) {
|
||||||
timezone := time.FixedZone("Timezone", easternmostTimezoneUtcOffset*60)
|
timezone := time.FixedZone("Timezone", easternmostTimezoneUtcOffset*60)
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ func TestParseNumericYearMonth(t *testing.T) {
|
|||||||
assert.Equal(t, expectedMonth, actualMonth)
|
assert.Equal(t, expectedMonth, actualMonth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFormatUnixTimeToLongDate(t *testing.T) {
|
||||||
|
unixTime := int64(1617228083)
|
||||||
|
utcTimezone := time.FixedZone("Test Timezone", 0) // UTC
|
||||||
|
utc8Timezone := time.FixedZone("Test Timezone", 28800) // UTC+8
|
||||||
|
|
||||||
|
expectedValue := "2021-03-31"
|
||||||
|
actualValue := FormatUnixTimeToLongDate(unixTime, utcTimezone)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
|
||||||
|
expectedValue = "2021-04-01"
|
||||||
|
actualValue = FormatUnixTimeToLongDate(unixTime, utc8Timezone)
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFormatUnixTimeToLongDateTime(t *testing.T) {
|
func TestFormatUnixTimeToLongDateTime(t *testing.T) {
|
||||||
unixTime := int64(1617228083)
|
unixTime := int64(1617228083)
|
||||||
utcTimezone := time.FixedZone("Test Timezone", 0) // UTC
|
utcTimezone := time.FixedZone("Test Timezone", 0) // UTC
|
||||||
@@ -106,6 +120,24 @@ func TestGetMaxUnixTimeWithSameLocalDateTime(t *testing.T) {
|
|||||||
assert.Equal(t, expectedValue, actualValue)
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseFromLongDateFirstTime(t *testing.T) {
|
||||||
|
expectedValue := int64(1690819200)
|
||||||
|
actualTime, err := ParseFromLongDateFirstTime("2023-08-01", 480)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
actualValue := actualTime.Unix()
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFromLongDateLastTime(t *testing.T) {
|
||||||
|
expectedValue := int64(1690905599)
|
||||||
|
actualTime, err := ParseFromLongDateLastTime("2023-08-01", 480)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
actualValue := actualTime.Unix()
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseFromLongDateTimeToMinUnixTime(t *testing.T) {
|
func TestParseFromLongDateTimeToMinUnixTime(t *testing.T) {
|
||||||
expectedValue := int64(1690797600)
|
expectedValue := int64(1690797600)
|
||||||
actualTime, err := ParseFromLongDateTimeToMinUnixTime("2023-08-01 00:00:00")
|
actualTime, err := ParseFromLongDateTimeToMinUnixTime("2023-08-01 00:00:00")
|
||||||
|
|||||||
+92
-94
@@ -4,10 +4,10 @@
|
|||||||
</v-app>
|
</v-app>
|
||||||
<v-snackbar class="cursor-pointer" color="notification-background" location="top"
|
<v-snackbar class="cursor-pointer" color="notification-background" location="top"
|
||||||
:multi-line="true" :timeout="-1" :close-on-content-click="true" v-model="showNotification">
|
:multi-line="true" :timeout="-1" :close-on-content-click="true" v-model="showNotification">
|
||||||
<v-tooltip activator="parent">{{ $t('Click to close') }}</v-tooltip>
|
<v-tooltip activator="parent">{{ tt('Click to close') }}</v-tooltip>
|
||||||
<div class="d-inline-flex">
|
<div class="d-inline-flex">
|
||||||
<img alt="logo" class="notification-logo" :src="ezBookkeepingLogoPath" />
|
<img alt="logo" class="notification-logo" :src="APPLICATION_LOGO_PATH" />
|
||||||
<span class="ml-2">{{ $t('global.app.title') }}</span>
|
<span class="ml-2">{{ tt('global.app.title') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ currentNotificationContent }}
|
{{ currentNotificationContent }}
|
||||||
@@ -15,106 +15,104 @@
|
|||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
|
|
||||||
import { useTheme } from 'vuetify';
|
import { useTheme } from 'vuetify';
|
||||||
import { register } from 'register-service-worker';
|
import { register } from 'register-service-worker';
|
||||||
|
|
||||||
import { mapStores } from 'pinia';
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
import { useRootStore } from '@/stores/index.js';
|
|
||||||
import { useSettingsStore } from '@/stores/setting.js';
|
|
||||||
import { useUserStore } from '@/stores/user.js';
|
|
||||||
import { useTokensStore } from '@/stores/token.js';
|
|
||||||
import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
|
|
||||||
|
|
||||||
import assetConstants from '@/consts/asset.js';
|
import { useRootStore } from '@/stores/index.ts';
|
||||||
import { isProduction } from '@/lib/version.js';
|
import { useSettingsStore } from '@/stores/setting.ts';
|
||||||
import { loadMapAssets } from '@/lib/map/index.js';
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
import { getSystemTheme, setExpenseAndIncomeAmountColor } from '@/lib/ui.js';
|
import { useTokensStore } from '@/stores/token.ts';
|
||||||
|
import { useExchangeRatesStore } from '@/stores/exchangeRates.ts';
|
||||||
|
|
||||||
export default {
|
import { APPLICATION_LOGO_PATH } from '@/consts/asset.ts';
|
||||||
data() {
|
import { ThemeType } from '@/core/theme.ts';
|
||||||
return {
|
import { isProduction } from '@/lib/version.ts';
|
||||||
showNotification: false
|
import { initMapProvider } from '@/lib/map/index.ts';
|
||||||
}
|
import { isUserLogined, isUserUnlocked } from '@/lib/userstate.ts';
|
||||||
},
|
import { getSystemTheme, setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
|
||||||
computed: {
|
|
||||||
...mapStores(useRootStore, useSettingsStore, useUserStore, useTokensStore, useExchangeRatesStore),
|
|
||||||
ezBookkeepingLogoPath() {
|
|
||||||
return assetConstants.ezBookkeepingLogoPath;
|
|
||||||
},
|
|
||||||
currentNotificationContent() {
|
|
||||||
return this.rootStore.currentNotification;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
currentNotificationContent: function (newValue) {
|
|
||||||
this.showNotification = !!newValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
const self = this;
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
if (self.settingsStore.appSettings.theme === 'light') {
|
const { tt, getCurrentLanguageInfo, setLanguage, initLocale } = useI18n();
|
||||||
theme.global.name.value = 'light';
|
|
||||||
} else if (self.settingsStore.appSettings.theme === 'dark') {
|
const theme = useTheme();
|
||||||
theme.global.name.value = 'dark';
|
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const tokensStore = useTokensStore();
|
||||||
|
const exchangeRatesStore = useExchangeRatesStore();
|
||||||
|
|
||||||
|
const showNotification = ref<boolean>(false);
|
||||||
|
|
||||||
|
const currentNotificationContent = computed<string | null>(() => rootStore.currentNotification);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const languageInfo = getCurrentLanguageInfo();
|
||||||
|
initMapProvider(languageInfo?.alternativeLanguageTag);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(currentNotificationContent, (newValue) => {
|
||||||
|
showNotification.value = !!newValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (settingsStore.appSettings.theme === ThemeType.Light) {
|
||||||
|
theme.global.name.value = ThemeType.Light;
|
||||||
|
} else if (settingsStore.appSettings.theme === ThemeType.Dark) {
|
||||||
|
theme.global.name.value = ThemeType.Dark;
|
||||||
|
} else {
|
||||||
|
theme.global.name.value = getSystemTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
|
||||||
|
if (settingsStore.appSettings.theme === 'auto') {
|
||||||
|
if (e.matches) {
|
||||||
|
theme.global.name.value = ThemeType.Dark;
|
||||||
} else {
|
} else {
|
||||||
theme.global.name.value = getSystemTheme();
|
theme.global.name.value = ThemeType.Light;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
|
|
||||||
if (self.settingsStore.appSettings.theme === 'auto') {
|
|
||||||
if (e.matches) {
|
|
||||||
theme.global.name.value = 'dark';
|
|
||||||
} else {
|
|
||||||
theme.global.name.value = 'light';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let localeDefaultSettings = self.$locale.initLocale(self.userStore.currentUserLanguage, self.settingsStore.appSettings.timeZone);
|
|
||||||
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
|
||||||
|
|
||||||
setExpenseAndIncomeAmountColor(self.userStore.currentUserExpenseAmountColor, self.userStore.currentUserIncomeAmountColor);
|
|
||||||
|
|
||||||
if (self.$user.isUserLogined()) {
|
|
||||||
if (!self.settingsStore.appSettings.applicationLock || self.$user.isUserUnlocked()) {
|
|
||||||
// refresh token if user is logined
|
|
||||||
self.tokensStore.refreshTokenAndRevokeOldToken().then(response => {
|
|
||||||
if (response.user) {
|
|
||||||
localeDefaultSettings = self.$locale.setLanguage(response.user.language);
|
|
||||||
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
|
||||||
|
|
||||||
setExpenseAndIncomeAmountColor(response.user.expenseAmountColor, response.user.incomeAmountColor);
|
|
||||||
|
|
||||||
if (response.notificationContent) {
|
|
||||||
self.rootStore.setNotificationContent(response.notificationContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// auto refresh exchange rates data
|
|
||||||
if (self.settingsStore.appSettings.autoUpdateExchangeRatesData) {
|
|
||||||
self.exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isProduction()) {
|
|
||||||
register('./sw.js', {
|
|
||||||
registrationOptions: {
|
|
||||||
scope: './'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const languageInfo = this.$locale.getCurrentLanguageInfo();
|
|
||||||
loadMapAssets(languageInfo ? languageInfo.alternativeLanguageTag : null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let localeDefaultSettings = initLocale(userStore.currentUserLanguage, settingsStore.appSettings.timeZone);
|
||||||
|
settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||||
|
|
||||||
|
setExpenseAndIncomeAmountColor(userStore.currentUserExpenseAmountColor, userStore.currentUserIncomeAmountColor);
|
||||||
|
|
||||||
|
if (isUserLogined()) {
|
||||||
|
if (!settingsStore.appSettings.applicationLock || isUserUnlocked()) {
|
||||||
|
// refresh token if user is logined
|
||||||
|
tokensStore.refreshTokenAndRevokeOldToken().then(response => {
|
||||||
|
if (response.user) {
|
||||||
|
localeDefaultSettings = setLanguage(response.user.language);
|
||||||
|
settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||||
|
|
||||||
|
setExpenseAndIncomeAmountColor(response.user.expenseAmountColor, response.user.incomeAmountColor);
|
||||||
|
|
||||||
|
if (response.notificationContent) {
|
||||||
|
rootStore.setNotificationContent(response.notificationContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// auto refresh exchange rates data
|
||||||
|
if (settingsStore.appSettings.autoUpdateExchangeRatesData) {
|
||||||
|
exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isProduction()) {
|
||||||
|
register('./sw.js', {
|
||||||
|
registrationOptions: {
|
||||||
|
scope: './'
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+214
-208
@@ -4,236 +4,242 @@
|
|||||||
</f7-app>
|
</f7-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
|
|
||||||
|
import type { Framework7Parameters, Notification, Actions, Dialog, Popover, Popup, Sheet } from 'framework7/types';
|
||||||
import { f7ready } from 'framework7-vue';
|
import { f7ready } from 'framework7-vue';
|
||||||
import routes from './router/mobile.js';
|
import routes from './router/mobile.ts';
|
||||||
|
|
||||||
import { mapStores } from 'pinia';
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
import { useRootStore } from '@/stores/index.js';
|
|
||||||
import { useSettingsStore } from '@/stores/setting.js';
|
|
||||||
import { useUserStore } from '@/stores/user.js';
|
|
||||||
import { useTokensStore } from '@/stores/token.js';
|
|
||||||
import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
|
|
||||||
|
|
||||||
import assetConstants from '@/consts/asset.js';
|
import { useRootStore } from '@/stores/index.ts';
|
||||||
import { isProduction } from '@/lib/version.js';
|
import { useSettingsStore } from '@/stores/setting.ts';
|
||||||
import { getTheme, isEnableAnimate } from '@/lib/settings.js';
|
import { useEnvironmentsStore } from '@/stores/environment.ts';
|
||||||
import { loadMapAssets } from '@/lib/map/index.js';
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
import { setExpenseAndIncomeAmountColor } from '@/lib/ui.js';
|
import { useTokensStore } from '@/stores/token.ts';
|
||||||
import { isModalShowing, setAppFontSize } from '@/lib/ui.mobile.js';
|
import { useExchangeRatesStore } from '@/stores/exchangeRates.ts';
|
||||||
|
|
||||||
export default {
|
import { APPLICATION_LOGO_PATH } from '@/consts/asset.ts';
|
||||||
data() {
|
import { ThemeType } from '@/core/theme.ts';
|
||||||
const self = this;
|
import { isProduction } from '@/lib/version.ts';
|
||||||
let darkMode = 'auto';
|
import { getTheme, isEnableAnimate } from '@/lib/settings.ts';
|
||||||
|
import { initMapProvider } from '@/lib/map/index.ts';
|
||||||
|
import { isUserLogined, isUserUnlocked } from '@/lib/userstate.ts';
|
||||||
|
import { setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
|
||||||
|
import { isModalShowing, setAppFontSize } from '@/lib/ui/mobile.ts';
|
||||||
|
|
||||||
if (getTheme() === 'light') {
|
const { tt, getCurrentLanguageInfo, setLanguage, initLocale } = useI18n();
|
||||||
|
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const environmentsStore = useEnvironmentsStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const tokensStore = useTokensStore();
|
||||||
|
const exchangeRatesStore = useExchangeRatesStore();
|
||||||
|
|
||||||
|
const f7params = ref<Framework7Parameters>({
|
||||||
|
name: 'ezBookkeeping',
|
||||||
|
theme: 'ios',
|
||||||
|
colors: {
|
||||||
|
primary: '#c67e48'
|
||||||
|
},
|
||||||
|
routes: routes,
|
||||||
|
darkMode: (() => {
|
||||||
|
let darkMode: boolean | string = 'auto';
|
||||||
|
|
||||||
|
if (getTheme() === ThemeType.Light) {
|
||||||
darkMode = false;
|
darkMode = false;
|
||||||
} else if (getTheme() === 'dark') {
|
} else if (getTheme() === ThemeType.Dark) {
|
||||||
darkMode = true;
|
darkMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return darkMode;
|
||||||
notification: null,
|
})(),
|
||||||
f7params: {
|
touch: {
|
||||||
name: 'ezBookkeeping',
|
disableContextMenu: true,
|
||||||
theme: 'ios',
|
tapHold: true
|
||||||
colors: {
|
|
||||||
primary: '#c67e48'
|
|
||||||
},
|
|
||||||
routes: routes,
|
|
||||||
darkMode: darkMode,
|
|
||||||
touch: {
|
|
||||||
disableContextMenu: true,
|
|
||||||
tapHold: true
|
|
||||||
},
|
|
||||||
serviceWorker: {
|
|
||||||
path: isProduction() ? './sw.js' : undefined,
|
|
||||||
scope: './',
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
animate: isEnableAnimate(),
|
|
||||||
backdrop: true,
|
|
||||||
closeOnEscape: true
|
|
||||||
},
|
|
||||||
dialog: {
|
|
||||||
animate: isEnableAnimate(),
|
|
||||||
backdrop: true
|
|
||||||
},
|
|
||||||
popover: {
|
|
||||||
animate: isEnableAnimate(),
|
|
||||||
backdrop: true,
|
|
||||||
closeOnEscape: true
|
|
||||||
},
|
|
||||||
popup: {
|
|
||||||
animate: isEnableAnimate(),
|
|
||||||
backdrop: true,
|
|
||||||
closeOnEscape: true,
|
|
||||||
swipeToClose: true
|
|
||||||
},
|
|
||||||
sheet: {
|
|
||||||
animate: isEnableAnimate(),
|
|
||||||
backdrop: true,
|
|
||||||
closeOnEscape: true
|
|
||||||
},
|
|
||||||
smartSelect: {
|
|
||||||
routableModals: false
|
|
||||||
},
|
|
||||||
view: {
|
|
||||||
animate: isEnableAnimate(),
|
|
||||||
browserHistory: !self.isiOSHomeScreenMode(),
|
|
||||||
browserHistoryInitialMatch: true,
|
|
||||||
browserHistoryAnimate: false,
|
|
||||||
iosSwipeBackAnimateShadow: false,
|
|
||||||
mdSwipeBackAnimateShadow: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isDarkMode: undefined,
|
|
||||||
hasPushPopupBackdrop: undefined,
|
|
||||||
hasBackdrop: undefined
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
serviceWorker: {
|
||||||
...mapStores(useRootStore, useSettingsStore, useUserStore, useTokensStore, useExchangeRatesStore),
|
path: isProduction() ? './sw.js' : undefined,
|
||||||
currentNotificationContent() {
|
scope: './',
|
||||||
return this.rootStore.currentNotification;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
actions: {
|
||||||
currentNotificationContent: function (newValue) {
|
animate: isEnableAnimate(),
|
||||||
const self = this;
|
backdrop: true,
|
||||||
|
closeOnEscape: true
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
// @ts-expect-error there is an "animate" field in dialog parameters, but it is not declared in the type definition file
|
||||||
|
animate: isEnableAnimate(),
|
||||||
|
backdrop: true
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
animate: isEnableAnimate(),
|
||||||
|
backdrop: true,
|
||||||
|
closeOnEscape: true
|
||||||
|
},
|
||||||
|
popup: {
|
||||||
|
animate: isEnableAnimate(),
|
||||||
|
backdrop: true,
|
||||||
|
closeOnEscape: true,
|
||||||
|
swipeToClose: true
|
||||||
|
},
|
||||||
|
sheet: {
|
||||||
|
animate: isEnableAnimate(),
|
||||||
|
backdrop: true,
|
||||||
|
closeOnEscape: true
|
||||||
|
},
|
||||||
|
smartSelect: {
|
||||||
|
routableModals: false
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
animate: isEnableAnimate(),
|
||||||
|
browserHistory: !isiOSHomeScreenMode(),
|
||||||
|
browserHistoryInitialMatch: true,
|
||||||
|
browserHistoryAnimate: false,
|
||||||
|
iosSwipeBackAnimateShadow: false,
|
||||||
|
mdSwipeBackAnimateShadow: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (self.notification) {
|
const notification = ref<Notification.Notification | null>(null);
|
||||||
self.notification.close();
|
|
||||||
self.notification.destroy();
|
const hasPushPopupBackdrop = ref<boolean | undefined>(undefined);
|
||||||
self.notification = null;
|
const hasBackdrop = ref<boolean | undefined>(undefined);
|
||||||
|
const currentNotificationContent = computed<string | null>(() => rootStore.currentNotification);
|
||||||
|
|
||||||
|
function isiOSHomeScreenMode(): boolean {
|
||||||
|
if ((/iphone|ipod|ipad/gi).test(navigator.platform) && (/Safari/i).test(navigator.appVersion) &&
|
||||||
|
window.matchMedia && window.matchMedia('(display-mode: standalone)').matches
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setThemeColorMeta(darkMode: boolean | undefined): void {
|
||||||
|
if (hasPushPopupBackdrop.value) {
|
||||||
|
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#000');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (darkMode) {
|
||||||
|
if (hasBackdrop.value) {
|
||||||
|
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#0b0b0b');
|
||||||
|
} else {
|
||||||
|
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#121212');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasBackdrop.value) {
|
||||||
|
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#949495');
|
||||||
|
} else {
|
||||||
|
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#f6f6f8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBackdropChanged(element: { push?: boolean, opened?: boolean }): void {
|
||||||
|
if (element.push) {
|
||||||
|
hasPushPopupBackdrop.value = element.opened;
|
||||||
|
} else {
|
||||||
|
hasBackdrop.value = element.opened;
|
||||||
|
}
|
||||||
|
|
||||||
|
setThemeColorMeta(environmentsStore.framework7DarkMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setAppFontSize(settingsStore.appSettings.fontSize);
|
||||||
|
|
||||||
|
f7ready((f7) => {
|
||||||
|
environmentsStore.framework7DarkMode = f7.darkMode;
|
||||||
|
setThemeColorMeta(f7.darkMode);
|
||||||
|
|
||||||
|
f7.on('actionsOpen', (actions: Actions.Actions) => onBackdropChanged(actions));
|
||||||
|
f7.on('actionsClose', (actions: Actions.Actions) => onBackdropChanged(actions));
|
||||||
|
f7.on('dialogOpen', (dialog: Dialog.Dialog) => onBackdropChanged(dialog));
|
||||||
|
f7.on('dialogClose', (dialog: Dialog.Dialog) => onBackdropChanged(dialog));
|
||||||
|
f7.on('popoverOpen', (popover: Popover.Popover) => onBackdropChanged(popover));
|
||||||
|
f7.on('popoverClose', (popover: Popover.Popover) => onBackdropChanged(popover));
|
||||||
|
f7.on('popupOpen', (popup: Popup.Popup) => onBackdropChanged(popup));
|
||||||
|
f7.on('popupClose', (popup: Popup.Popup) => onBackdropChanged(popup));
|
||||||
|
f7.on('sheetOpen', (sheet: Sheet.Sheet) => onBackdropChanged(sheet));
|
||||||
|
f7.on('sheetClose', (sheet: Sheet.Sheet) => onBackdropChanged(sheet));
|
||||||
|
|
||||||
|
f7.on('pageBeforeOut', () => {
|
||||||
|
if (isModalShowing()) {
|
||||||
|
f7.actions.close('.actions-modal.modal-in', false);
|
||||||
|
f7.dialog.close('.dialog.modal-in', false);
|
||||||
|
f7.popover.close('.popover.modal-in', false);
|
||||||
|
f7.popup.close('.popup.modal-in', false);
|
||||||
|
f7.sheet.close('.sheet-modal.modal-in', false);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (newValue) {
|
f7.on('darkModeChange', (darkMode) => {
|
||||||
f7ready((f7) => {
|
environmentsStore.framework7DarkMode = darkMode;
|
||||||
self.notification = f7.notification.create({
|
setThemeColorMeta(darkMode);
|
||||||
icon: `<img alt="logo" src="${assetConstants.ezBookkeepingLogoPath}" />`,
|
});
|
||||||
title: self.$t('global.app.title'),
|
});
|
||||||
text: newValue,
|
|
||||||
closeOnClick: true,
|
|
||||||
on: {
|
|
||||||
close() {
|
|
||||||
self.rootStore.setNotificationContent(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).open();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
let localeDefaultSettings = self.$locale.initLocale(self.userStore.currentUserLanguage, self.settingsStore.appSettings.timeZone);
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
const languageInfo = getCurrentLanguageInfo();
|
||||||
|
initMapProvider(languageInfo?.alternativeLanguageTag);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
setExpenseAndIncomeAmountColor(self.userStore.currentUserExpenseAmountColor, self.userStore.currentUserIncomeAmountColor);
|
watch(currentNotificationContent, (newValue) => {
|
||||||
|
if (notification.value) {
|
||||||
if (self.$user.isUserLogined()) {
|
notification.value.close();
|
||||||
if (!self.settingsStore.appSettings.applicationLock || self.$user.isUserUnlocked()) {
|
// @ts-expect-error there is an "destroy" function in the Notification component, but it is not defined in the type definition file
|
||||||
// refresh token if user is logined
|
// see https://framework7.io/docs/notification
|
||||||
self.tokensStore.refreshTokenAndRevokeOldToken().then(response => {
|
notification.value.destroy();
|
||||||
if (response.user) {
|
notification.value = null;
|
||||||
localeDefaultSettings = self.$locale.setLanguage(response.user.language);
|
}
|
||||||
self.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
|
||||||
|
|
||||||
setExpenseAndIncomeAmountColor(response.user.expenseAmountColor, response.user.incomeAmountColor);
|
|
||||||
|
|
||||||
if (response.notificationContent) {
|
|
||||||
self.rootStore.setNotificationContent(response.notificationContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// auto refresh exchange rates data
|
|
||||||
if (self.settingsStore.appSettings.autoUpdateExchangeRatesData) {
|
|
||||||
self.exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
setAppFontSize(this.settingsStore.appSettings.fontSize);
|
|
||||||
|
|
||||||
|
if (newValue) {
|
||||||
f7ready((f7) => {
|
f7ready((f7) => {
|
||||||
this.isDarkMode = f7.darkMode;
|
notification.value = f7.notification.create({
|
||||||
this.setThemeColorMeta(f7.darkMode);
|
icon: `<img alt="logo" src="${APPLICATION_LOGO_PATH}" />`,
|
||||||
|
title: tt('global.app.title'),
|
||||||
f7.on('actionsOpen', (event) => this.onBackdropChanged(event));
|
text: newValue,
|
||||||
f7.on('actionsClose', (event) => this.onBackdropChanged(event));
|
closeOnClick: true,
|
||||||
f7.on('dialogOpen', (event) => this.onBackdropChanged(event));
|
on: {
|
||||||
f7.on('dialogClose', (event) => this.onBackdropChanged(event));
|
close() {
|
||||||
f7.on('popoverOpen', (event) => this.onBackdropChanged(event));
|
rootStore.setNotificationContent(null);
|
||||||
f7.on('popoverClose', (event) => this.onBackdropChanged(event));
|
}
|
||||||
f7.on('popupOpen', (event) => this.onBackdropChanged(event));
|
|
||||||
f7.on('popupClose', (event) => this.onBackdropChanged(event));
|
|
||||||
f7.on('sheetOpen', (event) => this.onBackdropChanged(event));
|
|
||||||
f7.on('sheetClose', (event) => this.onBackdropChanged(event));
|
|
||||||
|
|
||||||
f7.on('pageBeforeOut', () => {
|
|
||||||
if (isModalShowing()) {
|
|
||||||
f7.actions.close('.actions-modal.modal-in', false);
|
|
||||||
f7.dialog.close('.dialog.modal-in', false);
|
|
||||||
f7.popover.close('.popover.modal-in', false);
|
|
||||||
f7.popup.close('.popup.modal-in', false);
|
|
||||||
f7.sheet.close('.sheet-modal.modal-in', false);
|
|
||||||
}
|
}
|
||||||
});
|
}).open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
f7.on('darkModeChange', (isDarkMode) => {
|
let localeDefaultSettings = initLocale(userStore.currentUserLanguage, settingsStore.appSettings.timeZone);
|
||||||
this.isDarkMode = isDarkMode;
|
settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||||
this.setThemeColorMeta(isDarkMode);
|
|
||||||
});
|
setExpenseAndIncomeAmountColor(userStore.currentUserExpenseAmountColor, userStore.currentUserIncomeAmountColor);
|
||||||
|
|
||||||
|
if (isUserLogined()) {
|
||||||
|
if (!settingsStore.appSettings.applicationLock || isUserUnlocked()) {
|
||||||
|
// refresh token if user is logined
|
||||||
|
tokensStore.refreshTokenAndRevokeOldToken().then(response => {
|
||||||
|
if (response.user) {
|
||||||
|
localeDefaultSettings = setLanguage(response.user.language);
|
||||||
|
settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||||
|
|
||||||
|
setExpenseAndIncomeAmountColor(response.user.expenseAmountColor, response.user.incomeAmountColor);
|
||||||
|
|
||||||
|
if (response.notificationContent) {
|
||||||
|
rootStore.setNotificationContent(response.notificationContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
// auto refresh exchange rates data
|
||||||
const languageInfo = this.$locale.getCurrentLanguageInfo();
|
if (settingsStore.appSettings.autoUpdateExchangeRatesData) {
|
||||||
loadMapAssets(languageInfo ? languageInfo.alternativeLanguageTag : null);
|
exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false });
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
isiOSHomeScreenMode() {
|
|
||||||
if ((/iphone|ipod|ipad/gi).test(navigator.platform) && (/Safari/i).test(navigator.appVersion) &&
|
|
||||||
window.matchMedia && window.matchMedia('(display-mode: standalone)').matches
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onBackdropChanged(event) {
|
|
||||||
if (event.push) {
|
|
||||||
this.hasPushPopupBackdrop = event.opened;
|
|
||||||
} else {
|
|
||||||
this.hasBackdrop = event.opened;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setThemeColorMeta(this.isDarkMode);
|
|
||||||
},
|
|
||||||
setThemeColorMeta(isDarkMode) {
|
|
||||||
if (this.hasPushPopupBackdrop) {
|
|
||||||
document.querySelector('meta[name=theme-color]').setAttribute('content', '#000');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDarkMode) {
|
|
||||||
if (this.hasBackdrop) {
|
|
||||||
document.querySelector('meta[name=theme-color]').setAttribute('content', '#0b0b0b');
|
|
||||||
} else {
|
|
||||||
document.querySelector('meta[name=theme-color]').setAttribute('content', '#121212');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.hasBackdrop) {
|
|
||||||
document.querySelector('meta[name=theme-color]').setAttribute('content', '#949495');
|
|
||||||
} else {
|
|
||||||
document.querySelector('meta[name=theme-color]').setAttribute('content', '#f6f6f8');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
import { type TimeRangeAndDateType, type PresetDateRange, type UnixTimeRange, DateRange } from '@/core/datetime.ts';
|
||||||
|
import { arrangeArrayWithNewStartIndex } from '@/lib/common.ts';
|
||||||
|
import {
|
||||||
|
getCurrentUnixTime,
|
||||||
|
getCurrentYear,
|
||||||
|
getUnixTime,
|
||||||
|
getLocalDatetimeFromUnixTime,
|
||||||
|
getTodayFirstUnixTime,
|
||||||
|
getDummyUnixTimeForLocalUsage,
|
||||||
|
getActualUnixTimeForStore,
|
||||||
|
getTimezoneOffsetMinutes,
|
||||||
|
getBrowserTimezoneOffsetMinutes,
|
||||||
|
getDateRangeByDateType
|
||||||
|
} from '@/lib/datetime.ts';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
|
|
||||||
|
export interface CommonDateRangeSelectionProps {
|
||||||
|
minTime?: number;
|
||||||
|
maxTime?: number;
|
||||||
|
title?: string;
|
||||||
|
hint?: string;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateRangeFromProps(props: CommonDateRangeSelectionProps): { minDate: number; maxDate: number } {
|
||||||
|
let minDate = getTodayFirstUnixTime();
|
||||||
|
let maxDate = getCurrentUnixTime();
|
||||||
|
|
||||||
|
if (props.minTime) {
|
||||||
|
minDate = props.minTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.maxTime) {
|
||||||
|
maxDate = props.maxTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
minDate,
|
||||||
|
maxDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDateRangeSelectionBase(props: CommonDateRangeSelectionProps) {
|
||||||
|
const { tt, getAllMinWeekdayNames, formatUnixTimeToLongDateTime, isLongDateMonthAfterYear, isLongTime24HourFormat } = useI18n();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { minDate, maxDate } = getDateRangeFromProps(props);
|
||||||
|
|
||||||
|
const yearRange = ref<number[]>([
|
||||||
|
2000,
|
||||||
|
getCurrentYear() + 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dateRange = ref<Date[]>([
|
||||||
|
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(minDate, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())),
|
||||||
|
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(maxDate, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()))
|
||||||
|
]);
|
||||||
|
|
||||||
|
const firstDayOfWeek = computed<number>(() => userStore.currentUserFirstDayOfWeek);
|
||||||
|
const dayNames = computed<string[]>(() => arrangeArrayWithNewStartIndex(getAllMinWeekdayNames(), firstDayOfWeek.value));
|
||||||
|
const isYearFirst = computed<boolean>(() => isLongDateMonthAfterYear());
|
||||||
|
const is24Hour = computed<boolean>(() => isLongTime24HourFormat());
|
||||||
|
const beginDateTime = computed<string>(() => {
|
||||||
|
const actualBeginUnixTime = getActualUnixTimeForStore(getUnixTime(dateRange.value[0]), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
||||||
|
return formatUnixTimeToLongDateTime(actualBeginUnixTime);
|
||||||
|
});
|
||||||
|
const endDateTime = computed<string>(() => {
|
||||||
|
const actualEndUnixTime = getActualUnixTimeForStore(getUnixTime(dateRange.value[1]), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
||||||
|
return formatUnixTimeToLongDateTime(actualEndUnixTime);
|
||||||
|
});
|
||||||
|
const presetRanges = computed<PresetDateRange[]>(() => {
|
||||||
|
const presetRanges:PresetDateRange[] = [];
|
||||||
|
|
||||||
|
[
|
||||||
|
DateRange.Today,
|
||||||
|
DateRange.LastSevenDays,
|
||||||
|
DateRange.LastThirtyDays,
|
||||||
|
DateRange.ThisWeek,
|
||||||
|
DateRange.ThisMonth,
|
||||||
|
DateRange.ThisYear
|
||||||
|
].forEach(dateRangeType => {
|
||||||
|
const dateRange = getDateRangeByDateType(dateRangeType.type, firstDayOfWeek.value) as TimeRangeAndDateType;
|
||||||
|
|
||||||
|
presetRanges.push({
|
||||||
|
label: tt(dateRangeType.name),
|
||||||
|
value: [
|
||||||
|
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dateRange.minTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())),
|
||||||
|
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dateRange.maxTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()))
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return presetRanges;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getFinalDateRange(): UnixTimeRange | null {
|
||||||
|
if (!dateRange.value[0] || !dateRange.value[1]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentMinDate = dateRange.value[0];
|
||||||
|
const currentMaxDate = dateRange.value[1];
|
||||||
|
|
||||||
|
let minUnixTime = getUnixTime(currentMinDate);
|
||||||
|
let maxUnixTime = getUnixTime(currentMaxDate);
|
||||||
|
|
||||||
|
if (minUnixTime < 0 || maxUnixTime < 0) {
|
||||||
|
throw new Error('Date is too early');
|
||||||
|
}
|
||||||
|
|
||||||
|
minUnixTime = getActualUnixTimeForStore(minUnixTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
||||||
|
maxUnixTime = getActualUnixTimeForStore(maxUnixTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
||||||
|
|
||||||
|
return {
|
||||||
|
minUnixTime,
|
||||||
|
maxUnixTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// states
|
||||||
|
yearRange,
|
||||||
|
dateRange,
|
||||||
|
// computed states
|
||||||
|
dayNames,
|
||||||
|
isYearFirst,
|
||||||
|
is24Hour,
|
||||||
|
beginDateTime,
|
||||||
|
endDateTime,
|
||||||
|
presetRanges,
|
||||||
|
// functions
|
||||||
|
getFinalDateRange
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import type { ColorValue } from '@/core/color.ts';
|
||||||
|
import { ALL_ACCOUNT_ICONS, DEFAULT_ACCOUNT_ICON, ALL_CATEGORY_ICONS, DEFAULT_CATEGORY_ICON } from '@/consts/icon.ts';
|
||||||
|
import { DEFAULT_ICON_COLOR, DEFAULT_ACCOUNT_COLOR, DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts';
|
||||||
|
import { isNumber } from '@/lib/common.ts';
|
||||||
|
|
||||||
|
type IconItemStyleName = string;
|
||||||
|
type IconItemStyleValue = ColorValue | string | number | undefined;
|
||||||
|
type CommonIconItemType = 'account' | 'category' | 'fixed';
|
||||||
|
type MobileIconItemType = 'fixed-f7';
|
||||||
|
|
||||||
|
export interface CommonIconProps {
|
||||||
|
iconType: CommonIconItemType | MobileIconItemType;
|
||||||
|
iconId: string | number;
|
||||||
|
color?: ColorValue;
|
||||||
|
defaultColor?: ColorValue;
|
||||||
|
additionalColorAttr?: string;
|
||||||
|
size?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useItemIconBase(props: CommonIconProps) {
|
||||||
|
const style = computed<Record<IconItemStyleName, IconItemStyleValue>>(() => {
|
||||||
|
let defaultColor = 'var(--default-icon-color)';
|
||||||
|
|
||||||
|
if (props.defaultColor) {
|
||||||
|
defaultColor = props.defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.iconType === 'account') {
|
||||||
|
return getAccountIconStyle(props.color, defaultColor, props.additionalColorAttr);
|
||||||
|
} else if (props.iconType === 'category') {
|
||||||
|
return getCategoryIconStyle(props.color, defaultColor, props.additionalColorAttr);
|
||||||
|
} else {
|
||||||
|
return getDefaultIconStyle(props.color, defaultColor, props.additionalColorAttr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getAccountIcon(iconId: string | number): string {
|
||||||
|
if (isNumber(iconId)) {
|
||||||
|
iconId = iconId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ALL_ACCOUNT_ICONS[iconId]) {
|
||||||
|
return DEFAULT_ACCOUNT_ICON.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ALL_ACCOUNT_ICONS[iconId].icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryIcon(iconId: string | number): string {
|
||||||
|
if (isNumber(iconId)) {
|
||||||
|
iconId = iconId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ALL_CATEGORY_ICONS[iconId]) {
|
||||||
|
return DEFAULT_CATEGORY_ICON.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ALL_CATEGORY_ICONS[iconId].icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccountIconStyle(color?: ColorValue | string, defaultColor?: ColorValue | string, additionalColorAttr?: string): Record<IconItemStyleName, IconItemStyleValue> {
|
||||||
|
if (color && color !== DEFAULT_ACCOUNT_COLOR) {
|
||||||
|
color = '#' + color;
|
||||||
|
} else {
|
||||||
|
color = defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret: Record<IconItemStyleName, IconItemStyleValue> = {
|
||||||
|
color: color
|
||||||
|
};
|
||||||
|
|
||||||
|
if (additionalColorAttr) {
|
||||||
|
ret[additionalColorAttr] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.size) {
|
||||||
|
ret['font-size'] = props.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryIconStyle(color?: ColorValue | string, defaultColor?: ColorValue | string, additionalColorAttr?: string): Record<IconItemStyleName, IconItemStyleValue> {
|
||||||
|
if (color && color !== DEFAULT_CATEGORY_COLOR) {
|
||||||
|
color = '#' + color;
|
||||||
|
} else {
|
||||||
|
color = defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret: Record<IconItemStyleName, IconItemStyleValue> = {
|
||||||
|
color: color
|
||||||
|
};
|
||||||
|
|
||||||
|
if (additionalColorAttr) {
|
||||||
|
ret[additionalColorAttr] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.size) {
|
||||||
|
ret['font-size'] = props.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultIconStyle(color?: ColorValue | string, defaultColor?: ColorValue | string, additionalColorAttr?: string): Record<IconItemStyleName, IconItemStyleValue> {
|
||||||
|
if (color && color !== DEFAULT_ICON_COLOR) {
|
||||||
|
color = '#' + color;
|
||||||
|
} else {
|
||||||
|
color = defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret: Record<IconItemStyleName, IconItemStyleValue> = {
|
||||||
|
color: color
|
||||||
|
};
|
||||||
|
|
||||||
|
if (additionalColorAttr) {
|
||||||
|
ret[additionalColorAttr] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.size) {
|
||||||
|
ret['font-size'] = props.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
style,
|
||||||
|
getAccountIcon,
|
||||||
|
getCategoryIcon
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import type { LanguageOption } from '@/locales/index.ts';
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import { useSettingsStore } from '@/stores/setting.ts';
|
||||||
|
|
||||||
|
export interface LanguageSelectBaseProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
includeSystemDefault?: boolean;
|
||||||
|
useModelValue?: boolean;
|
||||||
|
modelValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanguageSelectBaseEmits {
|
||||||
|
(e: 'update:modelValue', value: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLanguageSelectButtonBase(props: LanguageSelectBaseProps, emit: LanguageSelectBaseEmits) {
|
||||||
|
const { getCurrentLanguageTag, getCurrentLanguageDisplayName, getAllLanguageOptions, getLanguageInfo, setLanguage } = useI18n();
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
|
const allLanguages = computed<LanguageOption[]>(() => getAllLanguageOptions(!!props.includeSystemDefault));
|
||||||
|
|
||||||
|
const currentLocale = computed<string>({
|
||||||
|
get: () => getCurrentLanguageTag(),
|
||||||
|
set: (value: string) => {
|
||||||
|
const localeDefaultSettings = setLanguage(value);
|
||||||
|
settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentLanguageName = computed<string>(() => {
|
||||||
|
if (props.useModelValue && props.modelValue) {
|
||||||
|
const languageInfo = getLanguageInfo(props.modelValue);
|
||||||
|
|
||||||
|
if (!languageInfo) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return languageInfo.displayName;
|
||||||
|
} else {
|
||||||
|
return getCurrentLanguageDisplayName()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateLanguage(languageTag: string): void {
|
||||||
|
if (props.useModelValue) {
|
||||||
|
emit('update:modelValue', languageTag);
|
||||||
|
} else {
|
||||||
|
currentLocale.value = languageTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLanguageSelected(languageTag: string): boolean {
|
||||||
|
if (props.useModelValue) {
|
||||||
|
return props.modelValue === languageTag;
|
||||||
|
} else {
|
||||||
|
return currentLocale.value === languageTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// computed states
|
||||||
|
allLanguages,
|
||||||
|
currentLocale,
|
||||||
|
currentLanguageName,
|
||||||
|
// functions
|
||||||
|
updateLanguage,
|
||||||
|
isLanguageSelected
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
import type { YearMonth } from '@/core/datetime.ts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getYearMonthObjectFromUnixTime,
|
||||||
|
getYearMonthObjectFromString,
|
||||||
|
getYearMonthStringFromObject,
|
||||||
|
getCurrentUnixTime,
|
||||||
|
getCurrentYear,
|
||||||
|
getThisYearFirstUnixTime,
|
||||||
|
getYearMonthFirstUnixTime,
|
||||||
|
getYearMonthLastUnixTime
|
||||||
|
} from '@/lib/datetime.ts';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
export interface CommonMonthRangeSelectionProps {
|
||||||
|
minTime?: string;
|
||||||
|
maxTime?: string;
|
||||||
|
title?: string;
|
||||||
|
hint?: string;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonthRangeFromProps(props: CommonMonthRangeSelectionProps): { minDate: YearMonth; maxDate: YearMonth } {
|
||||||
|
let minDate: YearMonth = getYearMonthObjectFromUnixTime(getThisYearFirstUnixTime());
|
||||||
|
let maxDate: YearMonth = getYearMonthObjectFromUnixTime(getCurrentUnixTime());
|
||||||
|
|
||||||
|
if (props.minTime) {
|
||||||
|
const yearMonth = getYearMonthObjectFromString(props.minTime);
|
||||||
|
|
||||||
|
if (yearMonth) {
|
||||||
|
minDate = yearMonth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.maxTime) {
|
||||||
|
const yearMonth = getYearMonthObjectFromString(props.maxTime);
|
||||||
|
|
||||||
|
if (yearMonth) {
|
||||||
|
maxDate = yearMonth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
minDate,
|
||||||
|
maxDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMonthRangeSelectionBase(props: CommonMonthRangeSelectionProps) {
|
||||||
|
const { formatUnixTimeToLongYearMonth, isLongDateMonthAfterYear } = useI18n();
|
||||||
|
const { minDate, maxDate } = getMonthRangeFromProps(props);
|
||||||
|
|
||||||
|
const yearRange = ref<number[]>([
|
||||||
|
2000,
|
||||||
|
getCurrentYear() + 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dateRange = ref<YearMonth[]>([
|
||||||
|
minDate,
|
||||||
|
maxDate
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isYearFirst = computed<boolean>(() => isLongDateMonthAfterYear());
|
||||||
|
const beginDateTime = computed<string>(() => formatUnixTimeToLongYearMonth(getYearMonthFirstUnixTime(dateRange.value[0])));
|
||||||
|
const endDateTime = computed<string>(() => formatUnixTimeToLongYearMonth(getYearMonthLastUnixTime(dateRange.value[1])));
|
||||||
|
|
||||||
|
function getFinalMonthRange(): { minYearMonth: string, maxYearMonth: string } | null {
|
||||||
|
if (!dateRange.value[0] || !dateRange.value[1]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateRange.value[0].year <= 0 || dateRange.value[0].month < 0 || dateRange.value[1].year <= 0 || dateRange.value[1].month < 0) {
|
||||||
|
throw new Error('Date is too early');
|
||||||
|
}
|
||||||
|
|
||||||
|
const minYearMonth = getYearMonthStringFromObject(dateRange.value[0]);
|
||||||
|
const maxYearMonth = getYearMonthStringFromObject(dateRange.value[1]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
minYearMonth,
|
||||||
|
maxYearMonth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// states
|
||||||
|
yearRange,
|
||||||
|
dateRange,
|
||||||
|
// computed states
|
||||||
|
isYearFirst,
|
||||||
|
beginDateTime,
|
||||||
|
endDateTime,
|
||||||
|
// functions
|
||||||
|
getFinalMonthRange
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import { DEFAULT_CHART_COLORS } from '@/consts/color.ts';
|
||||||
|
|
||||||
|
import { isNumber } from '@/lib/common.ts';
|
||||||
|
import { formatPercent } from '@/lib/numeral.ts';
|
||||||
|
|
||||||
|
export interface CommonPieChartDataItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
value: number;
|
||||||
|
percent: number;
|
||||||
|
actualPercent: number;
|
||||||
|
color: string;
|
||||||
|
sourceItem: Record<string, unknown>;
|
||||||
|
displayPercent?: string;
|
||||||
|
displayValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommonPieChartProps {
|
||||||
|
skeleton?: boolean;
|
||||||
|
items: Record<string, unknown>[];
|
||||||
|
idField?: string;
|
||||||
|
nameField: string;
|
||||||
|
valueField: string;
|
||||||
|
percentField?: string;
|
||||||
|
colorField?: string;
|
||||||
|
hiddenField?: string;
|
||||||
|
minValidPercent?: number;
|
||||||
|
defaultCurrency?: string;
|
||||||
|
showValue?: boolean;
|
||||||
|
enableClickItem?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePieChartBase(props: CommonPieChartProps) {
|
||||||
|
const { formatAmountWithCurrency } = useI18n();
|
||||||
|
|
||||||
|
const selectedIndex = ref<number>(0);
|
||||||
|
|
||||||
|
const validItems = computed<CommonPieChartDataItem[]>(() => {
|
||||||
|
let totalValidValue = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < props.items.length; i++) {
|
||||||
|
const item = props.items[i];
|
||||||
|
const value = item[props.valueField];
|
||||||
|
|
||||||
|
if (isNumber(value) && value > 0 && (!props.hiddenField || !item[props.hiddenField])) {
|
||||||
|
totalValidValue += value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validItems: CommonPieChartDataItem[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < props.items.length; i++) {
|
||||||
|
const item = props.items[i];
|
||||||
|
const value = item[props.valueField];
|
||||||
|
const percent = props.percentField ? item[props.percentField] : -1;
|
||||||
|
|
||||||
|
if (isNumber(value) && value > 0 &&
|
||||||
|
(!props.hiddenField || !item[props.hiddenField]) &&
|
||||||
|
(!props.minValidPercent || value / totalValidValue > props.minValidPercent)) {
|
||||||
|
const finalItem: CommonPieChartDataItem = {
|
||||||
|
id: (props.idField && item[props.idField]) ? item[props.idField] as string : item[props.nameField] as string,
|
||||||
|
name: (props.idField && item[props.idField]) ? item[props.idField] as string : item[props.nameField] as string,
|
||||||
|
displayName: item[props.nameField] as string,
|
||||||
|
value: value,
|
||||||
|
percent: (isNumber(percent) && percent >= 0) ? percent : (value / totalValidValue * 100),
|
||||||
|
actualPercent: value / totalValidValue,
|
||||||
|
color: (props.colorField && item[props.colorField]) ? item[props.colorField] as string : DEFAULT_CHART_COLORS[validItems.length % DEFAULT_CHART_COLORS.length],
|
||||||
|
sourceItem: item
|
||||||
|
};
|
||||||
|
|
||||||
|
finalItem.displayPercent = formatPercent(finalItem.percent, 2, '<0.01');
|
||||||
|
finalItem.displayValue = formatAmountWithCurrency(finalItem.value, props.defaultCurrency);
|
||||||
|
|
||||||
|
validItems.push(finalItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validItems;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.items, () => {
|
||||||
|
selectedIndex.value = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// states
|
||||||
|
selectedIndex,
|
||||||
|
// computed states
|
||||||
|
validItems
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
|
|
||||||
|
import type { TypeAndDisplayName } from '@/core/base.ts';
|
||||||
|
import { sortNumbersArray } from '@/lib/common.ts';
|
||||||
|
|
||||||
|
export interface CommonScheduleFrequencySelectionProps {
|
||||||
|
type: number;
|
||||||
|
modelValue: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableMonthDay {
|
||||||
|
day: number;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useScheduleFrequencySelectionBase() {
|
||||||
|
const { getAllWeekDays, getAllTransactionScheduledFrequencyTypes, getMonthdayShortName } = useI18n();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const allTransactionScheduledFrequencyTypes = computed<TypeAndDisplayName[]>(() => getAllTransactionScheduledFrequencyTypes());
|
||||||
|
const allWeekDays = computed<TypeAndDisplayName[]>(() => getAllWeekDays(userStore.currentUserFirstDayOfWeek));
|
||||||
|
|
||||||
|
const allAvailableMonthDays = computed<AvailableMonthDay[]>(() => {
|
||||||
|
const allAvailableDays = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 28; i++) {
|
||||||
|
allAvailableDays.push({
|
||||||
|
day: i,
|
||||||
|
displayName: getMonthdayShortName(i),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return allAvailableDays;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getFrequencyValues(value: string): number[] {
|
||||||
|
const values = value.split(',');
|
||||||
|
const ret: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
if (values[i]) {
|
||||||
|
ret.push(parseInt(values[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortNumbersArray(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// computed states
|
||||||
|
allTransactionScheduledFrequencyTypes,
|
||||||
|
allWeekDays,
|
||||||
|
allAvailableMonthDays,
|
||||||
|
// functions
|
||||||
|
getFrequencyValues
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
YearMonth,
|
||||||
|
TimeRangeAndDateType,
|
||||||
|
YearUnixTime,
|
||||||
|
YearQuarterUnixTime,
|
||||||
|
YearMonthUnixTime
|
||||||
|
} from '@/core/datetime.ts';
|
||||||
|
import type { ColorValue } from '@/core/color.ts';
|
||||||
|
import { DEFAULT_ICON_COLOR } from '@/consts/color.ts';
|
||||||
|
import type { YearMonthItems } from '@/models/transaction.ts';
|
||||||
|
|
||||||
|
import { getAllDateRanges } from '@/lib/statistics.ts';
|
||||||
|
|
||||||
|
export interface CommonTrendsChartProps<T extends YearMonth> {
|
||||||
|
items: YearMonthItems<T>[];
|
||||||
|
startYearMonth: string;
|
||||||
|
endYearMonth: string;
|
||||||
|
sortingType: number;
|
||||||
|
dateAggregationType: number;
|
||||||
|
idField?: string;
|
||||||
|
nameField: string;
|
||||||
|
valueField: string;
|
||||||
|
colorField?: string;
|
||||||
|
hiddenField?: string;
|
||||||
|
displayOrdersField?: string;
|
||||||
|
translateName?: boolean;
|
||||||
|
defaultCurrency?: string;
|
||||||
|
enableClickItem?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrendsBarChartClickEvent {
|
||||||
|
itemId: string;
|
||||||
|
dateRange: TimeRangeAndDateType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTrendsChartBase<T extends YearMonth>(props: CommonTrendsChartProps<T>) {
|
||||||
|
const { tt } = useI18n();
|
||||||
|
|
||||||
|
const allDateRanges = computed<YearUnixTime[] | YearQuarterUnixTime[] | YearMonthUnixTime[]>(() => getAllDateRanges(props.items, props.startYearMonth, props.endYearMonth, props.dateAggregationType));
|
||||||
|
|
||||||
|
function getItemName(name: string): string {
|
||||||
|
return props.translateName ? tt(name) : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColor(color: string): ColorValue {
|
||||||
|
if (color && color !== DEFAULT_ICON_COLOR) {
|
||||||
|
color = '#' + color;
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// computed states
|
||||||
|
allDateRanges,
|
||||||
|
// functions
|
||||||
|
getItemName,
|
||||||
|
getColor
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { type Ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type TwoLevelItemSelectionBaseProps,
|
||||||
|
useTwoLevelItemSelectionBase
|
||||||
|
} from '@/components/base/TwoLevelItemSelectionBase.ts';
|
||||||
|
|
||||||
|
import { getItemByKeyValue, getPrimaryValueBySecondaryValue } from '@/lib/common.ts';
|
||||||
|
|
||||||
|
export interface CommonTwoColumnListItemSelectionProps extends TwoLevelItemSelectionBaseProps {
|
||||||
|
primaryValueField?: string;
|
||||||
|
primaryHeaderField?: string;
|
||||||
|
primaryHeaderI18n?: boolean;
|
||||||
|
primaryFooterField?: string;
|
||||||
|
primaryFooterI18n?: boolean;
|
||||||
|
secondaryHeaderField?: string;
|
||||||
|
secondaryHeaderI18n?: boolean;
|
||||||
|
secondaryFooterField?: string;
|
||||||
|
secondaryFooterI18n?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTwoColumnListItemSelectionBase(props: CommonTwoColumnListItemSelectionProps) {
|
||||||
|
const {
|
||||||
|
filterContent,
|
||||||
|
visibleItemsCount,
|
||||||
|
filteredItems,
|
||||||
|
getFilteredSubItems,
|
||||||
|
isSecondaryValueSelected,
|
||||||
|
getSelectedSecondaryItem,
|
||||||
|
updateCurrentSecondaryValue
|
||||||
|
} = useTwoLevelItemSelectionBase(props);
|
||||||
|
|
||||||
|
function getCurrentPrimaryValueBySecondaryValue(secondaryValue: unknown): unknown {
|
||||||
|
return getPrimaryValueBySecondaryValue(props.items as Record<string, Record<string, unknown>[]>[], props.primarySubItemsField, props.primaryValueField, props.primaryHiddenField, props.secondaryValueField, props.secondaryHiddenField, secondaryValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedPrimaryItem(currentPrimaryValue: unknown): unknown {
|
||||||
|
if (props.primaryValueField) {
|
||||||
|
return getItemByKeyValue(props.items, currentPrimaryValue, props.primaryValueField);
|
||||||
|
} else {
|
||||||
|
return currentPrimaryValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentPrimaryValue(currentPrimaryValue: Ref<unknown>, item: unknown): void {
|
||||||
|
if (props.primaryValueField) {
|
||||||
|
currentPrimaryValue.value = (item as Record<string, unknown>)[props.primaryValueField];
|
||||||
|
} else {
|
||||||
|
currentPrimaryValue.value = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// states
|
||||||
|
filterContent,
|
||||||
|
// computed states
|
||||||
|
visibleItemsCount,
|
||||||
|
filteredItems,
|
||||||
|
// functions
|
||||||
|
getFilteredSubItems,
|
||||||
|
getCurrentPrimaryValueBySecondaryValue,
|
||||||
|
isSecondaryValueSelected,
|
||||||
|
getSelectedPrimaryItem,
|
||||||
|
getSelectedSecondaryItem,
|
||||||
|
updateCurrentPrimaryValue,
|
||||||
|
updateCurrentSecondaryValue
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { type Ref, ref, computed } from 'vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import { getItemByKeyValue } from '@/lib/common.ts';
|
||||||
|
|
||||||
|
export interface TwoLevelItemSelectionBaseProps {
|
||||||
|
modelValue: unknown;
|
||||||
|
primaryKeyField?: string;
|
||||||
|
primaryTitleField?: string;
|
||||||
|
primaryTitleI18n?: boolean;
|
||||||
|
primaryIconField?: string;
|
||||||
|
primaryIconType?: string;
|
||||||
|
primaryColorField?: string;
|
||||||
|
primaryHiddenField?: string;
|
||||||
|
primarySubItemsField: string;
|
||||||
|
secondaryKeyField?: string;
|
||||||
|
secondaryValueField?: string;
|
||||||
|
secondaryTitleField?: string;
|
||||||
|
secondaryTitleI18n?: boolean;
|
||||||
|
secondaryIconField?: string;
|
||||||
|
secondaryIconType?: string;
|
||||||
|
secondaryColorField?: string;
|
||||||
|
secondaryHiddenField?: string;
|
||||||
|
enableFilter?: boolean;
|
||||||
|
filterPlaceholder?: string;
|
||||||
|
filterNoItemsText?: string;
|
||||||
|
items: Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTwoLevelItemSelectionBase(props: TwoLevelItemSelectionBaseProps) {
|
||||||
|
const { ti } = useI18n();
|
||||||
|
|
||||||
|
const filterContent = ref<string>('');
|
||||||
|
|
||||||
|
const visibleItemsCount = computed<number>(() => {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const item of props.items) {
|
||||||
|
if (props.primaryHiddenField && item[props.primaryHiddenField]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredItems = computed<Record<string, unknown>[]>(() => {
|
||||||
|
const finalItems: Record<string, unknown>[] = [];
|
||||||
|
const items = props.items;
|
||||||
|
const lowerCaseFilterContent = filterContent.value?.toLowerCase() ?? '';
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (props.primaryHiddenField && item[props.primaryHiddenField]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.enableFilter || !lowerCaseFilterContent) {
|
||||||
|
finalItems.push(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.primaryTitleField) {
|
||||||
|
const title = ti(item[props.primaryTitleField] as string, !!props.primaryTitleI18n);
|
||||||
|
|
||||||
|
if (title.toLowerCase().indexOf(lowerCaseFilterContent) >= 0) {
|
||||||
|
finalItems.push(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.primarySubItemsField) {
|
||||||
|
if (getFilteredSubItems(item).length > 0) {
|
||||||
|
finalItems.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalItems;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getFilteredSubItems(selectedPrimaryItem: unknown): Record<string, unknown>[] {
|
||||||
|
const finalItems: Record<string, unknown>[] = [];
|
||||||
|
|
||||||
|
if (!selectedPrimaryItem || !props.primarySubItemsField) {
|
||||||
|
return finalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subItems = (selectedPrimaryItem as Record<string, unknown>)[props.primarySubItemsField] as Record<string, unknown>[];
|
||||||
|
let primaryTitleHasFilterContent = false;
|
||||||
|
|
||||||
|
if (props.primaryTitleField) {
|
||||||
|
const title = ti((selectedPrimaryItem as Record<string, unknown>)[props.primaryTitleField] as string, !!props.primaryTitleI18n);
|
||||||
|
primaryTitleHasFilterContent = title.toLowerCase().indexOf(filterContent.value.toLowerCase()) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const subItem of subItems) {
|
||||||
|
if (props.secondaryHiddenField && subItem[props.secondaryHiddenField]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.enableFilter || !filterContent.value) {
|
||||||
|
finalItems.push(subItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primaryTitleHasFilterContent) {
|
||||||
|
finalItems.push(subItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.secondaryTitleField && filterContent.value) {
|
||||||
|
const title = ti(subItem[props.secondaryTitleField] as string, !!props.secondaryTitleI18n);
|
||||||
|
|
||||||
|
if (title.toLowerCase().indexOf(filterContent.value.toLowerCase()) >= 0) {
|
||||||
|
finalItems.push(subItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSecondaryValueSelected(currentSecondaryValue: unknown, subItem: unknown): boolean {
|
||||||
|
if (props.secondaryValueField) {
|
||||||
|
return currentSecondaryValue === (subItem as Record<string, unknown>)[props.secondaryValueField];
|
||||||
|
} else {
|
||||||
|
return currentSecondaryValue === subItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedSecondaryItem(currentSecondaryValue: unknown, selectedPrimaryItem: unknown): unknown {
|
||||||
|
if (currentSecondaryValue && selectedPrimaryItem && (selectedPrimaryItem as Record<string, unknown>)[props.primarySubItemsField]) {
|
||||||
|
return getItemByKeyValue((selectedPrimaryItem as Record<string, unknown>)[props.primarySubItemsField] as Record<string, unknown>[], currentSecondaryValue, props.secondaryValueField as string);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentSecondaryValue(currentSecondaryValue: Ref<unknown>, subItem: unknown): void {
|
||||||
|
if (props.secondaryValueField) {
|
||||||
|
currentSecondaryValue.value = (subItem as Record<string, unknown>)[props.secondaryValueField];
|
||||||
|
} else {
|
||||||
|
currentSecondaryValue.value = subItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// states
|
||||||
|
filterContent,
|
||||||
|
// computed states
|
||||||
|
visibleItemsCount,
|
||||||
|
filteredItems,
|
||||||
|
// functions
|
||||||
|
getFilteredSubItems,
|
||||||
|
isSecondaryValueSelected,
|
||||||
|
getSelectedSecondaryItem,
|
||||||
|
updateCurrentSecondaryValue
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,112 +8,104 @@
|
|||||||
v-if="!mapSupported || !mapDependencyLoaded"></slot>
|
v-if="!mapSupported || !mapDependencyLoaded"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import {
|
import { ref, computed, useTemplateRef } from 'vue';
|
||||||
copyObjectTo
|
|
||||||
} from '@/lib/common.js';
|
|
||||||
import {
|
|
||||||
createMapHolder,
|
|
||||||
initMapInstance,
|
|
||||||
setMapCenterTo,
|
|
||||||
setMapCenterMarker,
|
|
||||||
removeMapCenterMarker
|
|
||||||
} from '@/lib/map/index.js';
|
|
||||||
|
|
||||||
export default {
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
props: [
|
|
||||||
'height',
|
|
||||||
'mapClass',
|
|
||||||
'mapStyle',
|
|
||||||
'geoLocation'
|
|
||||||
],
|
|
||||||
expose: [
|
|
||||||
'init'
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
this.mapHolder = createMapHolder();
|
|
||||||
|
|
||||||
return {
|
import type { MapInstance, MapPosition } from '@/lib/map/base.ts';
|
||||||
mapSupported: !!this.mapHolder,
|
import { createMapInstance } from '@/lib/map/index.ts';
|
||||||
mapDependencyLoaded: this.mapHolder && this.mapHolder.dependencyLoaded,
|
|
||||||
mapInited: false,
|
const props = defineProps<{
|
||||||
initCenter: {
|
height?: string;
|
||||||
latitude: 0,
|
mapClass?: string;
|
||||||
longitude: 0,
|
mapStyle?: Record<string, string>;
|
||||||
},
|
geoLocation?: MapPosition;
|
||||||
zoomLevel: 1
|
}>();
|
||||||
|
|
||||||
|
const { tt, getCurrentLanguageInfo } = useI18n();
|
||||||
|
|
||||||
|
const mapContainer = useTemplateRef<HTMLElement>('mapContainer');
|
||||||
|
const mapInstance = ref<MapInstance | null>(createMapInstance());
|
||||||
|
const initCenter = ref<MapPosition>({
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0
|
||||||
|
});
|
||||||
|
const zoomLevel = ref<number>(1);
|
||||||
|
|
||||||
|
const mapSupported = computed<boolean>(() => !!mapInstance.value);
|
||||||
|
const mapDependencyLoaded = computed<boolean>(() => mapInstance.value?.dependencyLoaded || false);
|
||||||
|
|
||||||
|
const finalMapStyle = computed<Record<string, unknown>>(() => {
|
||||||
|
const styles: Record<string, unknown> = Object.assign({}, props.mapStyle);
|
||||||
|
|
||||||
|
if (props.height) {
|
||||||
|
styles['height'] = props.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mapSupported.value || !mapDependencyLoaded.value) {
|
||||||
|
styles['height'] = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return styles;
|
||||||
|
});
|
||||||
|
|
||||||
|
function initMapView(): void {
|
||||||
|
let isFirstInit = false;
|
||||||
|
let centerChanged = false;
|
||||||
|
|
||||||
|
if (!mapSupported.value || !mapDependencyLoaded.value || !mapInstance.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.geoLocation && (props.geoLocation.longitude || props.geoLocation.latitude)) {
|
||||||
|
if (initCenter.value.latitude !== props.geoLocation.latitude || initCenter.value.longitude !== props.geoLocation.longitude) {
|
||||||
|
initCenter.value.latitude = props.geoLocation.latitude;
|
||||||
|
initCenter.value.longitude = props.geoLocation.longitude;
|
||||||
|
zoomLevel.value = mapInstance.value.defaultZoomLevel;
|
||||||
|
|
||||||
|
centerChanged = true;
|
||||||
}
|
}
|
||||||
},
|
} else if (!props.geoLocation || (!props.geoLocation.longitude && !props.geoLocation.latitude)) {
|
||||||
computed: {
|
if (initCenter.value.latitude || initCenter.value.longitude) {
|
||||||
finalMapStyle() {
|
initCenter.value.latitude = 0;
|
||||||
const styles = copyObjectTo(this.mapStyle, {});
|
initCenter.value.longitude = 0;
|
||||||
|
zoomLevel.value = mapInstance.value.minZoomLevel;
|
||||||
|
|
||||||
if (this.height) {
|
centerChanged = true;
|
||||||
styles.height = this.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.mapSupported || !this.mapDependencyLoaded) {
|
|
||||||
styles.height = '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
return styles;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
init() {
|
|
||||||
let isFirstInit = false;
|
|
||||||
let centerChanged = false;
|
|
||||||
|
|
||||||
if (!this.mapSupported || !this.mapDependencyLoaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.geoLocation && (this.geoLocation.longitude || this.geoLocation.latitude)) {
|
|
||||||
if (this.initCenter.latitude !== this.geoLocation.latitude || this.initCenter.longitude !== this.geoLocation.longitude) {
|
|
||||||
this.initCenter.latitude = this.geoLocation.latitude;
|
|
||||||
this.initCenter.longitude = this.geoLocation.longitude;
|
|
||||||
this.zoomLevel = this.mapHolder.defaultZoomLevel;
|
|
||||||
|
|
||||||
centerChanged = true;
|
|
||||||
}
|
|
||||||
} else if (!this.geoLocation || (!this.geoLocation.longitude && !this.geoLocation.latitude)) {
|
|
||||||
if (this.initCenter.latitude || this.initCenter.longitude) {
|
|
||||||
this.initCenter.latitude = 0;
|
|
||||||
this.initCenter.longitude = 0;
|
|
||||||
this.zoomLevel = this.mapHolder.minZoomLevel;
|
|
||||||
|
|
||||||
centerChanged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.mapHolder.inited) {
|
|
||||||
const languageInfo = this.$locale.getCurrentLanguageInfo();
|
|
||||||
|
|
||||||
initMapInstance(this.mapHolder, this.$refs.mapContainer, {
|
|
||||||
language: languageInfo ? languageInfo.alternativeLanguageTag : null,
|
|
||||||
initCenter: this.initCenter,
|
|
||||||
zoomLevel: this.zoomLevel,
|
|
||||||
text: {
|
|
||||||
zoomIn: this.$t('Zoom in'),
|
|
||||||
zoomOut: this.$t('Zoom out'),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.mapHolder.inited) {
|
|
||||||
isFirstInit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFirstInit || centerChanged) {
|
|
||||||
setMapCenterTo(this.mapHolder, this.initCenter, this.zoomLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (centerChanged && this.zoomLevel > this.mapHolder.minZoomLevel) {
|
|
||||||
setMapCenterMarker(this.mapHolder, this.initCenter);
|
|
||||||
} else if (centerChanged && this.zoomLevel <= this.mapHolder.minZoomLevel) {
|
|
||||||
removeMapCenterMarker(this.mapHolder);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mapInstance.value.inited) {
|
||||||
|
const languageInfo = getCurrentLanguageInfo();
|
||||||
|
|
||||||
|
mapInstance.value.initMapInstance(mapContainer.value as HTMLElement, {
|
||||||
|
language: languageInfo?.alternativeLanguageTag,
|
||||||
|
initCenter: initCenter.value,
|
||||||
|
zoomLevel: zoomLevel.value,
|
||||||
|
text: {
|
||||||
|
zoomIn: tt('Zoom in'),
|
||||||
|
zoomOut: tt('Zoom out'),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mapInstance.value.inited) {
|
||||||
|
isFirstInit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirstInit || centerChanged) {
|
||||||
|
mapInstance.value.setMapCenterTo(initCenter.value, zoomLevel.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (centerChanged && zoomLevel.value > mapInstance.value.minZoomLevel) {
|
||||||
|
mapInstance.value.setMapCenterMarker(initCenter.value);
|
||||||
|
} else if (centerChanged && zoomLevel.value <= mapInstance.value.minZoomLevel) {
|
||||||
|
mapInstance.value.removeMapCenterMarker();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
initMapView
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
<div class="pin-code-input pin-code-input-outline"
|
<div class="pin-code-input pin-code-input-outline"
|
||||||
:class="{ 'pin-code-input-focued': codes[index].focused }" :key="index"
|
:class="{ 'pin-code-input-focued': codes[index].focused }" :key="index"
|
||||||
v-for="(code, index) in codes">
|
v-for="(code, index) in codes">
|
||||||
<input min="0" maxlength="1" pattern="[0-9]*"
|
<input ref="pin-code-input" min="0" maxlength="1" pattern="[0-9]*"
|
||||||
:ref="`pin-code-input-${index}`"
|
|
||||||
:value="codes[index].value"
|
:value="codes[index].value"
|
||||||
:type="codes[index].inputType"
|
:type="codes[index].inputType"
|
||||||
:disabled="disabled ? 'disabled' : undefined"
|
:disabled="disabled ? 'disabled' : undefined"
|
||||||
@@ -19,243 +18,257 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import { ref, computed, watch, useTemplateRef } from 'vue';
|
||||||
props: [
|
|
||||||
'modelValue',
|
|
||||||
'disabled',
|
|
||||||
'autofocus',
|
|
||||||
'secure',
|
|
||||||
'length'
|
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'update:modelValue',
|
|
||||||
'pincode:confirm'
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
codes: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
finalPinCode() {
|
|
||||||
let finalPinCode = '';
|
|
||||||
|
|
||||||
for (let i = 0; i < this.codes.length; i++) {
|
interface PinCode {
|
||||||
if (this.codes[i].value) {
|
value: string;
|
||||||
finalPinCode += this.codes[i].value;
|
inputType: string;
|
||||||
} else {
|
inputTimer: unknown | null;
|
||||||
break;
|
focused: boolean;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return finalPinCode;
|
const props = defineProps<{
|
||||||
}
|
modelValue: string;
|
||||||
},
|
length: number;
|
||||||
watch: {
|
disabled?: boolean;
|
||||||
'length': function (newValue) {
|
autofocus?: boolean;
|
||||||
this.init(newValue, this.modelValue);
|
autoConfirm?: boolean;
|
||||||
},
|
secure?: boolean;
|
||||||
'modelValue': function (newValue) {
|
}>();
|
||||||
if (newValue === this.finalPinCode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.init(this.length, newValue);
|
const emit = defineEmits<{
|
||||||
},
|
(e: 'update:modelValue', value: string): void;
|
||||||
'codes': {
|
(e: 'pincode:confirm', value: string): void;
|
||||||
handler() {
|
}>();
|
||||||
this.$emit('update:modelValue', this.finalPinCode);
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.init(this.length, this.modelValue);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
init(length, value) {
|
|
||||||
this.codes.length = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
const codes = ref<PinCode[]>([]);
|
||||||
const code = {
|
const pinCodeInputs = useTemplateRef<HTMLInputElement[]>('pin-code-input');
|
||||||
value: '',
|
|
||||||
inputType: 'tel',
|
|
||||||
inputTimer: null,
|
|
||||||
focused: false
|
|
||||||
};
|
|
||||||
|
|
||||||
if (value && value[i]) {
|
const finalPinCode = computed<string>(() => {
|
||||||
code.value = value[i];
|
let ret = '';
|
||||||
|
|
||||||
if (this.secure) {
|
for (let i = 0; i < codes.value.length; i++) {
|
||||||
code.inputType = 'password';
|
if (codes.value[i].value) {
|
||||||
}
|
ret += codes.value[i].value;
|
||||||
}
|
} else {
|
||||||
|
break;
|
||||||
this.codes.push(code);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
autoFillText(index, text) {
|
|
||||||
let lastIndex = index;
|
|
||||||
|
|
||||||
for (let i = index, j = 0; i < this.codes.length && j < text.length; i++, j++) {
|
|
||||||
if (text[j] < '0' || text[j] > '9') {
|
|
||||||
this.codes[i].value = '';
|
|
||||||
this.$forceUpdate();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.codes[i].value = text[j];
|
|
||||||
this.setInputType(i);
|
|
||||||
lastIndex = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setFocus(lastIndex);
|
|
||||||
|
|
||||||
if (this.finalPinCode.length === this.length) {
|
|
||||||
this.$emit('pincode:confirm', this.finalPinCode);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setInputType(index) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
if (!self.secure) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self.codes[index].value) {
|
|
||||||
self.codes[index].inputType = 'tel';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.codes[index].inputTimer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.codes[index].inputTimer = setTimeout(() => {
|
|
||||||
if (self.codes[index].value) {
|
|
||||||
self.codes[index].inputType = 'password';
|
|
||||||
} else {
|
|
||||||
self.codes[index].inputType = 'tel';
|
|
||||||
}
|
|
||||||
|
|
||||||
self.codes[index].inputTimer = null;
|
|
||||||
}, 300);
|
|
||||||
},
|
|
||||||
setFocus(index) {
|
|
||||||
const refId = `pin-code-input-${index}`;
|
|
||||||
const ref = this.$refs[refId];
|
|
||||||
|
|
||||||
if (ref && ref[0]) {
|
|
||||||
ref[0].focus();
|
|
||||||
ref[0].select();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setPreviousFocus(index) {
|
|
||||||
if (index > 0) {
|
|
||||||
this.setFocus(index - 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setNextFocus(index) {
|
|
||||||
if (index < this.length - 1) {
|
|
||||||
this.setFocus(index + 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeydown(index, event) {
|
|
||||||
if (event.altKey || (event.key.indexOf('F') === 0 && (event.key.length === 2 || event.key.length === 3))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Enter' && this.finalPinCode.length === this.length) {
|
|
||||||
this.$emit('pincode:confirm', this.finalPinCode);
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'ArrowLeft' || (event.shiftKey && event.key === 'Tab')) {
|
|
||||||
this.setPreviousFocus(index);
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'ArrowRight' || (!event.shiftKey && event.key === 'Tab')) {
|
|
||||||
this.setNextFocus(index);
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Home') {
|
|
||||||
this.setFocus(0);
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'End') {
|
|
||||||
this.setFocus(this.length - 1);
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (((event.ctrlKey || event.metaKey) && event.key === 'v') || event.key === 'Paste') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Del') {
|
|
||||||
for (let i = index; i < this.codes.length; i++) {
|
|
||||||
this.codes[i].value = '';
|
|
||||||
this.setInputType(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.code === 'Backspace') {
|
|
||||||
this.setPreviousFocus(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key.length === 1 && '0' <= event.key && event.key <= '9') {
|
|
||||||
this.codes[index].value = event.key;
|
|
||||||
this.setInputType(index);
|
|
||||||
this.setNextFocus(index);
|
|
||||||
|
|
||||||
if (this.finalPinCode.length === this.length) {
|
|
||||||
this.$emit('pincode:confirm', this.finalPinCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
},
|
|
||||||
onPaste(index, event) {
|
|
||||||
if (!event.clipboardData) {
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = event.clipboardData.getData('Text');
|
|
||||||
|
|
||||||
if (!text) {
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.autoFillText(index, text);
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
},
|
|
||||||
onInput(index, event) {
|
|
||||||
if (!event.target.value) {
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.autoFillText(index, event.target.value);
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
|
||||||
|
function init(length: number, value: string): void {
|
||||||
|
codes.value.length = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const code: PinCode = {
|
||||||
|
value: '',
|
||||||
|
inputType: 'tel',
|
||||||
|
inputTimer: null,
|
||||||
|
focused: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (value && value[i]) {
|
||||||
|
code.value = value[i];
|
||||||
|
|
||||||
|
if (props.secure) {
|
||||||
|
code.inputType = 'password';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codes.value.push(code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function autoFillText(index: number, text: string): void {
|
||||||
|
let lastIndex = index;
|
||||||
|
|
||||||
|
for (let i = index, j = 0; i < codes.value.length && j < text.length; i++, j++) {
|
||||||
|
if (text[j] < '0' || text[j] > '9') {
|
||||||
|
codes.value[i].value = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
codes.value[i].value = text[j];
|
||||||
|
setInputType(i);
|
||||||
|
lastIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFocus(lastIndex);
|
||||||
|
|
||||||
|
if (finalPinCode.value.length === length) {
|
||||||
|
emit('pincode:confirm', finalPinCode.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInputType(index: number): void {
|
||||||
|
if (!props.secure) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!codes.value[index].value) {
|
||||||
|
codes.value[index].inputType = 'tel';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codes.value[index].inputTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
codes.value[index].inputTimer = setTimeout(() => {
|
||||||
|
if (codes.value[index].value) {
|
||||||
|
codes.value[index].inputType = 'password';
|
||||||
|
} else {
|
||||||
|
codes.value[index].inputType = 'tel';
|
||||||
|
}
|
||||||
|
|
||||||
|
codes.value[index].inputTimer = null;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFocus(index: number): void {
|
||||||
|
if (pinCodeInputs.value && pinCodeInputs.value[index]) {
|
||||||
|
pinCodeInputs.value[index].focus();
|
||||||
|
pinCodeInputs.value[index].select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPreviousFocus(index: number): void {
|
||||||
|
if (index > 0) {
|
||||||
|
setFocus(index - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNextFocus(index: number): void {
|
||||||
|
if (index < props.length - 1) {
|
||||||
|
setFocus(index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(index: number, event: KeyboardEvent): void {
|
||||||
|
if (event.altKey || (event.key.indexOf('F') === 0 && (event.key.length === 2 || event.key.length === 3))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index <= 0 && (event.shiftKey && event.key === 'Tab')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= props.length - 1 && (!event.shiftKey && event.key === 'Tab')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter' && finalPinCode.value.length === props.length) {
|
||||||
|
emit('pincode:confirm', finalPinCode.value);
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowLeft' || (event.shiftKey && event.key === 'Tab')) {
|
||||||
|
setPreviousFocus(index);
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowRight' || (!event.shiftKey && event.key === 'Tab')) {
|
||||||
|
setNextFocus(index);
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Home') {
|
||||||
|
setFocus(0);
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'End') {
|
||||||
|
setFocus(props.length - 1);
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (((event.ctrlKey || event.metaKey) && event.key === 'v') || event.key === 'Paste') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Del') {
|
||||||
|
for (let i = index; i < codes.value.length; i++) {
|
||||||
|
codes.value[i].value = '';
|
||||||
|
setInputType(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.code === 'Backspace') {
|
||||||
|
setPreviousFocus(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key.length === 1 && '0' <= event.key && event.key <= '9') {
|
||||||
|
codes.value[index].value = event.key;
|
||||||
|
setInputType(index);
|
||||||
|
setNextFocus(index);
|
||||||
|
|
||||||
|
if (props.autoConfirm && finalPinCode.value.length === props.length) {
|
||||||
|
emit('pincode:confirm', finalPinCode.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPaste(index: number, event: ClipboardEvent): void {
|
||||||
|
if (!event.clipboardData) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = event.clipboardData.getData('Text');
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoFillText(index, text);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInput(index: number, event: Event | { target: { value: string }, preventDefault: () => void }): void {
|
||||||
|
if (!event.target || !(event.target as { value: string }).value) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoFillText(index, (event.target as { value: string }).value);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.length, newValue => {
|
||||||
|
init(newValue, props.modelValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.modelValue, newValue => {
|
||||||
|
if (newValue === finalPinCode.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(props.length, newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(codes, () => {
|
||||||
|
emit('update:modelValue', finalPinCode.value);
|
||||||
|
}, {
|
||||||
|
deep: true
|
||||||
|
});
|
||||||
|
|
||||||
|
init(props.length, props.modelValue);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
:label="label" :placeholder="placeholder"
|
:label="label" :placeholder="placeholder"
|
||||||
:persistent-placeholder="!!persistentPlaceholder"
|
:persistent-placeholder="!!persistentPlaceholder"
|
||||||
:rules="enableRules ? rules : []" v-model="currentValue" v-if="!hide"
|
:rules="enableRules ? rules : []" v-model="currentValue" v-if="!hide"
|
||||||
@keydown="onKeyUpDown" @keyup="onKeyUpDown" @paste="onPaste">
|
@keydown="onKeyUpDown" @keyup="onKeyUpDown" @paste="onPaste" @click="onClick">
|
||||||
<template #prepend-inner v-if="currency && prependText">
|
<template #prepend-inner v-if="currency && prependText">
|
||||||
<div>{{ prependText }}</div>
|
<div>{{ prependText }}</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
:label="label" :placeholder="placeholder"
|
:label="label" :placeholder="placeholder"
|
||||||
:persistent-placeholder="!!persistentPlaceholder"
|
:persistent-placeholder="!!persistentPlaceholder"
|
||||||
:rules="enableRules ? rules : []" v-model="currentValue" v-if="hide"
|
:rules="enableRules ? rules : []" v-model="currentValue" v-if="hide"
|
||||||
@keydown="onKeyUpDown" @keyup="onKeyUpDown" @paste="onPaste">
|
@keydown="onKeyUpDown" @keyup="onKeyUpDown" @paste="onPaste" @click="onClick">
|
||||||
<template #prepend-inner v-if="currency && prependText">
|
<template #prepend-inner v-if="currency && prependText">
|
||||||
<div>{{ prependText }}</div>
|
<div>{{ prependText }}</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -29,301 +29,363 @@
|
|||||||
</v-text-field>
|
</v-text-field>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapStores } from 'pinia';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useSettingsStore } from '@/stores/setting.js';
|
|
||||||
import { useUserStore } from '@/stores/user.js';
|
|
||||||
|
|
||||||
import transactionConstants from '@/consts/transaction.js';
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
import { removeAll } from '@/lib/common.js';
|
|
||||||
import logger from '@/lib/logger.js';
|
|
||||||
|
|
||||||
export default {
|
import type { CurrencyPrependAndAppendText } from '@/core/currency.ts';
|
||||||
props: [
|
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
|
||||||
'class',
|
import { removeAll } from '@/lib/common.ts';
|
||||||
'color',
|
import type { ComponentDensity } from '@/lib/ui/desktop.ts';
|
||||||
'density',
|
import logger from '@/lib/logger.ts';
|
||||||
'currency',
|
|
||||||
'showCurrency',
|
|
||||||
'label',
|
|
||||||
'placeholder',
|
|
||||||
'persistentPlaceholder',
|
|
||||||
'disabled',
|
|
||||||
'readonly',
|
|
||||||
'hide',
|
|
||||||
'enableRules',
|
|
||||||
'modelValue'
|
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'update:modelValue'
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
const self = this;
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
return {
|
const props = defineProps<{
|
||||||
currentValue: self.getFormattedValue(userStore, self.modelValue),
|
class?: string;
|
||||||
rules: [
|
color?: string;
|
||||||
(v) => {
|
density?: ComponentDensity;
|
||||||
if (v === '') {
|
currency: string;
|
||||||
return self.$t('Amount value is not number');
|
showCurrency?: boolean;
|
||||||
}
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
persistentPlaceholder?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
hide?: boolean;
|
||||||
|
enableRules?: boolean;
|
||||||
|
flipNegative?: boolean;
|
||||||
|
modelValue: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
try {
|
const emit = defineEmits<{
|
||||||
const val = self.$locale.parseAmount(userStore, v);
|
(e: 'update:modelValue', value: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
if (Number.isNaN(val) || !Number.isFinite(val)) {
|
const {
|
||||||
return self.$t('Amount value is not number');
|
tt,
|
||||||
}
|
getCurrentDecimalSeparator,
|
||||||
|
getCurrentDigitGroupingSymbol,
|
||||||
|
parseAmount,
|
||||||
|
formatAmount,
|
||||||
|
getAmountPrependAndAppendText
|
||||||
|
} = useI18n();
|
||||||
|
|
||||||
return (val >= transactionConstants.minAmountNumber && val <= transactionConstants.maxAmountNumber) || self.$t('Amount value exceeds limitation');
|
const rules = [
|
||||||
} catch (ex) {
|
(v: string) => {
|
||||||
logger.warn('cannot parse amount in amount input, original value is ' + v, ex);
|
if (v === '') {
|
||||||
return self.$t('Amount value is not number');
|
return tt('Amount value is not number');
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useSettingsStore, useUserStore),
|
|
||||||
extraClass() {
|
|
||||||
let finalClass = this.class;
|
|
||||||
|
|
||||||
if (this.color) {
|
try {
|
||||||
finalClass += ` text-${this.color}`;
|
const val = parseAmount(v);
|
||||||
|
|
||||||
|
if (Number.isNaN(val) || !Number.isFinite(val)) {
|
||||||
|
return tt('Amount value is not number');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currency && this.prependText) {
|
return (val >= TRANSACTION_MIN_AMOUNT && val <= TRANSACTION_MAX_AMOUNT) || tt('Amount value exceeds limitation');
|
||||||
finalClass += ` has-pretend-text`;
|
} catch (ex) {
|
||||||
}
|
logger.warn('cannot parse amount in amount input, original value is ' + v, ex);
|
||||||
|
return tt('Amount value is not number');
|
||||||
return finalClass;
|
|
||||||
},
|
|
||||||
prependText() {
|
|
||||||
if (!this.currency || !this.showCurrency) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const texts = this.getDisplayCurrencyPrependAndAppendText();
|
|
||||||
|
|
||||||
if (!texts) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return texts.prependText;
|
|
||||||
},
|
|
||||||
appendText() {
|
|
||||||
if (!this.currency || !this.showCurrency) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const texts = this.getDisplayCurrencyPrependAndAppendText();
|
|
||||||
|
|
||||||
if (!texts) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return texts.appendText;
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
watch: {
|
];
|
||||||
'currency': function () {
|
|
||||||
const newStringValue = this.getFormattedValue(this.userStore, this.modelValue);
|
|
||||||
|
|
||||||
if (!(newStringValue === '0' && this.currentValue === '')) {
|
const currentValue = ref<string>(getInitedFormattedValue(props.modelValue, props.flipNegative));
|
||||||
this.currentValue = newStringValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'modelValue': function (newValue) {
|
|
||||||
const numericCurrentValue = this.$locale.parseAmount(this.userStore, this.currentValue);
|
|
||||||
|
|
||||||
if (newValue !== numericCurrentValue) {
|
const prependText = computed<string | undefined>(() => {
|
||||||
const newStringValue = this.getFormattedValue(this.userStore, newValue);
|
if (!props.currency || !props.showCurrency) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
if (!(newStringValue === '0' && this.currentValue === '')) {
|
const texts = getDisplayCurrencyPrependAndAppendText();
|
||||||
this.currentValue = newStringValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'currentValue': function (newValue) {
|
|
||||||
let finalValue = '';
|
|
||||||
|
|
||||||
if (newValue) {
|
if (!texts) {
|
||||||
const decimalSeparator = this.$locale.getCurrentDecimalSeparator(this.userStore);
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < newValue.length; i++) {
|
return texts.prependText;
|
||||||
if (!('0' <= newValue[i] && newValue[i] <= '9') && newValue[i] !== '-' && newValue[i] !== decimalSeparator) {
|
});
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
finalValue += newValue[i];
|
const appendText = computed<string | undefined>(() => {
|
||||||
}
|
if (!props.currency || !props.showCurrency) {
|
||||||
}
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
if (finalValue !== newValue) {
|
const texts = getDisplayCurrencyPrependAndAppendText();
|
||||||
this.currentValue = finalValue;
|
|
||||||
} else {
|
if (!texts) {
|
||||||
this.$emit('update:modelValue', this.$locale.parseAmount(this.userStore, finalValue));
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return texts.appendText;
|
||||||
|
});
|
||||||
|
|
||||||
|
const extraClass = computed<string>(() => {
|
||||||
|
let finalClass = props.class || '';
|
||||||
|
|
||||||
|
if (props.color) {
|
||||||
|
finalClass += ` text-${props.color}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.currency && prependText.value) {
|
||||||
|
finalClass += ` has-pretend-text`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalClass;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onKeyUpDown(e: KeyboardEvent): void {
|
||||||
|
if (e.altKey || e.ctrlKey || e.metaKey || (e.key.indexOf('F') === 0 && (e.key.length === 2 || e.key.length === 3))
|
||||||
|
|| e.key === 'ArrowLeft' || e.key === 'ArrowRight'
|
||||||
|
|| e.key === 'Home' || e.key === 'End' || e.key === 'Tab'
|
||||||
|
|| e.key === 'Backspace' || e.key === 'Delete' || e.key === 'Del') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.readonly || props.disabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const digitGroupingSymbol = getCurrentDigitGroupingSymbol();
|
||||||
|
const decimalSeparator = getCurrentDecimalSeparator();
|
||||||
|
|
||||||
|
if (!('0' <= e.key && e.key <= '9') && e.key !== '-' && e.key !== decimalSeparator) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
|
||||||
|
let str = target.value;
|
||||||
|
|
||||||
|
if (str.indexOf(digitGroupingSymbol) >= 0) {
|
||||||
|
str = removeAll(str, digitGroupingSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === '-' && str.lastIndexOf('-') > 0) {
|
||||||
|
const lastMinusPos = str.lastIndexOf('-');
|
||||||
|
target.value = str.substring(0, lastMinusPos) + str.substring(lastMinusPos + 1, str.length);
|
||||||
|
currentValue.value = target.value;
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === decimalSeparator && str.indexOf(decimalSeparator) !== str.lastIndexOf(decimalSeparator)) {
|
||||||
|
const lastDecimalSeparatorPos = str.lastIndexOf(decimalSeparator);
|
||||||
|
target.value = str.substring(0, lastDecimalSeparatorPos) + str.substring(lastDecimalSeparatorPos + 1, str.length);
|
||||||
|
currentValue.value = target.value;
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === decimalSeparator && (str.indexOf(decimalSeparator) === 0 || (str.indexOf(decimalSeparator) === 1 && str.charAt(0) === '-'))) {
|
||||||
|
const negative = str.charAt(0) === '-';
|
||||||
|
|
||||||
|
if (negative) {
|
||||||
|
str = str.substring(1);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onKeyUpDown(e) {
|
|
||||||
if (e.altKey || e.ctrlKey || e.metaKey || (e.key.indexOf('F') === 0 && (e.key.length === 2 || e.key.length === 3))
|
|
||||||
|| e.key === 'ArrowLeft' || e.key === 'ArrowRight'
|
|
||||||
|| e.key === 'Home' || e.key === 'End' || e.key === 'Tab'
|
|
||||||
|| e.key === 'Backspace' || e.key === 'Delete' || e.key === 'Del') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const digitGroupingSymbol = this.$locale.getCurrentDigitGroupingSymbol(this.userStore);
|
str = (negative ? '-0' : '0') + str;
|
||||||
const decimalSeparator = this.$locale.getCurrentDecimalSeparator(this.userStore);
|
target.value = str;
|
||||||
|
currentValue.value = target.value;
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!('0' <= e.key && e.key <= '9') && e.key !== '-' && e.key !== decimalSeparator) {
|
let decimalLength = 0;
|
||||||
e.preventDefault();
|
const decimalIndex = str.indexOf(decimalSeparator);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let str = e.target.value;
|
if (decimalIndex >= 0) {
|
||||||
|
decimalLength = str.length - str.indexOf(decimalSeparator) - 1;
|
||||||
|
} else if ((str.startsWith('0') && str.length >= 2) || (str.startsWith('-0') && str.length >= 3)) {
|
||||||
|
const negative = str.charAt(0) === '-';
|
||||||
|
|
||||||
if (str.indexOf(digitGroupingSymbol) >= 0) {
|
if (negative) {
|
||||||
str = removeAll(str, digitGroupingSymbol);
|
str = str.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === '-' && str.lastIndexOf('-') > 0) {
|
while (str.charAt(0) === '0' && (str.length >= 2 || e.key !== '0')) {
|
||||||
const lastMinusPos = str.lastIndexOf('-');
|
str = str.substring(1);
|
||||||
e.target.value = str.substring(0, lastMinusPos) + str.substring(lastMinusPos + 1, str.length);
|
}
|
||||||
this.currentValue = e.target.value;
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === decimalSeparator && str.indexOf(decimalSeparator) !== str.lastIndexOf(decimalSeparator)) {
|
target.value = (negative ? '-' : '') + str;
|
||||||
const lastDecimalSeparatorPos = str.lastIndexOf(decimalSeparator);
|
currentValue.value = target.value;
|
||||||
e.target.value = str.substring(0, lastDecimalSeparatorPos) + str.substring(lastDecimalSeparatorPos + 1, str.length);
|
e.preventDefault();
|
||||||
this.currentValue = e.target.value;
|
return;
|
||||||
e.preventDefault();
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === decimalSeparator && (str.indexOf(decimalSeparator) === 0 || (str.indexOf(decimalSeparator) === 1 && str.charAt(0) === '-'))) {
|
if (decimalLength > 2) {
|
||||||
const negative = str.charAt(0) === '-';
|
target.value = str.substring(0, Math.min(decimalIndex + 3, str.length - 1));
|
||||||
|
currentValue.value = target.value;
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (negative) {
|
try {
|
||||||
str = str.substring(1);
|
const val = parseAmount(str);
|
||||||
}
|
const finalValue = getValidFormattedValue(val, str, decimalIndex >= 0);
|
||||||
|
|
||||||
str = (negative ? '-0' : '0') + str;
|
if (finalValue !== str) {
|
||||||
e.target.value = str;
|
target.value = finalValue;
|
||||||
this.currentValue = e.target.value;
|
currentValue.value = finalValue;
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let decimalLength = 0;
|
|
||||||
let decimalIndex = str.indexOf(decimalSeparator);
|
|
||||||
|
|
||||||
if (decimalIndex >= 0) {
|
|
||||||
decimalLength = str.length - str.indexOf(decimalSeparator) - 1;
|
|
||||||
} else if ((str.startsWith('0') && str.length >= 2) || (str.startsWith('-0') && str.length >= 3)) {
|
|
||||||
const negative = str.charAt(0) === '-';
|
|
||||||
|
|
||||||
if (negative) {
|
|
||||||
str = str.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (str.charAt(0) === '0' && (str.length >= 2 || e.key !== '0')) {
|
|
||||||
str = str.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.target.value = (negative ? '-' : '') + str;
|
|
||||||
this.currentValue = e.target.value;
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decimalLength > 2) {
|
|
||||||
e.target.value = str.substring(0, Math.min(decimalIndex + 3, str.length - 1));
|
|
||||||
this.currentValue = e.target.value;
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const val = this.$locale.parseAmount(this.userStore, str);
|
|
||||||
const finalValue = this.getValidFormattedValue(val, str, decimalIndex >= 0);
|
|
||||||
|
|
||||||
if (finalValue !== str) {
|
|
||||||
e.target.value = finalValue;
|
|
||||||
this.currentValue = finalValue;
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
ex.target.value = '0';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPaste(e) {
|
|
||||||
if (!e.clipboardData) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = e.clipboardData.getData('Text');
|
|
||||||
|
|
||||||
if (!text) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = this.$locale.parseAmount(this.userStore, text);
|
|
||||||
const textualValue = this.getFormattedValue(this.userStore, value);
|
|
||||||
const decimalSeparator = this.$locale.getCurrentDecimalSeparator(this.userStore);
|
|
||||||
const hasDecimalSeparator = text.indexOf(decimalSeparator) >= 0;
|
|
||||||
|
|
||||||
this.currentValue = this.getValidFormattedValue(value, textualValue, hasDecimalSeparator);
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
},
|
}
|
||||||
getValidFormattedValue(value, textualValue, hasDecimalSeparator) {
|
} catch (ex) {
|
||||||
let maxLength = transactionConstants.maxAmountNumber.toString().length;
|
logger.warn('cannot parse amount in amount input, original value is ' + str, ex);
|
||||||
|
target.value = '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (value < 0) {
|
function onPaste(e: ClipboardEvent): void {
|
||||||
maxLength = transactionConstants.minAmountNumber.toString().length;
|
if (!e.clipboardData || props.readonly || props.disabled) {
|
||||||
}
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (value < transactionConstants.minAmountNumber) {
|
const text = e.clipboardData.getData('Text');
|
||||||
return this.getFormattedValue(this.userStore, transactionConstants.minAmountNumber);
|
|
||||||
} else if (value > transactionConstants.maxAmountNumber) {
|
|
||||||
return this.getFormattedValue(this.userStore, transactionConstants.maxAmountNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasDecimalSeparator && textualValue.length > maxLength) {
|
if (!text) {
|
||||||
return textualValue.substring(0, maxLength);
|
e.preventDefault();
|
||||||
} else if (hasDecimalSeparator && textualValue.length > maxLength + 1) {
|
return;
|
||||||
return textualValue.substring(0, maxLength + 1);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return textualValue;
|
const value = parseAmount(text);
|
||||||
},
|
const textualValue = getFormattedValue(value);
|
||||||
getFormattedValue(userStore, value) {
|
const decimalSeparator = getCurrentDecimalSeparator();
|
||||||
if (!Number.isNaN(value) && Number.isFinite(value)) {
|
const hasDecimalSeparator = text.indexOf(decimalSeparator) >= 0;
|
||||||
const digitGroupingSymbol = this.$locale.getCurrentDigitGroupingSymbol(userStore);
|
|
||||||
return removeAll(this.$locale.formatAmount(userStore, value, this.currency), digitGroupingSymbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
return '0';
|
currentValue.value = getValidFormattedValue(value, textualValue, hasDecimalSeparator);
|
||||||
},
|
e.preventDefault();
|
||||||
getDisplayCurrencyPrependAndAppendText() {
|
}
|
||||||
const numericCurrentValue = this.$locale.parseAmount(this.userStore, this.currentValue);
|
|
||||||
const isPlural = numericCurrentValue !== 100 && numericCurrentValue !== -100;
|
|
||||||
|
|
||||||
return this.$locale.getAmountPrependAndAppendText(this.settingsStore, this.userStore, this.currency, isPlural);
|
function onClick(e: MouseEvent): void {
|
||||||
|
if (!props.disabled && !props.readonly && props.modelValue === 0 && e.target instanceof HTMLInputElement) {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
|
||||||
|
if ((!input?.selectionStart && !input?.selectionEnd) || input?.selectionStart === input?.selectionEnd) {
|
||||||
|
input?.select();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getValidFormattedValue(value: number, textualValue: string, hasDecimalSeparator: boolean): string {
|
||||||
|
let maxLength = TRANSACTION_MAX_AMOUNT.toString().length;
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
maxLength = TRANSACTION_MIN_AMOUNT.toString().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < TRANSACTION_MIN_AMOUNT) {
|
||||||
|
return getFormattedValue(TRANSACTION_MIN_AMOUNT);
|
||||||
|
} else if (value > TRANSACTION_MAX_AMOUNT) {
|
||||||
|
return getFormattedValue(TRANSACTION_MAX_AMOUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasDecimalSeparator && textualValue.length > maxLength) {
|
||||||
|
return textualValue.substring(0, maxLength);
|
||||||
|
} else if (hasDecimalSeparator && textualValue.length > maxLength + 1) {
|
||||||
|
return textualValue.substring(0, maxLength + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return textualValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitedFormattedValue(value: number, flipNegative?: boolean): string {
|
||||||
|
if (flipNegative) {
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFormattedValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedValue(value: number): string {
|
||||||
|
if (!Number.isNaN(value) && Number.isFinite(value)) {
|
||||||
|
const digitGroupingSymbol = getCurrentDigitGroupingSymbol();
|
||||||
|
return removeAll(formatAmount(value, props.currency), digitGroupingSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayCurrencyPrependAndAppendText(): CurrencyPrependAndAppendText | null {
|
||||||
|
const numericCurrentValue = parseAmount(currentValue.value);
|
||||||
|
const isPlural = numericCurrentValue !== 100 && numericCurrentValue !== -100;
|
||||||
|
|
||||||
|
return getAmountPrependAndAppendText(props.currency, isPlural);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.currency, () => {
|
||||||
|
const newStringValue = getInitedFormattedValue(props.modelValue, props.flipNegative);
|
||||||
|
|
||||||
|
if (!(newStringValue === '0' && currentValue.value === '')) {
|
||||||
|
currentValue.value = newStringValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.flipNegative, (newValue) => {
|
||||||
|
const newStringValue = getInitedFormattedValue(props.modelValue, newValue);
|
||||||
|
|
||||||
|
if (!(newStringValue === '0' && currentValue.value === '')) {
|
||||||
|
currentValue.value = newStringValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (props.flipNegative) {
|
||||||
|
newValue = -newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericCurrentValue = parseAmount(currentValue.value);
|
||||||
|
|
||||||
|
if (newValue !== numericCurrentValue) {
|
||||||
|
const newStringValue = getFormattedValue(newValue);
|
||||||
|
|
||||||
|
if (!(newStringValue === '0' && currentValue.value === '')) {
|
||||||
|
currentValue.value = newStringValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(currentValue, (newValue) => {
|
||||||
|
let finalValue = '';
|
||||||
|
|
||||||
|
if (newValue) {
|
||||||
|
const decimalSeparator = getCurrentDecimalSeparator();
|
||||||
|
|
||||||
|
for (let i = 0; i < newValue.length; i++) {
|
||||||
|
if (!('0' <= newValue[i] && newValue[i] <= '9') && newValue[i] !== '-' && newValue[i] !== decimalSeparator) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
finalValue += newValue[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalValue !== newValue) {
|
||||||
|
currentValue.value = finalValue;
|
||||||
|
} else {
|
||||||
|
let value: number = parseAmount(finalValue);
|
||||||
|
|
||||||
|
if (props.flipNegative) {
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.text-field-with-colored-label.has-pretend-text .v-field {
|
||||||
|
grid-template-columns: max-content minmax(0, 1fr) min-content min-content;
|
||||||
|
}
|
||||||
|
|
||||||
.text-field-with-colored-label.has-pretend-text .v-field__input {
|
.text-field-with-colored-label.has-pretend-text .v-field__input {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,31 +10,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import { computed } from 'vue';
|
||||||
props: [
|
|
||||||
'disabled',
|
|
||||||
'buttons',
|
|
||||||
'modelValue'
|
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'update:modelValue'
|
|
||||||
],
|
|
||||||
computed: {
|
|
||||||
value: {
|
|
||||||
get: function () {
|
|
||||||
return this.modelValue;
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
if (value === this.modelValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('update:modelValue', value);
|
interface Button {
|
||||||
}
|
name: string;
|
||||||
}
|
value: unknown;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: unknown;
|
||||||
|
buttons: Button[];
|
||||||
|
disabled?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: unknown): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const value = computed<unknown>({
|
||||||
|
get: () => {
|
||||||
|
return props.modelValue;
|
||||||
|
},
|
||||||
|
set: value => {
|
||||||
|
if (value === props.modelValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
>
|
>
|
||||||
<template #selection="{ item }">
|
<template #selection="{ item }">
|
||||||
<v-label class="cursor-pointer" style="padding-top: 3px">
|
<v-label class="cursor-pointer" style="padding-top: 3px">
|
||||||
<v-icon size="28" :icon="icons.square" :color="getFinalColor(item.raw)"/>
|
<v-icon size="28" :icon="mdiSquareRounded" :color="getFinalColor(item.raw)"/>
|
||||||
</v-label>
|
</v-label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -23,12 +23,12 @@
|
|||||||
<div class="text-center" :key="colorInfo.color" v-for="colorInfo in row">
|
<div class="text-center" :key="colorInfo.color" v-for="colorInfo in row">
|
||||||
<div class="cursor-pointer" @click="color = colorInfo.color">
|
<div class="cursor-pointer" @click="color = colorInfo.color">
|
||||||
<v-icon class="ma-2" size="28"
|
<v-icon class="ma-2" size="28"
|
||||||
:icon="icons.square" :color="getFinalColor(colorInfo.color)"
|
:icon="mdiSquareRounded" :color="getFinalColor(colorInfo.color)"
|
||||||
v-if="!modelValue || modelValue !== colorInfo.color" />
|
v-if="!modelValue || modelValue !== colorInfo.color" />
|
||||||
<v-badge class="right-bottom-icon" color="primary"
|
<v-badge class="right-bottom-icon" color="primary"
|
||||||
location="bottom right" offset-x="8" offset-y="8" :icon="icons.checked"
|
location="bottom right" offset-x="8" offset-y="8" :icon="mdiCheck"
|
||||||
v-if="modelValue && modelValue === colorInfo.color">
|
v-if="modelValue && modelValue === colorInfo.color">
|
||||||
<v-icon class="ma-2" size="28" :icon="icons.square" :color="getFinalColor(colorInfo.color)" />
|
<v-icon class="ma-2" size="28" :icon="mdiSquareRounded" :color="getFinalColor(colorInfo.color)" />
|
||||||
</v-badge>
|
</v-badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,74 +38,61 @@
|
|||||||
</v-select>
|
</v-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import colorConstants from '@/consts/color.js';
|
import { ref, computed, useTemplateRef, nextTick } from 'vue';
|
||||||
import { arrayContainsFieldValue } from '@/lib/common.js';
|
|
||||||
import { getColorsInRows } from '@/lib/color.js';
|
import type { ColorValue, ColorInfo } from '@/core/color.ts';
|
||||||
import { scrollToSelectedItem } from '@/lib/ui.desktop.js';
|
import { DEFAULT_ICON_COLOR } from '@/consts/color.ts';
|
||||||
|
import { arrayContainsFieldValue } from '@/lib/common.ts';
|
||||||
|
import { getColorsInRows } from '@/lib/color.ts';
|
||||||
|
import { scrollToSelectedItem } from '@/lib/ui/desktop.ts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
mdiSquareRounded,
|
mdiSquareRounded,
|
||||||
mdiCheck
|
mdiCheck
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
export default {
|
const props = defineProps<{
|
||||||
props: [
|
modelValue: ColorValue;
|
||||||
'modelValue',
|
disabled?: boolean;
|
||||||
'disabled',
|
label?: string;
|
||||||
'label',
|
columnCount?: number;
|
||||||
'columnCount',
|
allColorInfos: ColorValue[];
|
||||||
'allColorInfos'
|
}>();
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'update:modelValue',
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
return {
|
const emit = defineEmits<{
|
||||||
itemPerRow: self.columnCount || 7,
|
(e: 'update:modelValue', value: ColorValue): void;
|
||||||
icons: {
|
}>();
|
||||||
square: mdiSquareRounded,
|
|
||||||
checked: mdiCheck
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
allColorRows() {
|
|
||||||
return getColorsInRows(this.allColorInfos, this.itemPerRow);
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
get: function () {
|
|
||||||
return this.modelValue;
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
this.$emit('update:modelValue', value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
hasSelectedIcon(row) {
|
|
||||||
return arrayContainsFieldValue(row, 'id', this.modelValue);
|
|
||||||
},
|
|
||||||
getFinalColor(color) {
|
|
||||||
if (color && color !== colorConstants.defaultAccountColor) {
|
|
||||||
return '#' + color;
|
|
||||||
} else {
|
|
||||||
return 'var(--default-icon-color)';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMenuStateChanged(state) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
if (state) {
|
const dropdownMenu = useTemplateRef<HTMLElement>('dropdownMenu');
|
||||||
self.$nextTick(() => {
|
const itemPerRow = ref<number>(props.columnCount || 7);
|
||||||
if (self.$refs.dropdownMenu && self.$refs.dropdownMenu.parentElement) {
|
|
||||||
scrollToSelectedItem(self.$refs.dropdownMenu.parentElement, null, '.row-has-selected-item');
|
const allColorRows = computed<ColorInfo[][]>(() => getColorsInRows(props.allColorInfos, itemPerRow.value));
|
||||||
}
|
|
||||||
});
|
const color = computed<ColorValue>({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: ColorValue) => emit('update:modelValue', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
function hasSelectedIcon(row: ColorInfo[]): boolean {
|
||||||
|
return arrayContainsFieldValue(row, 'id', props.modelValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFinalColor(color: ColorValue): string {
|
||||||
|
if (color && color !== DEFAULT_ICON_COLOR) {
|
||||||
|
return '#' + color;
|
||||||
|
} else {
|
||||||
|
return 'var(--default-icon-color)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMenuStateChanged(state: boolean): void {
|
||||||
|
if (state) {
|
||||||
|
nextTick(() => {
|
||||||
|
if (dropdownMenu.value && dropdownMenu.value.parentElement) {
|
||||||
|
scrollToSelectedItem(dropdownMenu.value.parentElement, null, '.row-has-selected-item');
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,84 +7,96 @@
|
|||||||
<v-card-text v-if="textContent" class="pa-4 pb-6">{{ textContent }}</v-card-text>
|
<v-card-text v-if="textContent" class="pa-4 pb-6">{{ textContent }}</v-card-text>
|
||||||
<v-card-actions class="px-4 pb-4">
|
<v-card-actions class="px-4 pb-4">
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn color="gray" @click="cancel">{{ $t('Cancel') }}</v-btn>
|
<v-btn color="gray" @click="cancel">{{ tt('Cancel') }}</v-btn>
|
||||||
<v-btn :color="finalColor" @click="confirm">{{ $t('OK') }}</v-btn>
|
<v-btn :color="finalColor" @click="confirm">{{ tt('OK') }}</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { isString } from '@/lib/common.js';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
export default {
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
props: [
|
|
||||||
'show',
|
|
||||||
'color',
|
|
||||||
'title',
|
|
||||||
'text'
|
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'update:show'
|
|
||||||
],
|
|
||||||
expose: [
|
|
||||||
'open'
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
return {
|
import { isString, isObject } from '@/lib/common.ts';
|
||||||
showState: self.show,
|
|
||||||
titleContent: self.title || self.$t('global.app.title'),
|
const props = defineProps<{
|
||||||
textContent: self.text || '',
|
show?: boolean;
|
||||||
finalColor: self.color || 'primary',
|
color?: string;
|
||||||
resolve: null,
|
title?: string;
|
||||||
reject: null
|
text?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:show', value: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { tt } = useI18n();
|
||||||
|
|
||||||
|
const showState = ref<boolean>(false);
|
||||||
|
const titleContent = ref<string>(props.title || tt('global.app.title'));
|
||||||
|
const textContent = ref<string>(props.text || '');
|
||||||
|
const finalColor = ref<string>(props.color || 'primary');
|
||||||
|
|
||||||
|
let resolveFunc: ((value?: unknown) => void) | null = null;
|
||||||
|
let rejectFunc: ((reason?: unknown) => void) | null = null;
|
||||||
|
|
||||||
|
function open(titleOrText: string, textOrOptions?: string | Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown> {
|
||||||
|
showState.value = true;
|
||||||
|
|
||||||
|
if (!textOrOptions || isObject(textOrOptions)) { // only one parameter or second parameter is options
|
||||||
|
titleContent.value = tt('global.app.title');
|
||||||
|
|
||||||
|
if (!textOrOptions) {
|
||||||
|
textContent.value = tt(titleOrText);
|
||||||
|
} else {
|
||||||
|
const actualOptions = textOrOptions as Record<string, unknown>;
|
||||||
|
textContent.value = tt(titleOrText, actualOptions);
|
||||||
}
|
}
|
||||||
},
|
} else if (isString(textOrOptions)) { // second parameter is text
|
||||||
watch: {
|
if (!options) {
|
||||||
'showState': function (newValue) {
|
titleContent.value = tt(titleOrText);
|
||||||
this.$emit('update:show', newValue);
|
textContent.value = tt(textOrOptions);
|
||||||
}
|
} else {
|
||||||
},
|
titleContent.value = tt(titleOrText, options);
|
||||||
methods: {
|
textContent.value = tt(textOrOptions, options);
|
||||||
open(title, text, options) {
|
|
||||||
this.showState = true;
|
|
||||||
|
|
||||||
if (isString(text)) {
|
|
||||||
this.titleContent = this.$t(title, options);
|
|
||||||
this.textContent = this.$t(text, options);
|
|
||||||
} else {
|
|
||||||
options = text;
|
|
||||||
this.titleContent = this.$t('global.app.title');
|
|
||||||
this.textContent = this.$t(title, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options && options.color) {
|
|
||||||
this.finalColor = options.color || 'primary';
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.resolve = resolve;
|
|
||||||
this.reject = reject;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
confirm() {
|
|
||||||
if (this.resolve) {
|
|
||||||
this.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showState = false;
|
|
||||||
this.$emit('update:show', false);
|
|
||||||
},
|
|
||||||
cancel() {
|
|
||||||
if (this.reject) {
|
|
||||||
this.reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showState = false;
|
|
||||||
this.$emit('update:show', false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options && isString(options['color'])) {
|
||||||
|
finalColor.value = (options['color'] as string) || 'primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
resolveFunc = resolve;
|
||||||
|
rejectFunc = reject;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirm(): void {
|
||||||
|
if (resolveFunc) {
|
||||||
|
resolveFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
showState.value = false;
|
||||||
|
emit('update:show', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel(): void {
|
||||||
|
if (rejectFunc) {
|
||||||
|
rejectFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
showState.value = false;
|
||||||
|
emit('update:show', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(showState, newValue => {
|
||||||
|
emit('update:show', newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<v-autocomplete
|
||||||
|
item-title="displayName"
|
||||||
|
item-value="currencyCode"
|
||||||
|
auto-select-first
|
||||||
|
persistent-placeholder
|
||||||
|
:disabled="disabled"
|
||||||
|
:label="label"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:items="allCurrencies"
|
||||||
|
:no-data-text="tt('No results')"
|
||||||
|
:custom-filter="filterCurrency"
|
||||||
|
v-model="currentCurrencyValue"
|
||||||
|
>
|
||||||
|
<template #append-inner>
|
||||||
|
<small class="text-field-append-text smaller">{{ currentCurrencyValue }}</small>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #item="{ props, item }">
|
||||||
|
<v-list-item :value="item.value" v-bind="props">
|
||||||
|
<template #title>
|
||||||
|
<v-list-item-title>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<v-spacer style="min-width: 40px" />
|
||||||
|
<v-icon :icon="mdiCheck" v-if="currentCurrencyValue === item.raw.currencyCode" />
|
||||||
|
<small class="text-field-append-text" v-if="currentCurrencyValue !== item.raw.currencyCode">{{ item.raw.currencyCode }}</small>
|
||||||
|
</div>
|
||||||
|
</v-list-item-title>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-autocomplete>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import type { LocalizedCurrencyInfo } from '@/core/currency.ts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
mdiCheck
|
||||||
|
} from '@mdi/js';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
disabled?: boolean;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
modelValue: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { tt, getAllCurrencies } = useI18n();
|
||||||
|
|
||||||
|
const allCurrencies = computed<LocalizedCurrencyInfo[]>(() => getAllCurrencies());
|
||||||
|
|
||||||
|
const currentCurrencyValue = computed<string | null>({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: string | null) => {
|
||||||
|
if (value === null) {
|
||||||
|
emit('update:modelValue', '');
|
||||||
|
} else {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function filterCurrency(value: string, query: string, item?: { value: unknown, raw: LocalizedCurrencyInfo }): boolean {
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerCaseFilterContent = query.toLowerCase() || '';
|
||||||
|
|
||||||
|
if (!lowerCaseFilterContent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.raw.displayName.toLowerCase().indexOf(lowerCaseFilterContent) >= 0
|
||||||
|
|| item.raw.currencyCode.toLowerCase().indexOf(lowerCaseFilterContent) >= 0;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
</template>
|
</template>
|
||||||
<v-card-text class="mb-md-4 w-100 d-flex justify-center">
|
<v-card-text class="mb-md-4 w-100 d-flex justify-center">
|
||||||
<vue-date-picker inline enable-seconds auto-apply
|
<vue-date-picker inline enable-seconds auto-apply
|
||||||
ref="datetimepicker"
|
|
||||||
month-name-format="long"
|
month-name-format="long"
|
||||||
six-weeks="center"
|
six-weeks="center"
|
||||||
:clearable="false"
|
:clearable="false"
|
||||||
@@ -39,185 +38,94 @@
|
|||||||
{{ getMonthShortName(text) }}
|
{{ getMonthShortName(text) }}
|
||||||
</template>
|
</template>
|
||||||
<template #am-pm-button="{ toggle, value }">
|
<template #am-pm-button="{ toggle, value }">
|
||||||
<button class="dp__pm_am_button" tabindex="0" @click="toggle">{{ $t(`datetime.${value}.content`) }}</button>
|
<button class="dp__pm_am_button" tabindex="0" @click="toggle">{{ tt(`datetime.${value}.content`) }}</button>
|
||||||
</template>
|
</template>
|
||||||
</vue-date-picker>
|
</vue-date-picker>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-text class="overflow-y-visible">
|
<v-card-text class="overflow-y-visible">
|
||||||
<div class="w-100 d-flex justify-center gap-4">
|
<div class="w-100 d-flex justify-center gap-4">
|
||||||
<v-btn :disabled="!dateRange[0] || !dateRange[1]" @click="confirm">{{ $t('OK') }}</v-btn>
|
<v-btn :disabled="!dateRange[0] || !dateRange[1]" @click="confirm">{{ tt('OK') }}</v-btn>
|
||||||
<v-btn color="secondary" variant="tonal" @click="cancel">{{ $t('Cancel') }}</v-btn>
|
<v-btn color="secondary" variant="tonal" @click="cancel">{{ tt('Cancel') }}</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { computed, watch } from 'vue';
|
||||||
import { useTheme } from 'vuetify';
|
import { useTheme } from 'vuetify';
|
||||||
|
|
||||||
import { mapStores } from 'pinia';
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
import { useUserStore } from '@/stores/user.js';
|
import { type CommonDateRangeSelectionProps, useDateRangeSelectionBase } from '@/components/base/DateRangeSelectionBase.ts';
|
||||||
|
|
||||||
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
|
|
||||||
|
import { ThemeType } from '@/core/theme.ts';
|
||||||
|
|
||||||
import datetimeConstants from '@/consts/datetime.js';
|
|
||||||
import { arrangeArrayWithNewStartIndex } from '@/lib/common.js';
|
|
||||||
import {
|
import {
|
||||||
getCurrentUnixTime,
|
|
||||||
getCurrentYear,
|
|
||||||
getUnixTime,
|
|
||||||
getLocalDatetimeFromUnixTime,
|
getLocalDatetimeFromUnixTime,
|
||||||
getTodayFirstUnixTime,
|
|
||||||
getDummyUnixTimeForLocalUsage,
|
getDummyUnixTimeForLocalUsage,
|
||||||
getActualUnixTimeForStore,
|
|
||||||
getTimezoneOffsetMinutes,
|
getTimezoneOffsetMinutes,
|
||||||
getBrowserTimezoneOffsetMinutes,
|
getBrowserTimezoneOffsetMinutes
|
||||||
getDateRangeByDateType
|
} from '@/lib/datetime.ts';
|
||||||
} from '@/lib/datetime.js';
|
|
||||||
|
|
||||||
export default {
|
interface DesktopDateRangeSelectionProps extends CommonDateRangeSelectionProps {
|
||||||
props: [
|
persistent?: boolean;
|
||||||
'minTime',
|
}
|
||||||
'maxTime',
|
|
||||||
'title',
|
|
||||||
'hint',
|
|
||||||
'persistent',
|
|
||||||
'show'
|
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'update:show',
|
|
||||||
'dateRange:change'
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
const self = this;
|
|
||||||
let minDate = getTodayFirstUnixTime();
|
|
||||||
let maxDate = getCurrentUnixTime();
|
|
||||||
|
|
||||||
if (self.minTime) {
|
const props = defineProps<DesktopDateRangeSelectionProps>();
|
||||||
minDate = self.minTime;
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:show', value: boolean): void;
|
||||||
|
(e: 'dateRange:change', minUnixTime: number, maxUnixTime: number): void;
|
||||||
|
(e: 'error', message: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { tt, getMonthShortName } = useI18n();
|
||||||
|
const { yearRange, dateRange, dayNames, isYearFirst, is24Hour, beginDateTime, endDateTime, presetRanges, getFinalDateRange } = useDateRangeSelectionBase(props);
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
|
||||||
|
const firstDayOfWeek = computed<number>(() => userStore.currentUserFirstDayOfWeek);
|
||||||
|
const showState = computed<boolean>({
|
||||||
|
get: () => props.show || false,
|
||||||
|
set: (value) => emit('update:show', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
function confirm(): void {
|
||||||
|
try {
|
||||||
|
const finalDateRange = getFinalDateRange();
|
||||||
|
|
||||||
|
if (!finalDateRange) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.maxTime) {
|
emit('dateRange:change', finalDateRange.minUnixTime, finalDateRange.maxUnixTime);
|
||||||
maxDate = self.maxTime;
|
} catch (ex: unknown) {
|
||||||
}
|
if (ex instanceof Error) {
|
||||||
|
emit('error', ex.message);
|
||||||
return {
|
|
||||||
yearRange: [
|
|
||||||
2000,
|
|
||||||
getCurrentYear() + 1
|
|
||||||
],
|
|
||||||
dateRange: [
|
|
||||||
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(minDate, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())),
|
|
||||||
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(maxDate, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()))
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useUserStore),
|
|
||||||
showState: {
|
|
||||||
get: function () {
|
|
||||||
return this.show;
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
this.$emit('update:show', value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isDarkMode() {
|
|
||||||
return this.globalTheme.global.name.value === 'dark';
|
|
||||||
},
|
|
||||||
firstDayOfWeek() {
|
|
||||||
return this.userStore.currentUserFirstDayOfWeek;
|
|
||||||
},
|
|
||||||
dayNames() {
|
|
||||||
return arrangeArrayWithNewStartIndex(this.$locale.getAllMinWeekdayNames(), this.firstDayOfWeek);
|
|
||||||
},
|
|
||||||
isYearFirst() {
|
|
||||||
return this.$locale.isLongDateMonthAfterYear(this.userStore);
|
|
||||||
},
|
|
||||||
is24Hour() {
|
|
||||||
return this.$locale.isLongTime24HourFormat(this.userStore);
|
|
||||||
},
|
|
||||||
beginDateTime() {
|
|
||||||
const actualBeginUnixTime = getActualUnixTimeForStore(getUnixTime(this.dateRange[0]), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
|
||||||
return this.$locale.formatUnixTimeToLongDateTime(this.userStore, actualBeginUnixTime);
|
|
||||||
},
|
|
||||||
endDateTime() {
|
|
||||||
const actualEndUnixTime = getActualUnixTimeForStore(getUnixTime(this.dateRange[1]), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
|
||||||
return this.$locale.formatUnixTimeToLongDateTime(this.userStore, actualEndUnixTime);
|
|
||||||
},
|
|
||||||
presetRanges() {
|
|
||||||
const presetRanges = [];
|
|
||||||
|
|
||||||
[
|
|
||||||
datetimeConstants.allDateRanges.Today,
|
|
||||||
datetimeConstants.allDateRanges.LastSevenDays,
|
|
||||||
datetimeConstants.allDateRanges.LastThirtyDays,
|
|
||||||
datetimeConstants.allDateRanges.ThisWeek,
|
|
||||||
datetimeConstants.allDateRanges.ThisMonth,
|
|
||||||
datetimeConstants.allDateRanges.ThisYear
|
|
||||||
].forEach(dateRangeType => {
|
|
||||||
const dateRange = getDateRangeByDateType(dateRangeType.type, this.firstDayOfWeek);
|
|
||||||
|
|
||||||
presetRanges.push({
|
|
||||||
label: this.$t(dateRangeType.name),
|
|
||||||
value: [
|
|
||||||
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dateRange.minTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())),
|
|
||||||
getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(dateRange.maxTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()))
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return presetRanges;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'minTime': function (newValue) {
|
|
||||||
if (newValue) {
|
|
||||||
this.dateRange[0] = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(newValue, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'maxTime': function (newValue) {
|
|
||||||
if (newValue) {
|
|
||||||
this.dateRange[1] = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(newValue, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return {
|
|
||||||
globalTheme: theme
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
confirm() {
|
|
||||||
if (!this.dateRange[0] || !this.dateRange[1]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentMinDate = this.dateRange[0];
|
|
||||||
const currentMaxDate = this.dateRange[1];
|
|
||||||
|
|
||||||
let minUnixTime = getUnixTime(currentMinDate);
|
|
||||||
let maxUnixTime = getUnixTime(currentMaxDate);
|
|
||||||
|
|
||||||
if (minUnixTime < 0 || maxUnixTime < 0) {
|
|
||||||
this.$toast('Date is too early');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
minUnixTime = getActualUnixTimeForStore(minUnixTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
|
||||||
maxUnixTime = getActualUnixTimeForStore(maxUnixTime, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes());
|
|
||||||
|
|
||||||
this.$emit('dateRange:change', minUnixTime, maxUnixTime);
|
|
||||||
},
|
|
||||||
cancel() {
|
|
||||||
this.$emit('update:show', false);
|
|
||||||
},
|
|
||||||
getMonthShortName(month) {
|
|
||||||
return this.$locale.getMonthShortName(month);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel(): void {
|
||||||
|
emit('update:show', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.minTime, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
dateRange.value[0] = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(newValue, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.maxTime, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
dateRange.value[1] = getLocalDatetimeFromUnixTime(getDummyUnixTimeForLocalUsage(newValue, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()));
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<v-select
|
||||||
|
persistent-placeholder
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
:clearable="modelValue ? clearable : false"
|
||||||
|
:label="label"
|
||||||
|
:menu-props="{ 'content-class': 'date-select-menu' }"
|
||||||
|
v-model="dateTime"
|
||||||
|
>
|
||||||
|
<template #selection>
|
||||||
|
<span class="text-truncate cursor-pointer">{{ displayTime }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #no-data>
|
||||||
|
<vue-date-picker inline vertical auto-apply
|
||||||
|
ref="datepicker"
|
||||||
|
month-name-format="long"
|
||||||
|
model-type="yyyy-MM-dd"
|
||||||
|
:clearable="true"
|
||||||
|
:enable-time-picker="false"
|
||||||
|
:dark="isDarkMode"
|
||||||
|
:week-start="firstDayOfWeek"
|
||||||
|
:year-range="yearRange"
|
||||||
|
:day-names="dayNames"
|
||||||
|
:year-first="isYearFirst"
|
||||||
|
v-model="dateTime">
|
||||||
|
<template #month="{ text }">
|
||||||
|
{{ getMonthShortName(text) }}
|
||||||
|
</template>
|
||||||
|
<template #month-overlay-value="{ text }">
|
||||||
|
{{ getMonthShortName(text) }}
|
||||||
|
</template>
|
||||||
|
</vue-date-picker>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useTheme } from 'vuetify';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
|
|
||||||
|
import { ThemeType } from '@/core/theme.ts';
|
||||||
|
import { arrangeArrayWithNewStartIndex } from '@/lib/common.ts';
|
||||||
|
import { getCurrentYear } from '@/lib/datetime.ts';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
clearable?: boolean;
|
||||||
|
label?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const { tt, getAllMinWeekdayNames, getMonthShortName, formatDateToLongDate, isLongDateMonthAfterYear } = useI18n();
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const yearRange = ref<number[]>([
|
||||||
|
2000,
|
||||||
|
getCurrentYear() + 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dateTime = computed<string>({
|
||||||
|
get: () => props.modelValue ?? '',
|
||||||
|
set: (value: string) => emit('update:modelValue', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
|
||||||
|
const firstDayOfWeek = computed<number>(() => userStore.currentUserFirstDayOfWeek);
|
||||||
|
const dayNames = computed<string[]>(() => arrangeArrayWithNewStartIndex(getAllMinWeekdayNames(), firstDayOfWeek.value));
|
||||||
|
const isYearFirst = computed<boolean>(() => isLongDateMonthAfterYear());
|
||||||
|
const displayTime = computed<string>(() => {
|
||||||
|
if (props.modelValue) {
|
||||||
|
return formatDateToLongDate(props.modelValue);
|
||||||
|
} else {
|
||||||
|
return tt('Unspecified');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.date-select-menu {
|
||||||
|
max-height: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-select-menu .dp__menu {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -30,20 +30,23 @@
|
|||||||
{{ getMonthShortName(text) }}
|
{{ getMonthShortName(text) }}
|
||||||
</template>
|
</template>
|
||||||
<template #am-pm-button="{ toggle, value }">
|
<template #am-pm-button="{ toggle, value }">
|
||||||
<button class="dp__pm_am_button" tabindex="0" @click="toggle">{{ $t(`datetime.${value}.content`) }}</button>
|
<button class="dp__pm_am_button" tabindex="0" @click="toggle">{{ tt(`datetime.${value}.content`) }}</button>
|
||||||
</template>
|
</template>
|
||||||
</vue-date-picker>
|
</vue-date-picker>
|
||||||
</template>
|
</template>
|
||||||
</v-select>
|
</v-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
import { useTheme } from 'vuetify';
|
import { useTheme } from 'vuetify';
|
||||||
|
|
||||||
import { mapStores } from 'pinia';
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
import { useUserStore } from '@/stores/user.js';
|
|
||||||
|
|
||||||
import { arrangeArrayWithNewStartIndex } from '@/lib/common.js';
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
|
|
||||||
|
import { ThemeType } from '@/core/theme.ts';
|
||||||
|
import { arrangeArrayWithNewStartIndex } from '@/lib/common.ts';
|
||||||
import {
|
import {
|
||||||
getCurrentYear,
|
getCurrentYear,
|
||||||
getTimezoneOffsetMinutes,
|
getTimezoneOffsetMinutes,
|
||||||
@@ -51,76 +54,52 @@ import {
|
|||||||
getLocalDatetimeFromUnixTime,
|
getLocalDatetimeFromUnixTime,
|
||||||
getActualUnixTimeForStore,
|
getActualUnixTimeForStore,
|
||||||
getUnixTime
|
getUnixTime
|
||||||
} from '@/lib/datetime.js';
|
} from '@/lib/datetime.ts';
|
||||||
|
|
||||||
export default {
|
const props = defineProps<{
|
||||||
props: [
|
modelValue: number;
|
||||||
'modelValue',
|
disabled?: boolean;
|
||||||
'disabled',
|
readonly?: boolean;
|
||||||
'readonly',
|
label?: string;
|
||||||
'label'
|
}>();
|
||||||
],
|
|
||||||
emits: [
|
const emit = defineEmits<{
|
||||||
'update:modelValue',
|
(e: 'update:modelValue', value: number): void;
|
||||||
'error'
|
(e: 'error', message: string): void;
|
||||||
],
|
}>();
|
||||||
data() {
|
|
||||||
return {
|
const theme = useTheme();
|
||||||
yearRange: [
|
const { tt, getAllMinWeekdayNames, getMonthShortName, formatUnixTimeToLongDateTime, isLongDateMonthAfterYear, isLongTime24HourFormat } = useI18n();
|
||||||
2000,
|
|
||||||
getCurrentYear() + 1
|
const userStore = useUserStore();
|
||||||
]
|
|
||||||
}
|
const yearRange = ref<number[]>([
|
||||||
|
2000,
|
||||||
|
getCurrentYear() + 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dateTime = computed<Date>({
|
||||||
|
get: () => {
|
||||||
|
return getLocalDatetimeFromUnixTime(props.modelValue);
|
||||||
},
|
},
|
||||||
computed: {
|
set: (value: Date) => {
|
||||||
...mapStores(useUserStore),
|
const unixTime = getUnixTime(value);
|
||||||
dateTime: {
|
|
||||||
get: function () {
|
|
||||||
return getLocalDatetimeFromUnixTime(this.modelValue);
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
const unixTime = getUnixTime(value);
|
|
||||||
|
|
||||||
if (unixTime < 0) {
|
if (unixTime < 0) {
|
||||||
this.$emit('error', 'Date is too early');
|
emit('error', 'Date is too early');
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('update:modelValue', unixTime);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isDarkMode() {
|
|
||||||
return this.globalTheme.global.name.value === 'dark';
|
|
||||||
},
|
|
||||||
firstDayOfWeek() {
|
|
||||||
return this.userStore.currentUserFirstDayOfWeek;
|
|
||||||
},
|
|
||||||
dayNames() {
|
|
||||||
return arrangeArrayWithNewStartIndex(this.$locale.getAllMinWeekdayNames(), this.firstDayOfWeek);
|
|
||||||
},
|
|
||||||
isYearFirst() {
|
|
||||||
return this.$locale.isLongDateMonthAfterYear(this.userStore);
|
|
||||||
},
|
|
||||||
is24Hour() {
|
|
||||||
return this.$locale.isLongTime24HourFormat(this.userStore);
|
|
||||||
},
|
|
||||||
displayTime() {
|
|
||||||
return this.$locale.formatUnixTimeToLongDateTime(this.userStore, getActualUnixTimeForStore(this.modelValue, getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes()))
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return {
|
emit('update:modelValue', unixTime);
|
||||||
globalTheme: theme
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getMonthShortName(month) {
|
|
||||||
return this.$locale.getMonthShortName(month);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
|
||||||
|
const firstDayOfWeek = computed<number>(() => userStore.currentUserFirstDayOfWeek);
|
||||||
|
const dayNames = computed<string[]>(() => arrangeArrayWithNewStartIndex(getAllMinWeekdayNames(), firstDayOfWeek.value));
|
||||||
|
const isYearFirst = computed<boolean>(() => isLongDateMonthAfterYear());
|
||||||
|
const is24Hour = computed<boolean>(() => isLongTime24HourFormat());
|
||||||
|
const displayTime = computed<string>(() => formatUnixTimeToLongDateTime(getActualUnixTimeForStore(getUnixTime(dateTime.value), getTimezoneOffsetMinutes(), getBrowserTimezoneOffsetMinutes())));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="cursor-pointer" @click="icon = iconInfo.id">
|
<div class="cursor-pointer" @click="icon = iconInfo.id">
|
||||||
<ItemIcon class="ma-2" icon-type="fixed" :icon-id="iconInfo.icon" :color="color" v-if="!modelValue || modelValue !== iconInfo.id" />
|
<ItemIcon class="ma-2" icon-type="fixed" :icon-id="iconInfo.icon" :color="color" v-if="!modelValue || modelValue !== iconInfo.id" />
|
||||||
<v-badge class="right-bottom-icon" color="primary"
|
<v-badge class="right-bottom-icon" color="primary"
|
||||||
location="bottom right" offset-x="8" offset-y="10" :icon="icons.checked"
|
location="bottom right" offset-x="8" offset-y="10" :icon="mdiCheck"
|
||||||
v-if="modelValue && modelValue === iconInfo.id">
|
v-if="modelValue && modelValue === iconInfo.id">
|
||||||
<ItemIcon class="ma-2" icon-type="fixed" :icon-id="iconInfo.icon" :color="color" />
|
<ItemIcon class="ma-2" icon-type="fixed" :icon-id="iconInfo.icon" :color="color" />
|
||||||
</v-badge>
|
</v-badge>
|
||||||
@@ -36,66 +36,54 @@
|
|||||||
</v-select>
|
</v-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { arrayContainsFieldValue } from '@/lib/common.js';
|
import { ref, computed, useTemplateRef, nextTick } from 'vue';
|
||||||
import { getIconsInRows } from '@/lib/icon.js';
|
|
||||||
import { scrollToSelectedItem } from '@/lib/ui.desktop.js';
|
import type { ColorValue } from '@/core/color.ts';
|
||||||
|
import type { IconInfo, IconInfoWithId } from '@/core/icon.ts';
|
||||||
|
import { arrayContainsFieldValue } from '@/lib/common.ts';
|
||||||
|
import { getIconsInRows } from '@/lib/icon.ts';
|
||||||
|
import { scrollToSelectedItem } from '@/lib/ui/desktop.ts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
mdiCheck
|
mdiCheck
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
export default {
|
const props = defineProps<{
|
||||||
props: [
|
modelValue: string;
|
||||||
'modelValue',
|
disabled?: boolean;
|
||||||
'disabled',
|
label?: string;
|
||||||
'label',
|
iconType: string;
|
||||||
'iconType',
|
color: ColorValue;
|
||||||
'color',
|
columnCount?: number;
|
||||||
'columnCount',
|
allIconInfos: Record<string, IconInfo>;
|
||||||
'allIconInfos'
|
}>();
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'update:modelValue',
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
return {
|
const emit = defineEmits<{
|
||||||
itemPerRow: self.columnCount || 7,
|
(e: 'update:modelValue', value: string): void;
|
||||||
icons: {
|
}>();
|
||||||
checked: mdiCheck
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
allIconRows() {
|
|
||||||
return getIconsInRows(this.allIconInfos, this.itemPerRow);
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
get: function () {
|
|
||||||
return this.modelValue;
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
this.$emit('update:modelValue', value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
hasSelectedIcon(row) {
|
|
||||||
return arrayContainsFieldValue(row, 'id', this.modelValue);
|
|
||||||
},
|
|
||||||
onMenuStateChanged(state) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
if (state) {
|
const dropdownMenu = useTemplateRef<HTMLElement>('dropdownMenu');
|
||||||
self.$nextTick(() => {
|
const itemPerRow = ref<number>(props.columnCount || 7);
|
||||||
if (self.$refs.dropdownMenu && self.$refs.dropdownMenu.parentElement) {
|
|
||||||
scrollToSelectedItem(self.$refs.dropdownMenu.parentElement, null, '.row-has-selected-item');
|
const allIconRows = computed<IconInfoWithId[][]>(() => getIconsInRows(props.allIconInfos, itemPerRow.value));
|
||||||
}
|
|
||||||
});
|
const icon = computed<string>({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: string) => emit('update:modelValue', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
function hasSelectedIcon(row: IconInfoWithId[]): boolean {
|
||||||
|
return arrayContainsFieldValue(row, 'id', props.modelValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMenuStateChanged(state: boolean): void {
|
||||||
|
if (state) {
|
||||||
|
nextTick(() => {
|
||||||
|
if (dropdownMenu.value && dropdownMenu.value.parentElement) {
|
||||||
|
scrollToSelectedItem(dropdownMenu.value.parentElement, null, '.row-has-selected-item');
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
</i>
|
</i>
|
||||||
<v-badge class="right-bottom-icon" color="secondary"
|
<v-badge class="right-bottom-icon" color="secondary"
|
||||||
location="bottom right" offset-y="4" :icon="icons.hide"
|
location="bottom right" offset-y="4" :icon="mdiEyeOffOutline"
|
||||||
v-if="hiddenStatus">
|
v-if="hiddenStatus">
|
||||||
<i class="item-icon" :class="classes" :style="style">
|
<i class="item-icon" :class="classes" :style="style">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@@ -11,151 +11,35 @@
|
|||||||
</v-badge>
|
</v-badge>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import iconConstants from '@/consts/icon.js';
|
import { computed } from 'vue';
|
||||||
import colorConstants from '@/consts/color.js';
|
import { type CommonIconProps, useItemIconBase } from '@/components/base/ItemIconBase.ts';
|
||||||
import { isNumber } from '@/lib/common.js';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
mdiEyeOffOutline
|
mdiEyeOffOutline
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
export default {
|
interface DesktopItemIconProps extends CommonIconProps {
|
||||||
props: [
|
class?: string;
|
||||||
'class',
|
hiddenStatus?: boolean;
|
||||||
'iconType',
|
|
||||||
'iconId',
|
|
||||||
'color',
|
|
||||||
'defaultColor',
|
|
||||||
'additionalColorAttr',
|
|
||||||
'size',
|
|
||||||
'hiddenStatus'
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
icons: {
|
|
||||||
hide: mdiEyeOffOutline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
classes() {
|
|
||||||
let allClasses = this.class ? (this.class + ' ') : '';
|
|
||||||
|
|
||||||
if (this.iconType === 'account') {
|
|
||||||
allClasses += this.getAccountIcon(this.iconId);
|
|
||||||
} else if (this.iconType === 'category') {
|
|
||||||
allClasses += this.getCategoryIcon(this.iconId);
|
|
||||||
} else if (this.iconType === 'fixed') {
|
|
||||||
allClasses += this.iconId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return allClasses;
|
|
||||||
},
|
|
||||||
style() {
|
|
||||||
let defaultColor = 'var(--default-icon-color)';
|
|
||||||
|
|
||||||
if (this.defaultColor) {
|
|
||||||
defaultColor = this.defaultColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.iconType === 'account') {
|
|
||||||
return this.getAccountIconStyle(this.color, defaultColor, this.additionalColorAttr);
|
|
||||||
} else if (this.iconType === 'category') {
|
|
||||||
return this.getCategoryIconStyle(this.color, defaultColor, this.additionalColorAttr);
|
|
||||||
} else {
|
|
||||||
return this.getDefaultIconStyle(this.color, defaultColor, this.additionalColorAttr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getAccountIcon(iconId) {
|
|
||||||
if (isNumber(iconId)) {
|
|
||||||
iconId = iconId.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!iconConstants.allAccountIcons[iconId]) {
|
|
||||||
return iconConstants.defaultAccountIcon.icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
return iconConstants.allAccountIcons[iconId].icon;
|
|
||||||
},
|
|
||||||
getCategoryIcon(iconId) {
|
|
||||||
if (isNumber(iconId)) {
|
|
||||||
iconId = iconId.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!iconConstants.allCategoryIcons[iconId]) {
|
|
||||||
return iconConstants.defaultCategoryIcon.icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
return iconConstants.allCategoryIcons[iconId].icon;
|
|
||||||
},
|
|
||||||
getAccountIconStyle(color, defaultColor, additionalColorAttr) {
|
|
||||||
if (color && color !== colorConstants.defaultAccountColor) {
|
|
||||||
color = '#' + color;
|
|
||||||
} else {
|
|
||||||
color = defaultColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret = {
|
|
||||||
color: color
|
|
||||||
};
|
|
||||||
|
|
||||||
if (additionalColorAttr) {
|
|
||||||
ret[additionalColorAttr] = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.size) {
|
|
||||||
ret['font-size'] = this.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
getCategoryIconStyle(color, defaultColor, additionalColorAttr) {
|
|
||||||
if (color && color !== colorConstants.defaultCategoryColor) {
|
|
||||||
color = '#' + color;
|
|
||||||
} else {
|
|
||||||
color = defaultColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret = {
|
|
||||||
color: color
|
|
||||||
};
|
|
||||||
|
|
||||||
if (additionalColorAttr) {
|
|
||||||
ret[additionalColorAttr] = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.size) {
|
|
||||||
ret['font-size'] = this.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
getDefaultIconStyle(color, defaultColor, additionalColorAttr) {
|
|
||||||
if (color && color !== colorConstants.defaultColor) {
|
|
||||||
color = '#' + color;
|
|
||||||
} else {
|
|
||||||
color = defaultColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret = {
|
|
||||||
color: color
|
|
||||||
};
|
|
||||||
|
|
||||||
if (additionalColorAttr) {
|
|
||||||
ret[additionalColorAttr] = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.size) {
|
|
||||||
ret['font-size'] = this.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const props = defineProps<DesktopItemIconProps>();
|
||||||
|
const { style, getAccountIcon, getCategoryIcon } = useItemIconBase(props);
|
||||||
|
|
||||||
|
const classes = computed<string>(() => {
|
||||||
|
let allClasses = props.class ? (props.class + ' ') : '';
|
||||||
|
|
||||||
|
if (props.iconType === 'account') {
|
||||||
|
allClasses += getAccountIcon(props.iconId);
|
||||||
|
} else if (props.iconType === 'category') {
|
||||||
|
allClasses += getCategoryIcon(props.iconId);
|
||||||
|
} else if (props.iconType === 'fixed') {
|
||||||
|
allClasses += props.iconId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allClasses;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<v-select
|
||||||
|
item-title="nativeDisplayName"
|
||||||
|
item-value="languageTag"
|
||||||
|
persistent-placeholder
|
||||||
|
:disabled="disabled"
|
||||||
|
:label="label"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:items="allLanguages"
|
||||||
|
v-model="currentLocaleValue"
|
||||||
|
>
|
||||||
|
<template #item="{ props, item }">
|
||||||
|
<v-list-item :value="item.value" v-bind="props">
|
||||||
|
<template #title>
|
||||||
|
<v-list-item-title>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<v-spacer style="min-width: 40px" />
|
||||||
|
<v-icon :icon="mdiCheck" v-if="isLanguageSelected(item.raw.languageTag)" />
|
||||||
|
<span class="text-field-append-text" v-if="!isLanguageSelected(item.raw.languageTag)">{{ item.raw.displayName }}</span>
|
||||||
|
</div>
|
||||||
|
</v-list-item-title>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { type LanguageSelectBaseProps, type LanguageSelectBaseEmits, useLanguageSelectButtonBase } from '@/components/base/LanguageSelectBase.ts';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
mdiCheck
|
||||||
|
} from '@mdi/js';
|
||||||
|
|
||||||
|
interface DesktopLanguageSelectProps extends LanguageSelectBaseProps {
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<DesktopLanguageSelectProps>();
|
||||||
|
const emit = defineEmits<LanguageSelectBaseEmits>();
|
||||||
|
|
||||||
|
const { getCurrentLanguageTag } = useI18n();
|
||||||
|
|
||||||
|
const {
|
||||||
|
allLanguages,
|
||||||
|
updateLanguage,
|
||||||
|
isLanguageSelected
|
||||||
|
} = useLanguageSelectButtonBase(props, emit);
|
||||||
|
|
||||||
|
const currentLocaleValue = computed<string>({
|
||||||
|
get: () => {
|
||||||
|
if (props.useModelValue) {
|
||||||
|
return props.modelValue ?? '';
|
||||||
|
} else {
|
||||||
|
return getCurrentLanguageTag()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: (value: string) => {
|
||||||
|
updateLanguage(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<v-menu location="bottom" max-height="500">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn variant="text" :disabled="disabled" v-bind="props">{{ currentLanguageName }}</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item :key="lang.languageTag" :value="lang.languageTag" v-for="lang in allLanguages">
|
||||||
|
<v-list-item-title class="cursor-pointer" @click="updateLanguage(lang.languageTag)">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<span>{{ lang.nativeDisplayName }}</span>
|
||||||
|
<v-spacer style="min-width: 40px" />
|
||||||
|
<v-icon :icon="mdiCheck" v-if="isLanguageSelected(lang.languageTag)" />
|
||||||
|
<span class="text-field-append-text" v-if="!isLanguageSelected(lang.languageTag)">{{ lang.displayName }}</span>
|
||||||
|
</div>
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type LanguageSelectBaseProps, type LanguageSelectBaseEmits, useLanguageSelectButtonBase } from '@/components/base/LanguageSelectBase.ts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
mdiCheck
|
||||||
|
} from '@mdi/js';
|
||||||
|
|
||||||
|
const props = defineProps<LanguageSelectBaseProps>();
|
||||||
|
const emit = defineEmits<LanguageSelectBaseEmits>();
|
||||||
|
|
||||||
|
const {
|
||||||
|
allLanguages,
|
||||||
|
currentLanguageName,
|
||||||
|
updateLanguage,
|
||||||
|
isLanguageSelected
|
||||||
|
} = useLanguageSelectButtonBase(props, emit);
|
||||||
|
</script>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
:dark="isDarkMode"
|
:dark="isDarkMode"
|
||||||
:year-range="yearRange"
|
:year-range="yearRange"
|
||||||
:year-first="isYearFirst"
|
:year-first="isYearFirst"
|
||||||
v-model="startTime">
|
v-model="dateRange[0]">
|
||||||
<template #month="{ text }">
|
<template #month="{ text }">
|
||||||
{{ getMonthShortName(text) }}
|
{{ getMonthShortName(text) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
:dark="isDarkMode"
|
:dark="isDarkMode"
|
||||||
:year-range="yearRange"
|
:year-range="yearRange"
|
||||||
:year-first="isYearFirst"
|
:year-first="isYearFirst"
|
||||||
v-model="endTime">
|
v-model="dateRange[1]">
|
||||||
<template #month="{ text }">
|
<template #month="{ text }">
|
||||||
{{ getMonthShortName(text) }}
|
{{ getMonthShortName(text) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -55,131 +55,85 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-text class="overflow-y-visible">
|
<v-card-text class="overflow-y-visible">
|
||||||
<div class="w-100 d-flex justify-center gap-4">
|
<div class="w-100 d-flex justify-center gap-4">
|
||||||
<v-btn :disabled="!startTime || !endTime" @click="confirm">{{ $t('OK') }}</v-btn>
|
<v-btn :disabled="!dateRange[0] || !dateRange[1]" @click="confirm">{{ tt('OK') }}</v-btn>
|
||||||
<v-btn color="secondary" variant="tonal" @click="cancel">{{ $t('Cancel') }}</v-btn>
|
<v-btn color="secondary" variant="tonal" @click="cancel">{{ tt('Cancel') }}</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { computed, watch } from 'vue';
|
||||||
import { useTheme } from 'vuetify';
|
import { useTheme } from 'vuetify';
|
||||||
|
|
||||||
import { mapStores } from 'pinia';
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
import { useUserStore } from '@/stores/user.js';
|
import { type CommonMonthRangeSelectionProps, useMonthRangeSelectionBase } from '@/components/base/MonthRangeSelectionBase.ts';
|
||||||
|
|
||||||
import {
|
import { ThemeType } from '@/core/theme.ts';
|
||||||
getYearMonthObjectFromString,
|
import { getYearMonthObjectFromString } from '@/lib/datetime.ts';
|
||||||
getYearMonthStringFromObject,
|
|
||||||
getCurrentUnixTime,
|
|
||||||
getCurrentYear,
|
|
||||||
getThisYearFirstUnixTime,
|
|
||||||
getYearMonthFirstUnixTime,
|
|
||||||
getYearMonthLastUnixTime
|
|
||||||
} from '@/lib/datetime.js';
|
|
||||||
|
|
||||||
export default {
|
interface DesktopMonthRangeSelectionProps extends CommonMonthRangeSelectionProps {
|
||||||
props: [
|
persistent?: boolean;
|
||||||
'minTime',
|
}
|
||||||
'maxTime',
|
|
||||||
'title',
|
|
||||||
'hint',
|
|
||||||
'persistent',
|
|
||||||
'show'
|
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'update:show',
|
|
||||||
'dateRange:change'
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
const self = this;
|
|
||||||
let minDate = getThisYearFirstUnixTime();
|
|
||||||
let maxDate = getCurrentUnixTime();
|
|
||||||
|
|
||||||
if (self.minTime) {
|
const props = defineProps<DesktopMonthRangeSelectionProps>();
|
||||||
minDate = getYearMonthObjectFromString(self.minTime);
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:show', value: boolean): void;
|
||||||
|
(e: 'dateRange:change', minYearMonth: string, maxYearMonth: string): void;
|
||||||
|
(e: 'error', message: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { tt, getMonthShortName } = useI18n();
|
||||||
|
const { yearRange, dateRange, isYearFirst, beginDateTime, endDateTime, getFinalMonthRange } = useMonthRangeSelectionBase(props);
|
||||||
|
|
||||||
|
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
|
||||||
|
const showState = computed<boolean>({
|
||||||
|
get: () => props.show || false,
|
||||||
|
set: (value) => emit('update:show', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
function confirm(): void {
|
||||||
|
try {
|
||||||
|
const finalMonthRange = getFinalMonthRange();
|
||||||
|
|
||||||
|
if (!finalMonthRange) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.maxTime) {
|
emit('dateRange:change', finalMonthRange.minYearMonth, finalMonthRange.maxYearMonth);
|
||||||
maxDate = getYearMonthObjectFromString(self.maxTime);
|
} catch (ex: unknown) {
|
||||||
}
|
if (ex instanceof Error) {
|
||||||
|
emit('error', ex.message);
|
||||||
return {
|
|
||||||
yearRange: [
|
|
||||||
2000,
|
|
||||||
getCurrentYear() + 1
|
|
||||||
],
|
|
||||||
startTime: minDate,
|
|
||||||
endTime: maxDate
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useUserStore),
|
|
||||||
showState: {
|
|
||||||
get: function () {
|
|
||||||
return this.show;
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
this.$emit('update:show', value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isDarkMode() {
|
|
||||||
return this.globalTheme.global.name.value === 'dark';
|
|
||||||
},
|
|
||||||
isYearFirst() {
|
|
||||||
return this.$locale.isLongDateMonthAfterYear(this.userStore);
|
|
||||||
},
|
|
||||||
beginDateTime() {
|
|
||||||
return this.$locale.formatUnixTimeToLongYearMonth(this.userStore, getYearMonthFirstUnixTime(this.startTime));
|
|
||||||
},
|
|
||||||
endDateTime() {
|
|
||||||
return this.$locale.formatUnixTimeToLongYearMonth(this.userStore, getYearMonthLastUnixTime(this.endTime));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'minTime': function (newValue) {
|
|
||||||
if (newValue) {
|
|
||||||
this.startTime = getYearMonthObjectFromString(newValue);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'maxTime': function (newValue) {
|
|
||||||
if (newValue) {
|
|
||||||
this.endTime = getYearMonthObjectFromString(newValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return {
|
|
||||||
globalTheme: theme
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
confirm() {
|
|
||||||
if (!this.startTime || !this.endTime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.startTime.year <= 0 || this.startTime.month < 0 || this.endTime.year <= 0 || this.endTime.month < 0) {
|
|
||||||
this.$toast('Date is too early');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const minYearMonth = getYearMonthStringFromObject(this.startTime);
|
|
||||||
const maxYearMonth = getYearMonthStringFromObject(this.endTime);
|
|
||||||
|
|
||||||
this.$emit('dateRange:change', minYearMonth, maxYearMonth);
|
|
||||||
},
|
|
||||||
cancel() {
|
|
||||||
this.$emit('update:show', false);
|
|
||||||
},
|
|
||||||
getMonthShortName(month) {
|
|
||||||
return this.$locale.getMonthShortName(month);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel(): void {
|
||||||
|
emit('update:show', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.minTime, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
const yearMonth = getYearMonthObjectFromString(newValue);
|
||||||
|
|
||||||
|
if (yearMonth) {
|
||||||
|
dateRange.value[0] = yearMonth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.maxTime, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
const yearMonth = getYearMonthObjectFromString(newValue);
|
||||||
|
|
||||||
|
if (yearMonth) {
|
||||||
|
dateRange.value[1] = yearMonth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<v-pagination :density="density"
|
||||||
|
:disabled="disabled"
|
||||||
|
:total-visible="totalVisible ?? 7"
|
||||||
|
:length="totalPageCount"
|
||||||
|
v-model="currentPage">
|
||||||
|
<template #item="{ key, page, isActive }">
|
||||||
|
<v-btn variant="text"
|
||||||
|
:density="density"
|
||||||
|
:disabled="disabled"
|
||||||
|
:icon="true"
|
||||||
|
:color="isActive ? 'primary' : 'default'"
|
||||||
|
@click="currentPage = parseInt(page)"
|
||||||
|
v-if="page !== '...'"
|
||||||
|
>
|
||||||
|
<span>{{ page }}</span>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn variant="text"
|
||||||
|
color="default"
|
||||||
|
:density="density"
|
||||||
|
:disabled="disabled"
|
||||||
|
:icon="true"
|
||||||
|
v-if="page === '...'"
|
||||||
|
>
|
||||||
|
<span>{{ page }}</span>
|
||||||
|
<v-menu activator="parent"
|
||||||
|
:disabled="disabled"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
v-model="showMenus[key]">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item class="text-sm" :density="density">
|
||||||
|
<v-list-item-title class="cursor-pointer">
|
||||||
|
<v-autocomplete width="100"
|
||||||
|
item-title="page"
|
||||||
|
item-value="page"
|
||||||
|
auto-select-first="exact"
|
||||||
|
:density="density"
|
||||||
|
:items="allPages"
|
||||||
|
:no-data-text="tt('No results')"
|
||||||
|
v-model="currentPage"/>
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-pagination>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import type { ComponentDensity } from '@/lib/ui/desktop.ts';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
density?: ComponentDensity;
|
||||||
|
disabled?: boolean;
|
||||||
|
totalPageCount: number;
|
||||||
|
totalVisible?: number;
|
||||||
|
modelValue: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { tt } = useI18n();
|
||||||
|
|
||||||
|
const showMenus = ref<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const allPages = computed<{ page: number }[]>(() => {
|
||||||
|
const pages = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= props.totalPageCount; i++) {
|
||||||
|
pages.push({
|
||||||
|
page: i
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentPage = computed<number>({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => {
|
||||||
|
if (value && value >= 1 && value <= props.totalPageCount) {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
|
||||||
|
for (const key in showMenus.value) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(showMenus.value, key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
showMenus.value[key] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
+233
-269
@@ -3,295 +3,259 @@
|
|||||||
@click="clickItem" @legendselectchanged="onLegendSelectChanged" />
|
@click="clickItem" @legendselectchanged="onLegendSelectChanged" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
import { useTheme } from 'vuetify';
|
import { useTheme } from 'vuetify';
|
||||||
|
|
||||||
import { mapStores } from 'pinia';
|
import type { ECElementEvent } from 'echarts/core';
|
||||||
import { useSettingsStore } from '@/stores/setting.js';
|
import type { CallbackDataParams } from 'echarts/types/dist/shared';
|
||||||
import { useUserStore } from '@/stores/user.js';
|
|
||||||
|
|
||||||
import colorConstants from '@/consts/color.js';
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
import { formatPercent } from '@/lib/numeral.js';
|
import { type CommonPieChartDataItem, type CommonPieChartProps, usePieChartBase } from '@/components/base/PieChartBase.ts'
|
||||||
|
|
||||||
export default {
|
import type { ColorValue } from '@/core/color.ts';
|
||||||
props: [
|
import { ThemeType } from '@/core/theme.ts';
|
||||||
'skeleton',
|
import { DEFAULT_ICON_COLOR } from '@/consts/color.ts';
|
||||||
'items',
|
|
||||||
'idField',
|
|
||||||
'nameField',
|
|
||||||
'valueField',
|
|
||||||
'percentField',
|
|
||||||
'colorField',
|
|
||||||
'hiddenField',
|
|
||||||
'minValidPercent',
|
|
||||||
'defaultCurrency',
|
|
||||||
'showValue',
|
|
||||||
'enableClickItem'
|
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'click'
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selectedLegends: null,
|
|
||||||
selectedIndex: 0
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useSettingsStore, useUserStore),
|
|
||||||
isDarkMode() {
|
|
||||||
return this.globalTheme.global.name.value === 'dark';
|
|
||||||
},
|
|
||||||
itemsMap: function () {
|
|
||||||
const map = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < this.items.length; i++) {
|
interface DesktopPieChartDataItem extends CommonPieChartDataItem {
|
||||||
const item = this.items[i];
|
itemStyle: {
|
||||||
let id = '';
|
color: ColorValue;
|
||||||
|
};
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.idField && item[this.idField]) {
|
const props = defineProps<CommonPieChartProps>();
|
||||||
id = item[this.idField];
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'click', value: Record<string, unknown>): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { formatAmountWithCurrency } = useI18n();
|
||||||
|
const { selectedIndex, validItems } = usePieChartBase(props);
|
||||||
|
|
||||||
|
const selectedLegends = ref<Record<string, boolean> | null>(null);
|
||||||
|
|
||||||
|
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
|
||||||
|
|
||||||
|
const itemsMap = computed<Record<string, Record<string, unknown>>>(() => {
|
||||||
|
const map: Record<string, Record<string, unknown>> = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < props.items.length; i++) {
|
||||||
|
const item = props.items[i];
|
||||||
|
let id = '';
|
||||||
|
|
||||||
|
if (props.idField && item[props.idField]) {
|
||||||
|
id = item[props.idField] as string;
|
||||||
|
} else {
|
||||||
|
id = item[props.nameField] as string;;
|
||||||
|
}
|
||||||
|
|
||||||
|
map[id] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
|
const seriesData = computed<DesktopPieChartDataItem[]>(() => {
|
||||||
|
const ret: DesktopPieChartDataItem[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < validItems.value.length; i++) {
|
||||||
|
const item = validItems.value[i];
|
||||||
|
ret.push({
|
||||||
|
...item,
|
||||||
|
itemStyle: {
|
||||||
|
color: getColor(item.color),
|
||||||
|
},
|
||||||
|
selected: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasUnselectedItem = computed<boolean>(() => {
|
||||||
|
for (let i = 0; i < validItems.value.length; i++) {
|
||||||
|
const item = validItems.value[i];
|
||||||
|
|
||||||
|
if (selectedLegends.value && !selectedLegends.value[item.id]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstItemAndHalfCurrentItemTotalPercent = computed<number>(() => {
|
||||||
|
let totalValue = 0;
|
||||||
|
let firstValue = null;
|
||||||
|
let firstToCurrentTotalValue = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < validItems.value.length; i++) {
|
||||||
|
const item = validItems.value[i];
|
||||||
|
|
||||||
|
if (selectedLegends.value && !selectedLegends.value[item.id]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstValue === null) {
|
||||||
|
firstValue = item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstValue !== null) {
|
||||||
|
if (i < selectedIndex.value) {
|
||||||
|
firstToCurrentTotalValue += item.value;
|
||||||
|
} else if (i === selectedIndex.value) {
|
||||||
|
firstToCurrentTotalValue += item.value / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalValue += item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstToCurrentTotalValue && totalValue > 0) {
|
||||||
|
return firstToCurrentTotalValue / totalValue;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartOptions = computed<object>(() => {
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
backgroundColor: isDarkMode.value ? '#333' : '#fff',
|
||||||
|
borderColor: isDarkMode.value ? '#333' : '#fff',
|
||||||
|
textStyle: {
|
||||||
|
color: isDarkMode.value ? '#eee' : '#333'
|
||||||
|
},
|
||||||
|
formatter: (params: CallbackDataParams) => {
|
||||||
|
const dataItem = params.data as DesktopPieChartDataItem;
|
||||||
|
const name = dataItem ? dataItem.displayName : '';
|
||||||
|
const value = dataItem ? dataItem.displayValue : formatAmountWithCurrency(params.value as number);
|
||||||
|
let percent = dataItem ? dataItem.displayPercent : (params.percent + '%');
|
||||||
|
|
||||||
|
if (hasUnselectedItem.value) {
|
||||||
|
percent = params.percent + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
let tooltip = `<div><span class="chart-pointer" style="background-color: ${params.color}"></span>`;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
tooltip += `<span>${name}</span><br/><span>${value} (${percent})</span>`;
|
||||||
} else {
|
} else {
|
||||||
id = item[this.nameField];
|
tooltip += `<span>${value} (${percent})</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
map[id] = item;
|
tooltip += '</div>';
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
return tooltip;
|
||||||
},
|
|
||||||
validItems: function () {
|
|
||||||
let totalValidValue = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.items.length; i++) {
|
|
||||||
const item = this.items[i];
|
|
||||||
|
|
||||||
if (item[this.valueField] && item[this.valueField] > 0 && (!this.hiddenField || !item[this.hiddenField])) {
|
|
||||||
totalValidValue += item[this.valueField];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validItems = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < this.items.length; i++) {
|
|
||||||
const item = this.items[i];
|
|
||||||
|
|
||||||
if (item[this.valueField] && item[this.valueField] > 0 &&
|
|
||||||
(!this.hiddenField || !item[this.hiddenField]) &&
|
|
||||||
(!this.minValidPercent || item[this.valueField] / totalValidValue > this.minValidPercent)) {
|
|
||||||
const finalItem = {
|
|
||||||
id: (this.idField && item[this.idField]) ? item[this.idField] : item[this.nameField],
|
|
||||||
name: (this.idField && item[this.idField]) ? item[this.idField] : item[this.nameField],
|
|
||||||
displayName: item[this.nameField],
|
|
||||||
value: item[this.valueField],
|
|
||||||
percent: (item[this.percentField] > 0 || item[this.percentField] === 0 || item[this.percentField] === '0') ? item[this.percentField] : (item[this.valueField] / totalValidValue * 100),
|
|
||||||
actualPercent: item[this.valueField] / totalValidValue,
|
|
||||||
itemStyle: {
|
|
||||||
color: this.getColor(item[this.colorField] ? item[this.colorField] : colorConstants.defaultChartColors[validItems.length % colorConstants.defaultChartColors.length]),
|
|
||||||
},
|
|
||||||
selected: true,
|
|
||||||
sourceItem: item
|
|
||||||
};
|
|
||||||
|
|
||||||
finalItem.displayPercent = formatPercent(finalItem.percent, 2, '<0.01');
|
|
||||||
finalItem.displayValue = this.getDisplayCurrency(finalItem.value, this.defaultCurrency);
|
|
||||||
|
|
||||||
validItems.push(finalItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return validItems;
|
|
||||||
},
|
|
||||||
hasUnselectedItem: function () {
|
|
||||||
for (let i = 0; i < this.validItems.length; i++) {
|
|
||||||
const item = this.validItems[i];
|
|
||||||
|
|
||||||
if (this.selectedLegends && !this.selectedLegends[item.id]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
firstItemAndHalfCurrentItemTotalPercent: function () {
|
|
||||||
let totalValue = 0;
|
|
||||||
let firstValue = null;
|
|
||||||
let firstToCurrentTotalValue = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.validItems.length; i++) {
|
|
||||||
const item = this.validItems[i];
|
|
||||||
|
|
||||||
if (this.selectedLegends && !this.selectedLegends[item.id]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstValue === null) {
|
|
||||||
firstValue = item.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstValue !== null) {
|
|
||||||
if (i < this.selectedIndex) {
|
|
||||||
firstToCurrentTotalValue += item.value;
|
|
||||||
} else if (i === this.selectedIndex) {
|
|
||||||
firstToCurrentTotalValue += item.value / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalValue += item.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstToCurrentTotalValue && totalValue > 0) {
|
|
||||||
return firstToCurrentTotalValue / totalValue;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
chartOptions: function () {
|
legend: {
|
||||||
const self = this;
|
orient: 'horizontal',
|
||||||
|
data: validItems.value.map(item => item.name),
|
||||||
return {
|
selected: selectedLegends.value,
|
||||||
tooltip: {
|
textStyle: {
|
||||||
trigger: 'item',
|
color: isDarkMode.value ? '#eee' : '#333'
|
||||||
backgroundColor: self.isDarkMode ? '#333' : '#fff',
|
},
|
||||||
borderColor: self.isDarkMode ? '#333' : '#fff',
|
formatter: (id: string) => {
|
||||||
textStyle: {
|
const item = itemsMap.value[id];
|
||||||
color: self.isDarkMode ? '#eee' : '#333'
|
return item && props.nameField && item[props.nameField] ? item[props.nameField] as string : id;
|
||||||
},
|
}
|
||||||
formatter: params => {
|
},
|
||||||
const name = params.data ? params.data.displayName : '';
|
series: [
|
||||||
const value = params.data ? params.data.displayValue : self.getDisplayCurrency(params.value);
|
{
|
||||||
let percent = params.data ? params.data.displayPercent : (params.percent + '%');
|
type: 'pie',
|
||||||
|
data: seriesData.value,
|
||||||
if (self.hasUnselectedItem) {
|
top: 50,
|
||||||
percent = params.percent + '%';
|
startAngle: -90 + firstItemAndHalfCurrentItemTotalPercent.value * 360,
|
||||||
}
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
let tooltip = `<div><span class="chart-pointer" style="background-color: ${params.color}"></span>`;
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
if (name) {
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
tooltip += `<span>${name}</span><br/><span>${value} (${percent})</span>`;
|
|
||||||
} else {
|
|
||||||
tooltip += `<span>${value} (${percent})</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltip += '</div>';
|
|
||||||
|
|
||||||
return tooltip;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
label: {
|
||||||
orient: 'horizontal',
|
color: isDarkMode.value ? '#eee' : '#333',
|
||||||
data: self.validItems.map(item => item.name),
|
formatter: (params: CallbackDataParams) => {
|
||||||
selected: self.selectedLegends,
|
const dataItem = params.data as DesktopPieChartDataItem;
|
||||||
textStyle: {
|
return dataItem ? dataItem.displayName : '';
|
||||||
color: self.isDarkMode ? '#eee' : '#333'
|
|
||||||
},
|
|
||||||
formatter: id => {
|
|
||||||
return self.itemsMap[id] && self.nameField && self.itemsMap[id][self.nameField] ? self.itemsMap[id][self.nameField] : id;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
series: [
|
animation: !props.skeleton
|
||||||
{
|
}
|
||||||
type: 'pie',
|
],
|
||||||
data: self.validItems,
|
media: [
|
||||||
top: 50,
|
{
|
||||||
startAngle: -90 + self.firstItemAndHalfCurrentItemTotalPercent * 360,
|
query: {
|
||||||
emphasis: {
|
minWidth: 600,
|
||||||
itemStyle: {
|
},
|
||||||
shadowBlur: 10,
|
option: {
|
||||||
shadowOffsetX: 0,
|
legend: {
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
orient: 'vertical',
|
||||||
}
|
left: 'left'
|
||||||
},
|
},
|
||||||
label: {
|
series: [
|
||||||
color: self.isDarkMode ? '#eee' : '#333',
|
{
|
||||||
formatter: params => {
|
type: 'pie',
|
||||||
return params.data ? params.data.displayName : '';
|
top: 0
|
||||||
}
|
|
||||||
},
|
|
||||||
animation: !self.skeleton
|
|
||||||
}
|
|
||||||
],
|
|
||||||
media: [
|
|
||||||
{
|
|
||||||
query: {
|
|
||||||
minWidth: 600
|
|
||||||
},
|
|
||||||
option: {
|
|
||||||
legend: {
|
|
||||||
orient: 'vertical',
|
|
||||||
left: 'left'
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'pie',
|
|
||||||
top: 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
]
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function getColor(color: string): ColorValue {
|
||||||
|
if (color && color !== DEFAULT_ICON_COLOR) {
|
||||||
|
color = '#' + color;
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickItem(e: ECElementEvent): void {
|
||||||
|
if (!props.enableClickItem || e.componentType !== 'series' || e.seriesType !=='pie') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.event && e.event.target && e.event.target.currentStates && e.event.target.currentStates[0] && e.event.target.currentStates[0] === 'emphasis') {
|
||||||
|
selectedIndex.value = e.dataIndex;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = e.data as object;
|
||||||
|
|
||||||
|
if ('sourceItem' in data) {
|
||||||
|
emit('click', data.sourceItem as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLegendSelectChanged(e: { selected: Record<string, boolean> }): void {
|
||||||
|
selectedLegends.value = e.selected;
|
||||||
|
const selectedItem = validItems.value[selectedIndex.value];
|
||||||
|
|
||||||
|
if (!selectedItem || !selectedLegends.value[selectedItem.id]) {
|
||||||
|
let newSelectedIndex = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < validItems.value.length; i++) {
|
||||||
|
const item = validItems.value[i];
|
||||||
|
|
||||||
|
if (selectedLegends.value[item.id]) {
|
||||||
|
newSelectedIndex = i;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'items': function () {
|
|
||||||
this.selectedIndex = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return {
|
selectedIndex.value = newSelectedIndex;
|
||||||
globalTheme: theme
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
clickItem: function (e) {
|
|
||||||
if (!this.enableClickItem || e.componentType !== 'series' || e.seriesType !=='pie') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.event && e.event.target && e.event.target.currentStates && e.event.target.currentStates[0] && e.event.target.currentStates[0] === 'emphasis') {
|
|
||||||
this.selectedIndex = e.dataIndex;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!e.data || !e.data.sourceItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('click', e.data.sourceItem);
|
|
||||||
},
|
|
||||||
onLegendSelectChanged: function (e) {
|
|
||||||
this.selectedLegends = e.selected;
|
|
||||||
const selectedItem = this.validItems[this.selectedIndex];
|
|
||||||
|
|
||||||
if (!selectedItem || !this.selectedLegends[selectedItem.id]) {
|
|
||||||
let newSelectedIndex = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.validItems.length; i++) {
|
|
||||||
const item = this.validItems[i];
|
|
||||||
|
|
||||||
if (this.selectedLegends[item.id]) {
|
|
||||||
newSelectedIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectedIndex = newSelectedIndex;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getColor: function (color) {
|
|
||||||
if (color && color !== colorConstants.defaultColor) {
|
|
||||||
color = '#' + color;
|
|
||||||
}
|
|
||||||
|
|
||||||
return color;
|
|
||||||
},
|
|
||||||
getDisplayCurrency(value, currencyCode) {
|
|
||||||
return this.$locale.formatAmountWithCurrency(this.settingsStore, this.userStore, value, currencyCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -25,11 +25,11 @@
|
|||||||
</v-list>
|
</v-list>
|
||||||
</div>
|
</div>
|
||||||
<div class="schedule-frequency-value-container">
|
<div class="schedule-frequency-value-container">
|
||||||
<v-list v-if="frequencyType === allTemplateScheduledFrequencyTypes.Disabled.type">
|
<v-list v-if="frequencyType === ScheduledTemplateFrequencyType.Disabled.type">
|
||||||
<v-list-item :title="$t('None')"></v-list-item>
|
<v-list-item :title="tt('None')"></v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
<v-list select-strategy="classic" v-model:selected="frequencyValue"
|
<v-list select-strategy="classic" v-model:selected="frequencyValue"
|
||||||
v-else-if="frequencyType === allTemplateScheduledFrequencyTypes.Weekly.type">
|
v-else-if="frequencyType === ScheduledTemplateFrequencyType.Weekly.type">
|
||||||
<v-list-item :key="weekDay.type" :value="weekDay.type" :title="weekDay.displayName"
|
<v-list-item :key="weekDay.type" :value="weekDay.type" :title="weekDay.displayName"
|
||||||
:class="{ 'frequency-value-selected v-list-item--active text-primary': isFrequencyValueSelected(weekDay.type) }"
|
:class="{ 'frequency-value-selected v-list-item--active text-primary': isFrequencyValueSelected(weekDay.type) }"
|
||||||
v-for="weekDay in allWeekDays">
|
v-for="weekDay in allWeekDays">
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
<v-list select-strategy="classic" v-model:selected="frequencyValue"
|
<v-list select-strategy="classic" v-model:selected="frequencyValue"
|
||||||
v-else-if="frequencyType === allTemplateScheduledFrequencyTypes.Monthly.type">
|
v-else-if="frequencyType === ScheduledTemplateFrequencyType.Monthly.type">
|
||||||
<v-list-item :key="monthDay.day" :value="monthDay.day" :title="monthDay.displayName"
|
<v-list-item :key="monthDay.day" :value="monthDay.day" :title="monthDay.displayName"
|
||||||
:class="{ 'frequency-value-selected v-list-item--active text-primary': isFrequencyValueSelected(monthDay.day) }"
|
:class="{ 'frequency-value-selected v-list-item--active text-primary': isFrequencyValueSelected(monthDay.day) }"
|
||||||
v-for="monthDay in allAvailableMonthDays">
|
v-for="monthDay in allAvailableMonthDays">
|
||||||
@@ -54,137 +54,100 @@
|
|||||||
</v-select>
|
</v-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapStores } from 'pinia';
|
import { ref, computed, nextTick, useTemplateRef } from 'vue';
|
||||||
import { useUserStore } from '@/stores/user.js';
|
|
||||||
|
|
||||||
import templateConstants from '@/consts/template.js';
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
import { sortNumbersArray } from '@/lib/common.js';
|
import { type CommonScheduleFrequencySelectionProps, useScheduleFrequencySelectionBase } from '@/components/base/ScheduleFrequencySelectionBase.ts';
|
||||||
import { scrollToSelectedItem } from '@/lib/ui.desktop.js';
|
|
||||||
|
|
||||||
export default {
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
props: [
|
|
||||||
'type',
|
|
||||||
'modelValue',
|
|
||||||
'disabled',
|
|
||||||
'readonly',
|
|
||||||
'label'
|
|
||||||
],
|
|
||||||
emits: [
|
|
||||||
'update:type',
|
|
||||||
'update:modelValue'
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
menuState: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useUserStore),
|
|
||||||
allTransactionScheduledFrequencyTypes() {
|
|
||||||
return this.$locale.getAllTransactionScheduledFrequencyTypes();
|
|
||||||
},
|
|
||||||
allTemplateScheduledFrequencyTypes() {
|
|
||||||
return templateConstants.allTemplateScheduledFrequencyTypes;
|
|
||||||
},
|
|
||||||
allWeekDays() {
|
|
||||||
return this.$locale.getAllWeekDays(this.firstDayOfWeek);
|
|
||||||
},
|
|
||||||
allAvailableMonthDays() {
|
|
||||||
const allAvailableDays = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= 28; i++) {
|
import { ScheduledTemplateFrequencyType } from '@/core/template.ts';
|
||||||
allAvailableDays.push({
|
import { sortNumbersArray } from '@/lib/common.ts';
|
||||||
day: i,
|
import { scrollToSelectedItem } from '@/lib/ui/desktop.ts';
|
||||||
displayName: this.$locale.getMonthdayShortName(i),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return allAvailableDays;
|
const props = defineProps<CommonScheduleFrequencySelectionProps>();
|
||||||
},
|
const emit = defineEmits<{
|
||||||
firstDayOfWeek() {
|
(e: 'update:type', value: number): void;
|
||||||
return this.userStore.currentUserFirstDayOfWeek;
|
(e: 'update:modelValue', value: string): void;
|
||||||
},
|
}>();
|
||||||
frequencyType: {
|
|
||||||
get: function () {
|
|
||||||
return this.type;
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
if (this.type !== value) {
|
|
||||||
this.$emit('update:type', value);
|
|
||||||
|
|
||||||
if (value === templateConstants.allTemplateScheduledFrequencyTypes.Weekly.type) {
|
const { tt, getMultiMonthdayShortNames, getMultiWeekdayLongNames } = useI18n();
|
||||||
this.frequencyValue = [this.firstDayOfWeek];
|
const { allTransactionScheduledFrequencyTypes, allWeekDays, allAvailableMonthDays, getFrequencyValues } = useScheduleFrequencySelectionBase();
|
||||||
} else if (value === templateConstants.allTemplateScheduledFrequencyTypes.Monthly.type) {
|
|
||||||
this.frequencyValue = [1];
|
|
||||||
} else {
|
|
||||||
this.frequencyValue = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
frequencyValue: {
|
|
||||||
get: function () {
|
|
||||||
const values = this.modelValue.split(',');
|
|
||||||
const ret = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < values.length; i++) {
|
const userStore = useUserStore();
|
||||||
if (values[i]) {
|
|
||||||
ret.push(parseInt(values[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortNumbersArray(ret);
|
const dropdownMenu = useTemplateRef<HTMLElement>('dropdownMenu');
|
||||||
},
|
|
||||||
set: function (value) {
|
const menuState = ref<boolean>(false);
|
||||||
this.$emit('update:modelValue', sortNumbersArray(value).join(','));
|
|
||||||
}
|
const firstDayOfWeek = computed<number>(() => userStore.currentUserFirstDayOfWeek);
|
||||||
},
|
|
||||||
displayFrequency() {
|
const frequencyType = computed<number>({
|
||||||
if (this.type === templateConstants.allTemplateScheduledFrequencyTypes.Disabled.type) {
|
get: () => props.type,
|
||||||
return this.$t('Disabled');
|
set: (value: number) => {
|
||||||
} else if (this.type === templateConstants.allTemplateScheduledFrequencyTypes.Weekly.type) {
|
if (props.type !== value) {
|
||||||
if (this.frequencyValue.length) {
|
emit('update:type', value);
|
||||||
return this.$t('format.misc.everyMultiDaysOfWeek', {
|
|
||||||
days: this.$locale.getMultiWeekdayLongNames(this.frequencyValue, this.firstDayOfWeek)
|
if (value === ScheduledTemplateFrequencyType.Weekly.type) {
|
||||||
});
|
frequencyValue.value = [firstDayOfWeek.value];
|
||||||
} else {
|
} else if (value === ScheduledTemplateFrequencyType.Monthly.type) {
|
||||||
return this.$t('Weekly');
|
frequencyValue.value = [1];
|
||||||
}
|
|
||||||
} else if (this.type === templateConstants.allTemplateScheduledFrequencyTypes.Monthly.type) {
|
|
||||||
if (this.frequencyValue.length) {
|
|
||||||
return this.$t('format.misc.everyMultiDaysOfMonth', {
|
|
||||||
days: this.$locale.getMultiMonthdayShortNames(this.frequencyValue)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return this.$t('Monthly');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return '';
|
frequencyValue.value = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
methods: {
|
});
|
||||||
onMenuStateChanged(state) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
if (state) {
|
const frequencyValue = computed<number[]>({
|
||||||
self.$nextTick(() => {
|
get: () => getFrequencyValues(props.modelValue),
|
||||||
if (self.$refs.dropdownMenu && self.$refs.dropdownMenu.parentElement) {
|
set: (value: number[]) => {
|
||||||
scrollToSelectedItem(self.$refs.dropdownMenu.parentElement, '.schedule-frequency-value-container', '.frequency-value-selected');
|
emit('update:modelValue', sortNumbersArray(value).join(','));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
isFrequencyValueSelected(value) {
|
|
||||||
for (let i = 0; i < this.frequencyValue.length; i++) {
|
|
||||||
if (this.frequencyValue[i] === value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
const displayFrequency = computed<string>(() => {
|
||||||
|
if (frequencyType.value === ScheduledTemplateFrequencyType.Disabled.type) {
|
||||||
|
return tt('Disabled');
|
||||||
|
} else if (frequencyType.value === ScheduledTemplateFrequencyType.Weekly.type) {
|
||||||
|
if (frequencyValue.value.length) {
|
||||||
|
return tt('format.misc.everyMultiDaysOfWeek', {
|
||||||
|
days: getMultiWeekdayLongNames(frequencyValue.value, firstDayOfWeek.value)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return tt('Weekly');
|
||||||
}
|
}
|
||||||
|
} else if (frequencyType.value === ScheduledTemplateFrequencyType.Monthly.type) {
|
||||||
|
if (frequencyValue.value.length) {
|
||||||
|
return tt('format.misc.everyMultiDaysOfMonth', {
|
||||||
|
days: getMultiMonthdayShortNames(frequencyValue.value)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return tt('Monthly');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function isFrequencyValueSelected(value: number): boolean {
|
||||||
|
for (let i = 0; i < frequencyValue.value.length; i++) {
|
||||||
|
if (frequencyValue.value[i] === value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMenuStateChanged(state: boolean): void {
|
||||||
|
if (state) {
|
||||||
|
nextTick(() => {
|
||||||
|
if (dropdownMenu.value && dropdownMenu.value.parentElement) {
|
||||||
|
scrollToSelectedItem(dropdownMenu.value.parentElement, '.schedule-frequency-value-container', '.frequency-value-selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user