fix: persist has_header for imports, fix orphan categories, add re-initialize
Some checks failed
Release / build (windows-latest) (push) Has been cancelled
Some checks failed
Release / build (windows-latest) (push) Has been cancelled
- Import: persist `has_header` flag to DB (migration v3) so headerless CSVs like Desjardins don't lose their first data row on re-import. - Categories: promote children to root on parent deletion instead of cascading deactivation, preventing invisible orphans. - Categories: add re-initialize button to reset all categories and keywords to seed defaults. - Bump version to 0.2.1 across tauri.conf.json, package.json, Cargo.toml. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5f5696c29a
commit
c73f466429
15 changed files with 557 additions and 44 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "simpl_result_scaffold",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
|||
321
src-tauri/Cargo.lock
generated
321
src-tauri/Cargo.lock
generated
|
|
@ -53,6 +53,15 @@ version = "1.0.101"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
|
|
@ -703,6 +712,17 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
|
|
@ -992,6 +1012,17 @@ dependencies = [
|
|||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
|
|
@ -1629,6 +1660,22 @@ dependencies = [
|
|||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
|
|
@ -2139,6 +2186,12 @@ version = "0.3.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
|
|
@ -2458,6 +2511,18 @@ dependencies = [
|
|||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
|
|
@ -2527,6 +2592,12 @@ dependencies = [
|
|||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
|
|
@ -2543,6 +2614,20 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
|
|
@ -3143,15 +3228,20 @@ dependencies = [
|
|||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
|
|
@ -3187,6 +3277,20 @@ dependencies = [
|
|||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.10"
|
||||
|
|
@ -3229,6 +3333,79 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||
dependencies = [
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
|
|
@ -3250,6 +3427,15 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
|
|
@ -3307,6 +3493,29 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.24.0"
|
||||
|
|
@ -3571,7 +3780,9 @@ dependencies = [
|
|||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-sql",
|
||||
"tauri-plugin-updater",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
|
@ -4046,6 +4257,17 @@ dependencies = [
|
|||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
|
|
@ -4245,6 +4467,16 @@ dependencies = [
|
|||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-process"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
|
||||
dependencies = [
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-sql"
|
||||
version = "2.3.2"
|
||||
|
|
@ -4265,6 +4497,39 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.10.0"
|
||||
|
|
@ -4500,6 +4765,16 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
|
|
@ -4822,6 +5097,12 @@ version = "1.12.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
|
|
@ -5081,6 +5362,15 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
|
|
@ -5330,6 +5620,15 @@ dependencies = [
|
|||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
|
|
@ -5724,6 +6023,16 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
|
|
@ -5888,6 +6197,18 @@ dependencies = [
|
|||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.13.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.19"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "simpl-result"
|
||||
version = "0.1.0"
|
||||
version = "0.2.1"
|
||||
description = "Personal finance management app"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ pub fn run() {
|
|||
sql: database::SEED_CATEGORIES,
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 3,
|
||||
description: "add has_header to import_sources",
|
||||
sql: "ALTER TABLE import_sources ADD COLUMN has_header INTEGER NOT NULL DEFAULT 1;",
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
];
|
||||
|
||||
tauri::Builder::default()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Simpl Résultat",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.1",
|
||||
"identifier": "com.simpl.resultat",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ import {
|
|||
updateCategory,
|
||||
deactivateCategory,
|
||||
getCategoryUsageCount,
|
||||
getChildrenUsageCount,
|
||||
getKeywordsByCategoryId,
|
||||
createKeyword,
|
||||
updateKeyword,
|
||||
deactivateKeyword,
|
||||
reinitializeCategories as reinitializeCategoriesSvc,
|
||||
} from "../services/categoryService";
|
||||
|
||||
interface CategoriesState {
|
||||
|
|
@ -190,6 +192,11 @@ export function useCategories() {
|
|||
if (count > 0) {
|
||||
return { blocked: true, count };
|
||||
}
|
||||
// Also check children usage — they'll be promoted to root, not deleted
|
||||
const childrenCount = await getChildrenUsageCount(id);
|
||||
if (childrenCount > 0) {
|
||||
return { blocked: true, count: childrenCount };
|
||||
}
|
||||
dispatch({ type: "SET_SAVING", payload: true });
|
||||
try {
|
||||
await deactivateCategory(id);
|
||||
|
|
@ -205,6 +212,19 @@ export function useCategories() {
|
|||
[loadCategories]
|
||||
);
|
||||
|
||||
const reinitializeCategories = useCallback(async () => {
|
||||
dispatch({ type: "SET_SAVING", payload: true });
|
||||
dispatch({ type: "SET_ERROR", payload: null });
|
||||
try {
|
||||
await reinitializeCategoriesSvc();
|
||||
dispatch({ type: "SELECT_CATEGORY", payload: null });
|
||||
await loadCategories();
|
||||
dispatch({ type: "SET_SAVING", payload: false });
|
||||
} catch (e) {
|
||||
dispatch({ type: "SET_ERROR", payload: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
}, [loadCategories]);
|
||||
|
||||
const loadKeywords = useCallback(async (categoryId: number) => {
|
||||
try {
|
||||
const kws = await getKeywordsByCategoryId(categoryId);
|
||||
|
|
@ -268,5 +288,6 @@ export function useCategories() {
|
|||
addKeyword,
|
||||
editKeyword,
|
||||
removeKeyword,
|
||||
reinitializeCategories,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ export function useImportWizard() {
|
|||
let activeDelimiter = defaultConfig.delimiter;
|
||||
let activeEncoding = "utf-8";
|
||||
let activeSkipLines = 0;
|
||||
const activeHasHeader = true;
|
||||
let activeHasHeader = true;
|
||||
|
||||
if (existing) {
|
||||
// Restore config from DB
|
||||
|
|
@ -286,12 +286,13 @@ export function useImportWizard() {
|
|||
amountMode:
|
||||
mapping.debitAmount !== undefined ? "debit_credit" : "single",
|
||||
signConvention: "negative_expense",
|
||||
hasHeader: true,
|
||||
hasHeader: !!existing.has_header,
|
||||
};
|
||||
dispatch({ type: "SET_SOURCE_CONFIG", payload: config });
|
||||
activeDelimiter = existing.delimiter;
|
||||
activeEncoding = existing.encoding;
|
||||
activeSkipLines = existing.skip_lines;
|
||||
activeHasHeader = !!existing.has_header;
|
||||
} else {
|
||||
// Auto-detect encoding for first file
|
||||
if (source.files.length > 0) {
|
||||
|
|
@ -545,6 +546,7 @@ export function useImportWizard() {
|
|||
date_format: config.dateFormat,
|
||||
column_mapping: mappingJson,
|
||||
skip_lines: config.skipLines,
|
||||
has_header: config.hasHeader,
|
||||
});
|
||||
} else {
|
||||
sourceId = await createSource({
|
||||
|
|
@ -554,6 +556,7 @@ export function useImportWizard() {
|
|||
date_format: config.dateFormat,
|
||||
column_mapping: mappingJson,
|
||||
skip_lines: config.skipLines,
|
||||
has_header: config.hasHeader,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -230,8 +230,10 @@
|
|||
"addCategory": "Add Category",
|
||||
"editCategory": "Edit Category",
|
||||
"deleteCategory": "Delete Category",
|
||||
"deleteConfirm": "Are you sure you want to delete this category? Its children will also be deleted.",
|
||||
"deleteBlocked": "Cannot delete: this category is used by {{count}} transaction(s).",
|
||||
"deleteConfirm": "Are you sure you want to delete this category? Its children will be promoted to top-level.",
|
||||
"deleteBlocked": "Cannot delete: this category or its children are used by {{count}} transaction(s).",
|
||||
"reinitialize": "Re-initialize",
|
||||
"reinitializeConfirm": "Reset all categories and keywords to their default values? Transaction categories will be unlinked. This cannot be undone.",
|
||||
"noParent": "No parent (top-level)",
|
||||
"sortOrder": "Sort Order",
|
||||
"selectCategory": "Select a category to view details",
|
||||
|
|
|
|||
|
|
@ -230,8 +230,10 @@
|
|||
"addCategory": "Ajouter une catégorie",
|
||||
"editCategory": "Modifier la catégorie",
|
||||
"deleteCategory": "Supprimer la catégorie",
|
||||
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer cette catégorie ? Ses sous-catégories seront également supprimées.",
|
||||
"deleteBlocked": "Impossible de supprimer : cette catégorie est utilisée par {{count}} transaction(s).",
|
||||
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer cette catégorie ? Ses sous-catégories seront promues au niveau supérieur.",
|
||||
"deleteBlocked": "Impossible de supprimer : cette catégorie ou ses sous-catégories sont utilisées par {{count}} transaction(s).",
|
||||
"reinitialize": "Réinitialiser",
|
||||
"reinitializeConfirm": "Réinitialiser toutes les catégories et mots-clés à leurs valeurs par défaut ? Les catégories des transactions seront dissociées. Cette action est irréversible.",
|
||||
"noParent": "Aucun parent (niveau supérieur)",
|
||||
"sortOrder": "Ordre de tri",
|
||||
"selectCategory": "Sélectionnez une catégorie pour voir les détails",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Plus, RotateCcw } from "lucide-react";
|
||||
import { PageHelp } from "../components/shared/PageHelp";
|
||||
import { useCategories } from "../hooks/useCategories";
|
||||
import CategoryTree from "../components/categories/CategoryTree";
|
||||
|
|
@ -18,8 +18,15 @@ export default function CategoriesPage() {
|
|||
addKeyword,
|
||||
editKeyword,
|
||||
removeKeyword,
|
||||
reinitializeCategories,
|
||||
} = useCategories();
|
||||
|
||||
const handleReinitialize = async () => {
|
||||
if (confirm(t("categories.reinitializeConfirm"))) {
|
||||
await reinitializeCategories();
|
||||
}
|
||||
};
|
||||
|
||||
const selectedCategory =
|
||||
state.selectedCategoryId !== null
|
||||
? state.categories.find((c) => c.id === state.selectedCategoryId) ?? null
|
||||
|
|
@ -32,13 +39,23 @@ export default function CategoriesPage() {
|
|||
<h1 className="text-2xl font-bold">{t("categories.title")}</h1>
|
||||
<PageHelp helpKey="categories" />
|
||||
</div>
|
||||
<button
|
||||
onClick={startCreating}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[var(--primary)] text-white text-sm font-medium hover:opacity-90"
|
||||
>
|
||||
<Plus size={16} />
|
||||
{t("categories.addCategory")}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleReinitialize}
|
||||
disabled={state.isSaving}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-[var(--border)] text-sm font-medium hover:bg-[var(--muted)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
{t("categories.reinitialize")}
|
||||
</button>
|
||||
<button
|
||||
onClick={startCreating}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[var(--primary)] text-white text-sm font-medium hover:opacity-90"
|
||||
>
|
||||
<Plus size={16} />
|
||||
{t("categories.addCategory")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state.error && (
|
||||
|
|
|
|||
|
|
@ -59,8 +59,14 @@ export async function updateCategory(
|
|||
|
||||
export async function deactivateCategory(id: number): Promise<void> {
|
||||
const db = await getDb();
|
||||
// Promote children to root level so they don't become orphans
|
||||
await db.execute(
|
||||
`UPDATE categories SET is_active = 0 WHERE id = $1 OR parent_id = $1`,
|
||||
`UPDATE categories SET parent_id = NULL WHERE parent_id = $1`,
|
||||
[id]
|
||||
);
|
||||
// Only deactivate the target category itself
|
||||
await db.execute(
|
||||
`UPDATE categories SET is_active = 0 WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
}
|
||||
|
|
@ -74,6 +80,136 @@ export async function getCategoryUsageCount(id: number): Promise<number> {
|
|||
return rows[0]?.cnt ?? 0;
|
||||
}
|
||||
|
||||
export async function getChildrenUsageCount(parentId: number): Promise<number> {
|
||||
const db = await getDb();
|
||||
const rows = await db.select<Array<{ cnt: number }>>(
|
||||
`SELECT COUNT(*) AS cnt FROM transactions WHERE category_id IN
|
||||
(SELECT id FROM categories WHERE parent_id = $1 AND is_active = 1)`,
|
||||
[parentId]
|
||||
);
|
||||
return rows[0]?.cnt ?? 0;
|
||||
}
|
||||
|
||||
export async function reinitializeCategories(): Promise<void> {
|
||||
const db = await getDb();
|
||||
// Clear keywords, unlink transactions, delete all categories
|
||||
await db.execute("DELETE FROM keywords");
|
||||
await db.execute("UPDATE transactions SET category_id = NULL");
|
||||
await db.execute("DELETE FROM categories");
|
||||
|
||||
// Re-seed parent categories
|
||||
const parents = [
|
||||
[1, "Revenus", "income", 1],
|
||||
[2, "Dépenses récurrentes", "expense", 2],
|
||||
[3, "Dépenses ponctuelles", "expense", 3],
|
||||
[4, "Maison", "expense", 4],
|
||||
[5, "Placements", "transfer", 5],
|
||||
[6, "Autres", "expense", 6],
|
||||
] as const;
|
||||
for (const [id, name, type, sort] of parents) {
|
||||
await db.execute(
|
||||
"INSERT INTO categories (id, name, type, sort_order) VALUES ($1, $2, $3, $4)",
|
||||
[id, name, type, sort]
|
||||
);
|
||||
}
|
||||
|
||||
// Re-seed child categories
|
||||
const children: Array<[number, string, number, string, string, number]> = [
|
||||
[10, "Paie", 1, "income", "#22c55e", 1],
|
||||
[11, "Autres revenus", 1, "income", "#4ade80", 2],
|
||||
[20, "Loyer", 2, "expense", "#ef4444", 1],
|
||||
[21, "Électricité", 2, "expense", "#f59e0b", 2],
|
||||
[22, "Épicerie", 2, "expense", "#10b981", 3],
|
||||
[23, "Dons", 2, "expense", "#ec4899", 4],
|
||||
[24, "Restaurant", 2, "expense", "#f97316", 5],
|
||||
[25, "Frais bancaires", 2, "expense", "#6b7280", 6],
|
||||
[26, "Jeux, Films & Livres", 2, "expense", "#8b5cf6", 7],
|
||||
[27, "Abonnements Musique", 2, "expense", "#06b6d4", 8],
|
||||
[28, "Transport en commun", 2, "expense", "#3b82f6", 9],
|
||||
[29, "Internet & Télécom", 2, "expense", "#6366f1", 10],
|
||||
[30, "Animaux", 2, "expense", "#a855f7", 11],
|
||||
[31, "Assurances", 2, "expense", "#14b8a6", 12],
|
||||
[32, "Pharmacie", 2, "expense", "#f43f5e", 13],
|
||||
[33, "Taxes municipales", 2, "expense", "#78716c", 14],
|
||||
[40, "Voiture", 3, "expense", "#64748b", 1],
|
||||
[41, "Amazon", 3, "expense", "#f59e0b", 2],
|
||||
[42, "Électroniques", 3, "expense", "#3b82f6", 3],
|
||||
[43, "Alcool", 3, "expense", "#7c3aed", 4],
|
||||
[44, "Cadeaux", 3, "expense", "#ec4899", 5],
|
||||
[45, "Vêtements", 3, "expense", "#d946ef", 6],
|
||||
[46, "CPA", 3, "expense", "#0ea5e9", 7],
|
||||
[47, "Voyage", 3, "expense", "#f97316", 8],
|
||||
[48, "Sports & Plein air", 3, "expense", "#22c55e", 9],
|
||||
[49, "Spectacles & sorties", 3, "expense", "#e11d48", 10],
|
||||
[50, "Hypothèque", 4, "expense", "#dc2626", 1],
|
||||
[51, "Achats maison", 4, "expense", "#ea580c", 2],
|
||||
[52, "Entretien maison", 4, "expense", "#ca8a04", 3],
|
||||
[53, "Électroménagers & Meubles", 4, "expense", "#0d9488", 4],
|
||||
[54, "Outils", 4, "expense", "#b45309", 5],
|
||||
[60, "Placements", 5, "transfer", "#2563eb", 1],
|
||||
[61, "Transferts", 5, "transfer", "#7c3aed", 2],
|
||||
[70, "Impôts", 6, "expense", "#dc2626", 1],
|
||||
[71, "Paiement CC", 6, "transfer", "#6b7280", 2],
|
||||
[72, "Retrait cash", 6, "expense", "#57534e", 3],
|
||||
[73, "Projets", 6, "expense", "#0ea5e9", 4],
|
||||
];
|
||||
for (const [id, name, parentId, type, color, sort] of children) {
|
||||
await db.execute(
|
||||
"INSERT INTO categories (id, name, parent_id, type, color, sort_order) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
[id, name, parentId, type, color, sort]
|
||||
);
|
||||
}
|
||||
|
||||
// Re-seed keywords
|
||||
const keywords: Array<[string, number]> = [
|
||||
["PAY/PAY", 10],
|
||||
["HYDRO-QUEBEC", 21],
|
||||
["METRO", 22], ["IGA", 22], ["MAXI", 22], ["SUPER C", 22],
|
||||
["BOUCHERIE LAFLECHE", 22], ["BOULANGERIE JARRY", 22], ["DOLLARAMA", 22], ["WALMART", 22],
|
||||
["OXFAM", 23], ["CENTRAIDE", 23], ["FPA", 23],
|
||||
["SUBWAY", 24], ["MCDONALD", 24], ["A&W", 24], ["DD/DOORDASH", 24],
|
||||
["DOORDASH", 24], ["SUSHI", 24], ["DOMINOS", 24], ["BELLE PROVINCE", 24],
|
||||
["PROGRAMME PERFORMANCE", 25],
|
||||
["STEAMGAMES", 26], ["PLAYSTATION", 26], ["PRIMEVIDEO", 26], ["NINTENDO", 26],
|
||||
["RENAUD-BRAY", 26], ["CINEMA DU PARC", 26], ["LEGO", 26],
|
||||
["SPOTIFY", 27],
|
||||
["STM", 28], ["GARE MONT-SAINT", 28], ["GARE SAINT-HUBERT", 28],
|
||||
["GARE CENTRALE", 28], ["REM", 28],
|
||||
["VIDEOTRON", 29], ["ORICOM", 29],
|
||||
["MONDOU", 30],
|
||||
["BELAIR", 31], ["PRYSM", 31], ["INS/ASS", 31],
|
||||
["JEAN COUTU", 32], ["FAMILIPRIX", 32], ["PHARMAPRIX", 32],
|
||||
["M-ST-HILAIRE TX", 33], ["CSS PATRIOT", 33],
|
||||
["SHELL", 40], ["ESSO", 40], ["ULTRAMAR", 40], ["PETRO-CANADA", 40],
|
||||
["SAAQ", 40], ["CREVIER", 40],
|
||||
["AMAZON", 41], ["AMZN", 41],
|
||||
["MICROSOFT", 42], ["ADDISON ELECTRONIQUE", 42],
|
||||
["SAQ", 43], ["SQDC", 43],
|
||||
["DANS UN JARDIN", 44],
|
||||
["UNIQLO", 45], ["WINNERS", 45], ["SIMONS", 45],
|
||||
["ORDRE DES COMPTABL", 46],
|
||||
["NORWEGIAN CRUISE", 47], ["AEROPORTS DE MONTREAL", 47], ["HILTON", 47],
|
||||
["BLOC SHOP", 48], ["SEPAQ", 48], ["LA CORDEE", 48],
|
||||
["MOUNTAIN EQUIPMENT", 48], ["PHYSIOACTIF", 48], ["DECATHLON", 48],
|
||||
["TICKETMASTER", 49], ["CLUB SODA", 49], ["LEPOINTDEVENTE", 49],
|
||||
["MTG/HYP", 50],
|
||||
["CANADIAN TIRE", 51], ["CANAC", 51], ["RONA", 51],
|
||||
["IKEA", 52],
|
||||
["TANGUAY", 53], ["BOUCLAIR", 53],
|
||||
["BMR", 54], ["HOME DEPOT", 54], ["PRINCESS AUTO", 54],
|
||||
["DYNAMIC FUND", 60], ["FIDELITY", 60], ["AGF", 60],
|
||||
["WS INVESTMENTS", 61], ["PEAK INVESTMENT", 61],
|
||||
["GOUV. QUEBEC", 70],
|
||||
["CLAUDE.AI", 73], ["NAME-CHEAP", 73],
|
||||
];
|
||||
for (const [kw, catId] of keywords) {
|
||||
await db.execute(
|
||||
"INSERT INTO keywords (keyword, category_id) VALUES ($1, $2)",
|
||||
[kw, catId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getKeywordsByCategoryId(
|
||||
categoryId: number
|
||||
): Promise<Keyword[]> {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ export async function createSource(
|
|||
): Promise<number> {
|
||||
const db = await getDb();
|
||||
const result = await db.execute(
|
||||
`INSERT INTO import_sources (name, description, date_format, delimiter, encoding, column_mapping, skip_lines)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`INSERT INTO import_sources (name, description, date_format, delimiter, encoding, column_mapping, skip_lines, has_header)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
description = excluded.description,
|
||||
date_format = excluded.date_format,
|
||||
|
|
@ -42,6 +42,7 @@ export async function createSource(
|
|||
encoding = excluded.encoding,
|
||||
column_mapping = excluded.column_mapping,
|
||||
skip_lines = excluded.skip_lines,
|
||||
has_header = excluded.has_header,
|
||||
updated_at = CURRENT_TIMESTAMP`,
|
||||
[
|
||||
source.name,
|
||||
|
|
@ -51,6 +52,7 @@ export async function createSource(
|
|||
source.encoding,
|
||||
source.column_mapping,
|
||||
source.skip_lines,
|
||||
source.has_header ? 1 : 0,
|
||||
]
|
||||
);
|
||||
// On conflict, lastInsertId may be 0 — look up the existing row
|
||||
|
|
@ -96,6 +98,10 @@ export async function updateSource(
|
|||
fields.push(`skip_lines = $${paramIndex++}`);
|
||||
values.push(source.skip_lines);
|
||||
}
|
||||
if (source.has_header !== undefined) {
|
||||
fields.push(`has_header = $${paramIndex++}`);
|
||||
values.push(source.has_header ? 1 : 0);
|
||||
}
|
||||
|
||||
if (fields.length === 0) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export interface ImportSource {
|
|||
encoding: string;
|
||||
column_mapping: string;
|
||||
skip_lines: number;
|
||||
has_header: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,3 +35,9 @@
|
|||
**Pattern**: Identifier columns have very low cardinality relative to row count (often a single repeated value). Amount columns vary per transaction.
|
||||
**Rule**: When detecting "amount" columns in CSV auto-detect, exclude numeric columns with ≤1 distinct value. Also treat 0 as "empty" in sparse-complementary detection for debit/credit pairs.
|
||||
**Applied**: CSV auto-detection, any heuristic column type inference
|
||||
|
||||
## 2026-02-12 - Config fields must be persisted to DB, not hardcoded on restore
|
||||
**Mistake**: `hasHeader` was part of the SourceConfig in-memory object but was never stored in the `import_sources` DB table. When restoring config from DB, it was hardcoded to `true`, causing the first data row of headerless CSVs (Desjardins) to be treated as column headers.
|
||||
**Pattern**: When adding a config field to an in-memory interface, you must also add the column to the DB schema and update all CRUD paths (create, update, restore). Hardcoding a default on restore silently loses user preferences.
|
||||
**Rule**: For every field in a config interface: (1) verify it has a corresponding DB column, (2) verify it's included in INSERT/UPDATE queries, (3) verify it's restored from the DB row — never hardcoded. Use a grep for the field name across service, hook, and schema files.
|
||||
**Applied**: Import source config, any settings/preferences that need to survive across sessions
|
||||
|
|
|
|||
|
|
@ -1,29 +1,21 @@
|
|||
# Task: Fix 3 Desjardins CSV Import Bugs
|
||||
# Task: Fix orphan categories + add re-initialize button
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### Bug 1: No-header columns show "0: Col 0" for every column
|
||||
**Root cause:** `loadHeadersWithConfig` doesn't call `preprocessQuotedCSV()` before parsing. For Desjardins-style quoted CSVs, PapaParse sees each line as a single column. So `firstDataRow` has only 1 element → generates only `["Col 0"]`.
|
||||
|
||||
### Bug 2: Going back from preview loses column mapping
|
||||
**Root cause:** `parsePreview` only populates `headers` when `hasHeader: true`. When `hasHeader: false`, it leaves `headers = []`, overwriting `previewHeaders`. On source-config, `{headers.length > 0 && <ColumnMappingEditor />}` renders nothing.
|
||||
|
||||
### Bug 3: Auto-detect amount picks account number column
|
||||
**Root cause:** Constant numeric columns (account number, transit number) pass the "≥50% numeric" check. If the debit/credit columns have 0 instead of empty, `isSparseComplementary` fails. Falls back to first numeric candidate = account number.
|
||||
## Root Cause (orphan categories)
|
||||
`deactivateCategory` ran `SET is_active = 0 WHERE id = $1 OR parent_id = $1`, which silently
|
||||
deactivated ALL children when a parent was deleted — even children that had transactions assigned.
|
||||
Since `getAllCategoriesWithCounts` filters `WHERE is_active = 1`, those children vanished from the UI
|
||||
with no way to recover them.
|
||||
|
||||
## Plan
|
||||
- [x] Bug 1: Add `preprocessQuotedCSV()` in `loadHeadersWithConfig` before PapaParse
|
||||
- [x] Bug 2: In `parsePreview`, generate synthetic headers when `hasHeader: false`
|
||||
- [x] Bug 3a: Exclude constant-value numeric columns from amount candidates
|
||||
- [x] Bug 3b: Treat 0 values as empty in `isSparseComplementary`
|
||||
- [x] Build verification
|
||||
- [x] Update lessons.md
|
||||
|
||||
## Progress Notes
|
||||
- Bug 1: Added `preprocessQuotedCSV(preview)` call in `loadHeadersWithConfig` (useImportWizard.ts:341)
|
||||
- Bug 2: Added else-if branch in `parsePreview` to generate `Col N` headers when hasHeader is false (useImportWizard.ts:450-453)
|
||||
- Bug 3a: Added `distinctValues` tracking in `detectNumericColumns`, skip columns with ≤1 distinct value and >2 rows (csvAutoDetect.ts:227-236)
|
||||
- Bug 3b: Changed `isSparseComplementary` to parse values and treat `0` as empty (csvAutoDetect.ts:452-455)
|
||||
- [x] Fix `deactivateCategory`: promote children to root, only deactivate the parent itself
|
||||
- [x] Add `getChildrenUsageCount` to block deletion when children have transactions
|
||||
- [x] Add `reinitializeCategories` service function (re-runs seed data)
|
||||
- [x] Add `reinitializeCategories` to hook
|
||||
- [x] Add re-initialize button with confirmation on CategoriesPage
|
||||
- [x] Add i18n keys (en + fr)
|
||||
- [x] Update deleteConfirm/deleteBlocked messages to reflect new behavior
|
||||
- [x] `npm run build` passes
|
||||
|
||||
## Review
|
||||
Build passes. 2 files changed: useImportWizard.ts, csvAutoDetect.ts. All fixes are minimal and targeted.
|
||||
6 files changed. Orphan fix promotes children to root level instead of cascading deactivation.
|
||||
Re-initialize button resets all categories+keywords to seed state (with user confirmation).
|
||||
|
|
|
|||
Loading…
Reference in a new issue