Compare commits
3 Commits
eb13123d86
...
80977ee896
| Author | SHA1 | Date | |
|---|---|---|---|
|
80977ee896
|
|||
|
192f21c3a6
|
|||
|
149d8f01b7
|
389
package-lock.json
generated
389
package-lock.json
generated
@@ -18,7 +18,6 @@
|
|||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"multer": "^1.4.5-lts.2",
|
"multer": "^1.4.5-lts.2",
|
||||||
"papaparse": "^5.5.2",
|
"papaparse": "^5.5.2",
|
||||||
"pug": "^3.0.3",
|
|
||||||
"yaml": "^2.7.1"
|
"yaml": "^2.7.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -58,52 +57,6 @@
|
|||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
|
||||||
"version": "7.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
|
||||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
|
||||||
"version": "7.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
|
||||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/parser": {
|
|
||||||
"version": "7.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz",
|
|
||||||
"integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/types": "^7.27.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"parser": "bin/babel-parser.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/types": {
|
|
||||||
"version": "7.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
|
|
||||||
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/helper-string-parser": "^7.27.1",
|
|
||||||
"@babel/helper-validator-identifier": "^7.27.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.3",
|
"version": "0.25.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
|
||||||
@@ -694,18 +647,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
|
||||||
"version": "7.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"acorn": "bin/acorn"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/anymatch": {
|
"node_modules/anymatch": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
@@ -736,30 +677,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/asap": {
|
|
||||||
"version": "2.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
|
||||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/assert-never": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/babel-walk": {
|
|
||||||
"version": "3.0.0-canary-5",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
|
|
||||||
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/types": "^7.9.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -950,15 +867,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/character-parser": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-regex": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
@@ -1020,16 +928,6 @@
|
|||||||
"typedarray": "^0.0.6"
|
"typedarray": "^0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/constantinople": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.6.0",
|
|
||||||
"@babel/types": "^7.6.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||||
@@ -1156,12 +1054,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/doctypes": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -1603,21 +1495,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-tostringtag": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"has-symbols": "^1.0.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
@@ -1722,31 +1599,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-core-module": {
|
|
||||||
"version": "2.16.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
|
||||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"hasown": "^2.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-expression": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"acorn": "^7.1.1",
|
|
||||||
"object-assign": "^4.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -1786,52 +1638,12 @@
|
|||||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/is-regex": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"call-bound": "^1.0.2",
|
|
||||||
"gopd": "^1.2.0",
|
|
||||||
"has-tostringtag": "^1.0.2",
|
|
||||||
"hasown": "^2.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/isarray": {
|
"node_modules/isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-stringify": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/jstransformer": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-promise": "^2.0.0",
|
|
||||||
"promise": "^7.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jstransformer/node_modules/is-promise": {
|
|
||||||
"version": "2.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
|
||||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -2165,12 +1977,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-parse": {
|
|
||||||
"version": "1.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
|
||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "8.2.0",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
||||||
@@ -2248,15 +2054,6 @@
|
|||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/promise": {
|
|
||||||
"version": "7.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
|
||||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"asap": "~2.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -2270,130 +2067,6 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pug": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pug-code-gen": "^3.0.3",
|
|
||||||
"pug-filters": "^4.0.0",
|
|
||||||
"pug-lexer": "^5.0.1",
|
|
||||||
"pug-linker": "^4.0.0",
|
|
||||||
"pug-load": "^3.0.0",
|
|
||||||
"pug-parser": "^6.0.0",
|
|
||||||
"pug-runtime": "^3.0.1",
|
|
||||||
"pug-strip-comments": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-attrs": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"constantinople": "^4.0.1",
|
|
||||||
"js-stringify": "^1.0.2",
|
|
||||||
"pug-runtime": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-code-gen": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"constantinople": "^4.0.1",
|
|
||||||
"doctypes": "^1.1.0",
|
|
||||||
"js-stringify": "^1.0.2",
|
|
||||||
"pug-attrs": "^3.0.0",
|
|
||||||
"pug-error": "^2.1.0",
|
|
||||||
"pug-runtime": "^3.0.1",
|
|
||||||
"void-elements": "^3.1.0",
|
|
||||||
"with": "^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-error": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/pug-filters": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"constantinople": "^4.0.1",
|
|
||||||
"jstransformer": "1.0.0",
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"pug-walk": "^2.0.0",
|
|
||||||
"resolve": "^1.15.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-lexer": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"character-parser": "^2.2.0",
|
|
||||||
"is-expression": "^4.0.0",
|
|
||||||
"pug-error": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-linker": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"pug-walk": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-load": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"object-assign": "^4.1.1",
|
|
||||||
"pug-walk": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-parser": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pug-error": "^2.0.0",
|
|
||||||
"token-stream": "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-runtime": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/pug-strip-comments": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pug-error": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pug-walk": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||||
@@ -2523,26 +2196,6 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/resolve": {
|
|
||||||
"version": "1.22.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
|
||||||
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-core-module": "^2.16.0",
|
|
||||||
"path-parse": "^1.0.7",
|
|
||||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"resolve": "bin/resolve"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/resolve-pkg-maps": {
|
"node_modules/resolve-pkg-maps": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
@@ -2853,18 +2506,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supports-preserve-symlinks-flag": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tar-fs": {
|
"node_modules/tar-fs": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
|
||||||
@@ -2929,12 +2570,6 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/token-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tsc-alias": {
|
"node_modules/tsc-alias": {
|
||||||
"version": "1.8.15",
|
"version": "1.8.15",
|
||||||
"resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.15.tgz",
|
"resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.15.tgz",
|
||||||
@@ -3076,15 +2711,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/void-elements": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/web-streams-polyfill": {
|
"node_modules/web-streams-polyfill": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
@@ -3094,21 +2720,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/with": {
|
|
||||||
"version": "7.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
|
|
||||||
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.9.6",
|
|
||||||
"@babel/types": "^7.9.6",
|
|
||||||
"assert-never": "^1.2.1",
|
|
||||||
"babel-walk": "3.0.0-canary-5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"multer": "^1.4.5-lts.2",
|
"multer": "^1.4.5-lts.2",
|
||||||
"papaparse": "^5.5.2",
|
"papaparse": "^5.5.2",
|
||||||
"pug": "^3.0.3",
|
|
||||||
"yaml": "^2.7.1"
|
"yaml": "^2.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
package.nix
25
package.nix
@@ -1,16 +1,19 @@
|
|||||||
{
|
{
|
||||||
|
pkgs,
|
||||||
buildNpmPackage,
|
buildNpmPackage,
|
||||||
lib,
|
lib,
|
||||||
...
|
...
|
||||||
}:
|
}: let
|
||||||
buildNpmPackage {
|
frontend = pkgs.callPackage ./web/package.nix {};
|
||||||
pname = "actual-importer";
|
in
|
||||||
version = "0.0.1";
|
buildNpmPackage {
|
||||||
src = ./.;
|
pname = "actual-importer";
|
||||||
npmDepsHash = "sha256-QqSZJuQLPll3qFi5Lv4kbQo+548l3VKgx073WLNXMsw=";
|
version = "0.0.1";
|
||||||
|
src = ./.;
|
||||||
|
npmDepsHash = "sha256-ayfvTn8FVo7/K/Gy7oTDQiXDewvrU7B5JsNLHfrxibQ=";
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
mkdir -p $out/views
|
mkdir -p $out/lib/node_modules/actual-importer/public
|
||||||
cp -r ${./views}/* $out/views/
|
cp -r ${frontend}/lib/node_modules/web/dist/* $out/lib/node_modules/actual-importer/public/
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,27 +9,8 @@ import path from 'path';
|
|||||||
export function serve(config: Config, port: number) {
|
export function serve(config: Config, port: number) {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.set('view engine', 'pug');
|
|
||||||
app.set('views', path.join(__dirname, '../../views'));
|
|
||||||
|
|
||||||
app.use(express.static('public'));
|
|
||||||
app.use(function(req, res, next) {
|
|
||||||
res.header("Access-Control-Allow-Origin", "*");
|
|
||||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
|
||||||
res.render('index', {
|
|
||||||
profiles: Object.keys(config.profiles),
|
|
||||||
servers: Object.keys(config.servers),
|
|
||||||
defaultProfile: config.defaultProfile,
|
|
||||||
defaultServer: config.defaultServer
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/config", (req, res) => {
|
app.get("/config", (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
profiles: Object.keys(config.profiles),
|
profiles: Object.keys(config.profiles),
|
||||||
@@ -61,7 +42,12 @@ export function serve(config: Config, port: number) {
|
|||||||
res.send(response);
|
res.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(express.static(path.join(__dirname, '../../public')));
|
||||||
|
|
||||||
|
app.get('*public', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, '../../', 'public', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server running on ${port} port`);
|
console.log(`Server running on ${port} port`);
|
||||||
});
|
});
|
||||||
|
|||||||
222
views/index.pug
222
views/index.pug
@@ -1,222 +0,0 @@
|
|||||||
html
|
|
||||||
head
|
|
||||||
title Actual Importer
|
|
||||||
|
|
||||||
body
|
|
||||||
h1 Import transactions
|
|
||||||
|
|
||||||
form#prepareForm
|
|
||||||
p
|
|
||||||
label(for="file") CSV File:
|
|
||||||
input(type="file" name="file" required)
|
|
||||||
|
|
||||||
p
|
|
||||||
label(for="profile") Profile:
|
|
||||||
select(name="profile" required)
|
|
||||||
each profile in profiles
|
|
||||||
option(value=profile selected=(defaultProfile === profile))= profile
|
|
||||||
|
|
||||||
p
|
|
||||||
label(for="server") Server:
|
|
||||||
select(name="server" required)
|
|
||||||
each server in servers
|
|
||||||
option(value=server selected=(defaultServer === server))= server
|
|
||||||
|
|
||||||
button(type="submit") Load
|
|
||||||
|
|
||||||
div#prepare
|
|
||||||
|
|
||||||
div#result
|
|
||||||
|
|
||||||
script.
|
|
||||||
const form = document.querySelector("#prepareForm");
|
|
||||||
|
|
||||||
function renderTable(transactions) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerHTML = `
|
|
||||||
<h3>Pending transactions (${transactions.length})</h3>
|
|
||||||
`;
|
|
||||||
const table = document.createElement('table');
|
|
||||||
const thead = document.createElement('thead');
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.innerHTML = `
|
|
||||||
<th>#</th>
|
|
||||||
<th>Import</th>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Kind</th>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>From</th>
|
|
||||||
<th>To</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
<th>Title</th>
|
|
||||||
`;
|
|
||||||
|
|
||||||
thead.appendChild(tr);
|
|
||||||
table.appendChild(thead);
|
|
||||||
|
|
||||||
const tbody = document.createElement('tbody');
|
|
||||||
transactions.forEach((transaction, index) => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${index+1}</td>
|
|
||||||
<td><input type="checkbox" id="transaction${index}" checked/></td>
|
|
||||||
<td>${transaction.id}</td>
|
|
||||||
<td>${transaction.kind}</td>
|
|
||||||
<td>${transaction.date}</td>
|
|
||||||
<td>${transaction.from}</td>
|
|
||||||
<td>${transaction.to}</td>
|
|
||||||
<td>${transaction.amount}</td>
|
|
||||||
<td>${transaction.title}</td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
tbody.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
table.appendChild(tbody);
|
|
||||||
div.appendChild(table);
|
|
||||||
return div;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSkipped(skipped) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerHTML = `
|
|
||||||
<h3>Skipped CSV rows</h3>
|
|
||||||
<p>The <tt>:::</tt> is CSV field delimiter</p>
|
|
||||||
<pre>${skipped.map(d => d.join(" ::: ")).join("\n")}</pre>
|
|
||||||
`
|
|
||||||
|
|
||||||
return div;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderResult(result) {
|
|
||||||
const resultWrapper = document.querySelector('#result');
|
|
||||||
resultWrapper.innerHTML = `
|
|
||||||
<h2>Import status</h3>
|
|
||||||
<p>
|
|
||||||
<ul>
|
|
||||||
<li>Added: <strong>${result.added.length}</strong></li>
|
|
||||||
<li>Updated: <strong>${result.updated.length}</strong></li>
|
|
||||||
<li>Errors: <strong>${result?.errors?.length ?? 0}</strong></li>
|
|
||||||
</ul>
|
|
||||||
${(result.errors?.length ?? 0) > 0
|
|
||||||
? `<p>Errors: <ul>${result.errors?.map(e => `<li>${e.message}</li>`)}</ul></p>`
|
|
||||||
: ""}
|
|
||||||
</p>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitTransactions(config, transactions, opts) {
|
|
||||||
const filtered = transactions.filter((transaction, index) => !!document.querySelector(`#transaction${index}`).checked);
|
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/submit", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
server: config.get('server'),
|
|
||||||
transactions: filtered,
|
|
||||||
opts
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
renderResult(result);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPrepare(config, data) {
|
|
||||||
document.querySelector('#result').innerHTML = '';
|
|
||||||
const prepare = document.querySelector("#prepare");
|
|
||||||
prepare.innerHTML = '';
|
|
||||||
|
|
||||||
const title = document.createElement("h2");
|
|
||||||
title.innerHTML = "Prepare transactions to submit";
|
|
||||||
|
|
||||||
prepare.appendChild(title);
|
|
||||||
prepare.appendChild(renderTable(data.transactions));
|
|
||||||
prepare.appendChild(renderSkipped(data.skipped));
|
|
||||||
|
|
||||||
const addForm = document.createElement("form");
|
|
||||||
addForm.innerHTML = `
|
|
||||||
<fieldset>
|
|
||||||
<legend>Add</legend>
|
|
||||||
<p>
|
|
||||||
<label for="learn">Learn categories</label>
|
|
||||||
<input type="checkbox" name="learn" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<label for="transfers">Run transfers</label>
|
|
||||||
<input type="checkbox" name="transfers" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button type="submit">Add</button>
|
|
||||||
</fieldset>
|
|
||||||
`;
|
|
||||||
|
|
||||||
addForm.addEventListener("submit", async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const options = new FormData(addForm);
|
|
||||||
const opts = {
|
|
||||||
mode: 'add',
|
|
||||||
learnCategories: !!options.get('learn'),
|
|
||||||
runTransfers: !!options.get('transfers')
|
|
||||||
};
|
|
||||||
|
|
||||||
await submitTransactions(config, data.transactions, opts);
|
|
||||||
prepare.innerHTML = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const importForm = document.createElement("form");
|
|
||||||
importForm.innerHTML = `
|
|
||||||
<fieldset>
|
|
||||||
<legend>Import</legend>
|
|
||||||
<button type="submit">Import</button>
|
|
||||||
</fieldset>
|
|
||||||
`;
|
|
||||||
|
|
||||||
importForm.addEventListener("submit", async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await submitTransactions(config, data.transactions, { mode: 'import' });
|
|
||||||
prepare.innerHTML = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.transactions.length > 0) {
|
|
||||||
prepare.appendChild(addForm);
|
|
||||||
prepare.appendChild(importForm);
|
|
||||||
} else {
|
|
||||||
const message = document.createElement('p');
|
|
||||||
message.innerHTML = `
|
|
||||||
<strong>No transactions to import</strong>
|
|
||||||
`;
|
|
||||||
prepare.appendChild(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handlePrepare(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const config = new FormData(form);
|
|
||||||
const response = await fetch("/prepare", {
|
|
||||||
method: "POST",
|
|
||||||
body: config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
renderPrepare(config, data);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
form.addEventListener("submit", handlePrepare);
|
|
||||||
@@ -7,5 +7,5 @@ buildNpmPackage {
|
|||||||
pname = "actual-importer-frontend";
|
pname = "actual-importer-frontend";
|
||||||
version = "0.0.1";
|
version = "0.0.1";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
npmDepsHash = "sha256-CC9mDUyLBPigwlccQMauNoqzHKMeOppPu5y2eJXIzLY=";
|
npmDepsHash = "sha256-+HDXY0d3gvXJWy4GPa6vCIqLnq1mCP8kimek5Zl8u80=";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import LoadFilePage from "./pages/LoadFilePage/LoadFilePage";
|
import LoadFilePage from "./pages/LoadFilePage/LoadFilePage";
|
||||||
|
import PrepareTransactionsPage from "./pages/PrepareTransactionsPage/PrepareTransactionsPage";
|
||||||
|
import ResultPage from "./pages/ResultPage/ResultPage";
|
||||||
|
import { AppStoreProvider } from "./store/AppStore";
|
||||||
|
import { Wizard } from "./wizard/Wizard";
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (<LoadFilePage />);
|
return (
|
||||||
|
<AppStoreProvider>
|
||||||
|
<Wizard>
|
||||||
|
<LoadFilePage />
|
||||||
|
<PrepareTransactionsPage />
|
||||||
|
<ResultPage />
|
||||||
|
</Wizard>
|
||||||
|
</AppStoreProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import LoadFileForm from './LoadFileForm';
|
import LoadFileForm from './LoadFileForm';
|
||||||
import { fetchConfig, loadTransactions } from '../../services/api.service';
|
import { fetchConfig, loadTransactions } from '../../services/api.service';
|
||||||
|
import { useStore } from '../../store/AppStore';
|
||||||
|
|
||||||
export default function LoadFilePage() {
|
export default function LoadFilePage() {
|
||||||
|
const { dispatch } = useStore();
|
||||||
|
|
||||||
const [profiles, setProfiles] = useState<string[]>([]);
|
const [profiles, setProfiles] = useState<string[]>([]);
|
||||||
const [servers, setServers] = useState<string[]>([]);
|
const [servers, setServers] = useState<string[]>([]);
|
||||||
@@ -16,6 +18,11 @@ export default function LoadFilePage() {
|
|||||||
setServers(config.servers);
|
setServers(config.servers);
|
||||||
setDefaultProfile(config.defaultProfile);
|
setDefaultProfile(config.defaultProfile);
|
||||||
setDefaultServer(config.defaultServer);
|
setDefaultServer(config.defaultServer);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_CONFIG',
|
||||||
|
payload: config
|
||||||
|
});
|
||||||
}, [setProfiles, setServers, setDefaultProfile, setDefaultServer]);
|
}, [setProfiles, setServers, setDefaultProfile, setDefaultServer]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -24,14 +31,29 @@ export default function LoadFilePage() {
|
|||||||
|
|
||||||
const handleSubmit = useCallback(async (csvFile: File, profile: string, server: string) => {
|
const handleSubmit = useCallback(async (csvFile: File, profile: string, server: string) => {
|
||||||
const data = await loadTransactions(csvFile, profile, server);
|
const data = await loadTransactions(csvFile, profile, server);
|
||||||
console.log(data);
|
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_DATA',
|
||||||
|
payload: {
|
||||||
|
profile,
|
||||||
|
server,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'MOVE_STEP',
|
||||||
|
payload: {
|
||||||
|
offset: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="columns mt-6">
|
<div className="columns mt-6">
|
||||||
<div className="column is-6 is-offset-3">
|
<div className="column is-6 is-offset-3">
|
||||||
<div className="box">
|
<div className="box">
|
||||||
<h2 className="title is-4 has-text-centered">Import Transactions</h2>
|
<h2 className="title is-4 has-text-centered">Import Transactions</h2>
|
||||||
<LoadFileForm
|
<LoadFileForm
|
||||||
profiles={profiles}
|
profiles={profiles}
|
||||||
servers={servers}
|
servers={servers}
|
||||||
|
|||||||
77
web/src/pages/PrepareTransactionsPage/ImportBar.tsx
Normal file
77
web/src/pages/PrepareTransactionsPage/ImportBar.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import type { ImportOptions } from "../../types/api";
|
||||||
|
|
||||||
|
export type ImportBarProps = {
|
||||||
|
onSubmit: (opts: ImportOptions) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ImportBar({ onSubmit }: ImportBarProps) {
|
||||||
|
const [formData, setFormData] = useState<ImportOptions>({ mode: 'import' });
|
||||||
|
|
||||||
|
|
||||||
|
const handleSubmit = useCallback((e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onSubmit(formData);
|
||||||
|
}, [formData, onSubmit]);
|
||||||
|
|
||||||
|
const changeMode = useCallback((mode: ImportOptions['mode']) => {
|
||||||
|
if (mode === 'add') {
|
||||||
|
setFormData({ ...formData, mode });
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
setFormData({ mode });
|
||||||
|
}
|
||||||
|
}, [formData, setFormData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Import mode</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="buttons has-addons is-left">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`button ${formData.mode === 'import' ? "is-primary is-selected" : ""}`}
|
||||||
|
onClick={() => changeMode('import')}>Import</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`button ${formData.mode === 'add' ? "is-primary is-selected" : ""}`}
|
||||||
|
onClick={() => changeMode('add')}>Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.mode === 'add' && <div className="field">
|
||||||
|
<label className="label">Additional options</label>
|
||||||
|
<div className="control">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.learnCategories}
|
||||||
|
onChange={e => setFormData({ ...formData, learnCategories: e.target.checked})}/> Learn categories
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.runTransfers}
|
||||||
|
onChange={e => setFormData({ ...formData, runTransfers: e.target.checked})}/> Run transfers
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<div className="field mt-5">
|
||||||
|
<div className="control">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="button is-link is-fullwidth">Submit transactions</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { useStore } from "../../store/AppStore";
|
||||||
|
import TransactionsTable from "./TransactionsTable";
|
||||||
|
import { SkippedLines } from "./SkippedLines";
|
||||||
|
import ImportBar from "./ImportBar";
|
||||||
|
import type { ImportOptions } from "../../types/api";
|
||||||
|
import { submitTransactions } from "../../services/api.service";
|
||||||
|
|
||||||
|
|
||||||
|
export default function PrepareTransactionsPage() {
|
||||||
|
const { state, dispatch } = useStore();
|
||||||
|
|
||||||
|
const desiredTransactions = useMemo(() => state.transactions.filter((_, i) => !state.filteredOut.includes(i)), [state, state.filteredOut]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(async (opts: ImportOptions) => {
|
||||||
|
if(!state.server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await submitTransactions(desiredTransactions, state.server, opts);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_RESULT',
|
||||||
|
payload: response
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'MOVE_STEP',
|
||||||
|
payload: {
|
||||||
|
offset: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [desiredTransactions, state]);
|
||||||
|
|
||||||
|
const filterOut = useCallback((index: number) => dispatch({
|
||||||
|
type: 'FILTER_OUT',
|
||||||
|
payload: { index }
|
||||||
|
}), [dispatch]);
|
||||||
|
|
||||||
|
const removeFilter = useCallback((index: number) => dispatch({
|
||||||
|
type: 'REMOVE_FILTER',
|
||||||
|
payload: { index }
|
||||||
|
}), [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="columns mt-6">
|
||||||
|
<div className="column is-8 is-offset-2">
|
||||||
|
<div className="box">
|
||||||
|
<h2 className="title is-4 has-text-centered">Prepare Transactions</h2>
|
||||||
|
|
||||||
|
<div className="content">
|
||||||
|
<h3 className="title is-5">Considered transactions ({state.transactions.length})</h3>
|
||||||
|
<TransactionsTable
|
||||||
|
transactions={state.transactions}
|
||||||
|
filteredOut={state.filteredOut}
|
||||||
|
filterOut={filterOut}
|
||||||
|
removeFilter={removeFilter} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content">
|
||||||
|
<h3 className="title is-5">Skipped lines ({state.skipped.length})</h3>
|
||||||
|
<SkippedLines skipped={state.skipped} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content">
|
||||||
|
<ImportBar onSubmit={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.skipped {
|
||||||
|
content: red;
|
||||||
|
}
|
||||||
19
web/src/pages/PrepareTransactionsPage/SkippedLines.tsx
Normal file
19
web/src/pages/PrepareTransactionsPage/SkippedLines.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import styles from "./SkippedLines.module.css";
|
||||||
|
|
||||||
|
export type SkippedLinesProps = {
|
||||||
|
skipped: string[][];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SkippedLines({ skipped }: SkippedLinesProps) {
|
||||||
|
if (skipped.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.skipped}>
|
||||||
|
<pre>
|
||||||
|
{skipped.map(s => `▶️ ${s.join("🔸")}`).join("\n")}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.transactionTable tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled td {
|
||||||
|
color: var(--bulma-text-light);
|
||||||
|
}
|
||||||
63
web/src/pages/PrepareTransactionsPage/TransactionsTable.tsx
Normal file
63
web/src/pages/PrepareTransactionsPage/TransactionsTable.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import type { Transaction } from "../../types/api";
|
||||||
|
import styles from "./TransactionTable.module.css";
|
||||||
|
|
||||||
|
export type TransactionsTableProps = {
|
||||||
|
transactions: Transaction[];
|
||||||
|
filteredOut: number[];
|
||||||
|
filterOut: (index: number) => void;
|
||||||
|
removeFilter: (index: number) => void;
|
||||||
|
readonly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TransactionsTable({ transactions, filteredOut, filterOut, removeFilter, readonly }: TransactionsTableProps) {
|
||||||
|
|
||||||
|
const changeSelection = useCallback((index: number) => {
|
||||||
|
if (readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(filteredOut.includes(index)) {
|
||||||
|
removeFilter(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
filterOut(index);
|
||||||
|
}
|
||||||
|
}, [filteredOut, filterOut, removeFilter]);
|
||||||
|
|
||||||
|
|
||||||
|
const renderRow = useCallback((transaction: Transaction, index: number) => (
|
||||||
|
<tr key={index} onClick={() => changeSelection(index)} className={`${!readonly && filteredOut.includes(index) ? styles.disabled : ""}`}>
|
||||||
|
<td>{transaction.kind}</td>
|
||||||
|
<td>{transaction.date}</td>
|
||||||
|
<td>{transaction.from}</td>
|
||||||
|
<td>{transaction.to}</td>
|
||||||
|
<td>{transaction.amount}</td>
|
||||||
|
<td>{transaction.title}</td>
|
||||||
|
<td>{transaction.id}</td>
|
||||||
|
</tr>
|
||||||
|
), [changeSelection]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="table-container">
|
||||||
|
<table className={`table is-hoverable ${!readonly ? styles.transactionTable : ""}`}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<th>Kind</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>From</th>
|
||||||
|
<th>To</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>ID</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{transactions.map(renderRow)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
web/src/pages/ResultPage/ResultPage.tsx
Normal file
47
web/src/pages/ResultPage/ResultPage.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useStore } from "../../store/AppStore";
|
||||||
|
|
||||||
|
export default function ResultPage() {
|
||||||
|
const { state } = useStore();
|
||||||
|
|
||||||
|
const errors = useMemo(() => {
|
||||||
|
if (!state.result?.errors) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.result.errors.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="notification is-danger">
|
||||||
|
<p>Errors:</p>
|
||||||
|
<ul>
|
||||||
|
{state.result.errors.map(e => <li>{e.message}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [state.result?.errors]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="columns mt-6">
|
||||||
|
<div className="column is-8 is-offset-2">
|
||||||
|
<div className="box">
|
||||||
|
<h2 className="title is-4 has-text-centered">Import Result</h2>
|
||||||
|
<div className="content">
|
||||||
|
<div className="notification is-info">
|
||||||
|
<ul>
|
||||||
|
<li>Added: <strong>{state.result?.added?.length ?? 0}</strong></li>
|
||||||
|
<li>Updated: <strong>{state.result?.updated?.length ?? 0}</strong></li>
|
||||||
|
<li>Errors: <strong>{state.result?.errors?.length ?? 0}</strong></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="content">
|
||||||
|
{errors}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ConfigResponse, PrepareResponse } from "../types/api";
|
import type { ConfigResponse, ImportOptions, PrepareResponse, SubmitResponse, Transaction } from "../types/api";
|
||||||
|
|
||||||
export async function fetchConfig(): Promise<ConfigResponse> {
|
export async function fetchConfig(): Promise<ConfigResponse> {
|
||||||
const response = await fetch("http://localhost:3000/config")
|
const response = await fetch("/config")
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data as ConfigResponse;
|
return data as ConfigResponse;
|
||||||
}
|
}
|
||||||
@@ -12,11 +12,30 @@ export async function loadTransactions(csvFile: File, profile: string, server: s
|
|||||||
payload.append("profile", profile);
|
payload.append("profile", profile);
|
||||||
payload.append("server", server);
|
payload.append("server", server);
|
||||||
|
|
||||||
const response = await fetch("http://localhost:3000/prepare", {
|
const response = await fetch("/prepare", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: payload
|
body: payload
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data as PrepareResponse;
|
return data as PrepareResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitTransactions(transactions: Transaction[], server: string, opts: ImportOptions): Promise<SubmitResponse> {
|
||||||
|
const payload = {
|
||||||
|
transactions,
|
||||||
|
server,
|
||||||
|
opts
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("/submit", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data as SubmitResponse;
|
||||||
}
|
}
|
||||||
38
web/src/store/AppStore.tsx
Normal file
38
web/src/store/AppStore.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { StoreContext } from "../types/store";
|
||||||
|
import { createContext, useContext, useMemo, useReducer } from "react";
|
||||||
|
import { initialState } from "./state";
|
||||||
|
import { reducer } from "./reducer";
|
||||||
|
|
||||||
|
export type AppStoreProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseStoreReturnType = StoreContext & {
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const AppContext = createContext<StoreContext>({
|
||||||
|
state: initialState,
|
||||||
|
dispatch: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function AppStoreProvider({ children }: AppStoreProviderProps) {
|
||||||
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
|
||||||
|
const context = useMemo(() => ({ state, dispatch }), [state, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContext.Provider value={context}>
|
||||||
|
{children}
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStore(): UseStoreReturnType {
|
||||||
|
const { state, dispatch } = useContext(AppContext);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
dispatch,
|
||||||
|
}
|
||||||
|
}
|
||||||
59
web/src/store/reducer.ts
Normal file
59
web/src/store/reducer.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { Action, State } from "../types/store";
|
||||||
|
|
||||||
|
export function reducer(state: State, action: Action): State {
|
||||||
|
switch(action.type) {
|
||||||
|
case 'UPDATE_CONFIG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
availableProfiles: action.payload.profiles,
|
||||||
|
availableServers: action.payload.servers,
|
||||||
|
defaultProfile: action.payload.defaultProfile,
|
||||||
|
defaultServer: action.payload.defaultServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_DATA':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
profile: action.payload.profile,
|
||||||
|
server: action.payload.server,
|
||||||
|
transactions: action.payload.transactions,
|
||||||
|
skipped: action.payload.skipped,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_STEP':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
wizard: {
|
||||||
|
...state.wizard,
|
||||||
|
step: action.payload.step
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MOVE_STEP':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
wizard: {
|
||||||
|
...state.wizard,
|
||||||
|
step: state.wizard.step + action.payload.offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'FILTER_OUT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filteredOut: [...state.filteredOut, action.payload.index]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'REMOVE_FILTER':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filteredOut: state.filteredOut.filter(x => x !== action.payload.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_RESULT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
result: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
web/src/store/state.ts
Normal file
14
web/src/store/state.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { State } from "../types/store";
|
||||||
|
|
||||||
|
export const initialState: State = {
|
||||||
|
availableProfiles: [],
|
||||||
|
availableServers: [],
|
||||||
|
transactions: [],
|
||||||
|
skipped: [],
|
||||||
|
|
||||||
|
wizard: {
|
||||||
|
step: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
filteredOut: []
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ export type ConfigResponse = {
|
|||||||
|
|
||||||
export type PrepareResponse = {
|
export type PrepareResponse = {
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
skipped: string[];
|
skipped: string[][];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Transaction = {
|
export type Transaction = {
|
||||||
@@ -20,4 +20,18 @@ export type Transaction = {
|
|||||||
title: string;
|
title: string;
|
||||||
id: string;
|
id: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImportOptions =
|
||||||
|
| { mode: 'add', learnCategories?: boolean, runTransfers?: boolean }
|
||||||
|
| { mode: 'import' };
|
||||||
|
|
||||||
|
export type SubmitResponse = {
|
||||||
|
errors?: {
|
||||||
|
message: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
added: unknown[];
|
||||||
|
|
||||||
|
updated: unknown[];
|
||||||
};
|
};
|
||||||
88
web/src/types/store.ts
Normal file
88
web/src/types/store.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import type { Transaction } from "./api";
|
||||||
|
|
||||||
|
export type State = {
|
||||||
|
availableProfiles: string[];
|
||||||
|
availableServers: string[];
|
||||||
|
defaultProfile?: string;
|
||||||
|
defaultServer?: string;
|
||||||
|
profile?: string;
|
||||||
|
server?: string;
|
||||||
|
transactions: Transaction[];
|
||||||
|
skipped: string[][];
|
||||||
|
|
||||||
|
wizard: {
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
filteredOut: number[];
|
||||||
|
|
||||||
|
result?: {
|
||||||
|
errors?: {
|
||||||
|
message: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
added: unknown[];
|
||||||
|
updated: unknown[];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Action =
|
||||||
|
| {
|
||||||
|
type: 'UPDATE_CONFIG',
|
||||||
|
payload: {
|
||||||
|
profiles: string[],
|
||||||
|
servers: string[],
|
||||||
|
defaultProfile?: string,
|
||||||
|
defaultServer?: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'UPDATE_DATA',
|
||||||
|
payload: {
|
||||||
|
profile: string;
|
||||||
|
server: string;
|
||||||
|
transactions: Transaction[];
|
||||||
|
skipped: string[][];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_STEP',
|
||||||
|
payload: {
|
||||||
|
step: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'MOVE_STEP',
|
||||||
|
payload: {
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'FILTER_OUT',
|
||||||
|
payload: {
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'REMOVE_FILTER',
|
||||||
|
payload: {
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_RESULT',
|
||||||
|
payload: {
|
||||||
|
errors?: {
|
||||||
|
message: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
added: unknown[];
|
||||||
|
|
||||||
|
updated: unknown[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StoreContext = {
|
||||||
|
state: State,
|
||||||
|
dispatch: React.Dispatch<Action>
|
||||||
|
};
|
||||||
11
web/src/wizard/Wizard.tsx
Normal file
11
web/src/wizard/Wizard.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useStore } from "../store/AppStore";
|
||||||
|
|
||||||
|
export type WizardProps = {
|
||||||
|
children: React.ReactNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Wizard({ children }: WizardProps) {
|
||||||
|
const { state } = useStore();
|
||||||
|
|
||||||
|
return children[state.wizard.step];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user