mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 11:06:40 +07:00
Compare commits
6 Commits
14948aff1d
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93e6dcd889 | ||
|
|
64a190678e | ||
|
|
0cf72831a8 | ||
|
|
848ff21665 | ||
|
|
c867710f38 | ||
|
|
3bd484b88e |
12
DOCS.md
12
DOCS.md
@@ -368,6 +368,16 @@ Used by the CSV import wizard, but callable directly for any bulk load (e.g. syn
|
||||
export tool). All rows are validated **before** any is inserted — one bad row fails the whole
|
||||
request with no partial import.
|
||||
|
||||
The wizard itself (`/transactions/import`) is bank-agnostic: it auto-detects delimiter and
|
||||
encoding and lets you map columns by hand. On top of that it ships **bank profiles** that pre-fill
|
||||
the mapping and formats when the header row matches a known export (or when you pick the bank
|
||||
explicitly): **T-Bank** (semicolon CSV, `Сумма платежа`, rows with `Статус ≠ OK` skipped),
|
||||
**monobank** (Ukrainian-language CSV, `Сума в валюті картки (…)`) and **PrivatBank**
|
||||
(`Сума в валюті картки`, `Опис операції`). Profiles live in `frontend/src/lib/bankPresets.ts` —
|
||||
adding a bank is one declarative entry (header matchers, date/amount format, optional row filter).
|
||||
Banks that only export PDF/XLSX (Sber, Kaspi, Halyk, maib…) are imported via a third-party
|
||||
PDF→CSV converter and the manual mapping.
|
||||
|
||||
```json
|
||||
{ "items": [ /* 1–5000 TransactionCreate objects */ ] }
|
||||
```
|
||||
@@ -768,7 +778,7 @@ Single row, created automatically on first run — there's nothing to create, on
|
||||
"risky_allocation_threshold_percent": 20,
|
||||
"idle_cash_threshold_amount": "1000.00",
|
||||
"idle_cash_threshold_days": 60,
|
||||
"app_version": "1.1.7"
|
||||
"app_version": "1.1.8"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ Aurum treats your financial life as a **system**, not a spreadsheet.
|
||||
## 🧩 Core Features
|
||||
|
||||
### 💰 Cash Flow & Transactions
|
||||
Log income, expenses, and transfers across as many accounts as you want, organized into categories (with one level of subcategories) plus free-form tags, and searchable. One purchase spanning multiple subcategories of the same parent (a grocery receipt part "Sweets", part "Alcohol") can be recorded as a single split transaction instead of several — every report and chart still counts each share under its own category. Already have history elsewhere? Import a bank's CSV export in a guided 3-step wizard instead of typing every line by hand. A dedicated Cash Flow view charts income vs. expense month by month.
|
||||
Log income, expenses, and transfers across as many accounts as you want, organized into categories (with one level of subcategories) plus free-form tags, and searchable. One purchase spanning multiple subcategories of the same parent (a grocery receipt part "Sweets", part "Alcohol") can be recorded as a single split transaction instead of several — every report and chart still counts each share under its own category. Already have history elsewhere? Import a bank's CSV export in a guided 3-step wizard instead of typing every line by hand — with built-in profiles for T-Bank, monobank and PrivatBank exports (columns, date/amount formats and declined-payment rows handled automatically), and manual column mapping for everything else. A dedicated Cash Flow view charts income vs. expense month by month.
|
||||
|
||||
### 📈 Net Worth Engine
|
||||
A live net worth timeline (30 days to all-time) aggregating cash and every manually tracked asset — investments, crypto, real estate, vehicles, precious metals — into one number, with a full breakdown by asset class, by how each asset behaves (income / neutral / drain), and by risk level.
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
# it in Settings; it deliberately does NOT ride on the unauthenticated
|
||||
# /api/health), and embedded in exported backups so an old file can be told
|
||||
# apart from a current one.
|
||||
APP_VERSION = "1.1.7"
|
||||
APP_VERSION = "1.1.8"
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
# backend container (see backend/tests/README.md) rather than baking these
|
||||
# into the Docker image.
|
||||
-r requirements.txt
|
||||
pytest==9.0.3
|
||||
pytest==9.1.1
|
||||
pytest-asyncio==1.4.0
|
||||
httpx==0.28.1
|
||||
|
||||
@@ -2,7 +2,7 @@ fastapi==0.141.1
|
||||
uvicorn[standard]==0.52.4
|
||||
sqlalchemy==2.0.52
|
||||
asyncpg==0.31.0
|
||||
alembic==1.19.2
|
||||
alembic==1.20.0
|
||||
pydantic==2.13.5
|
||||
pydantic-settings==2.15.0
|
||||
python-dotenv==1.2.3
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0b0b0b" />
|
||||
<title>Aurum — Личные финансы</title>
|
||||
<title>Aurum — Personal Finance</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Aurum is a self-hosted personal finance operating system: cash flow, net worth, budgets, subscriptions, assets, and every income stream in one place."
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="personal finance, finance tracker, budgeting, self-hosted, cash flow, net worth, expense tracker, subscription tracker, crypto tracker, family budget"
|
||||
/>
|
||||
<meta name="author" content="Aurum" />
|
||||
<!-- Self-hosted app without a fixed public domain, so og:url/og:image
|
||||
are left out — each deployment would need its own values. -->
|
||||
<meta property="og:title" content="Aurum — Personal Finance" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A self-hosted personal finance operating system: cash flow, net worth, budgets, subscriptions, and assets."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="Aurum — Personal Finance" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="A self-hosted personal finance operating system: cash flow, net worth, budgets, subscriptions, and assets."
|
||||
/>
|
||||
<!-- Runs before the first paint to avoid a wrong-theme flash. Kept in its
|
||||
own file (public/theme-init.js), not inline, so the CSP in
|
||||
frontend/nginx.conf can forbid inline scripts entirely. -->
|
||||
|
||||
508
frontend/package-lock.json
generated
508
frontend/package-lock.json
generated
@@ -1,66 +1,66 @@
|
||||
{
|
||||
"name": "aurum-frontend",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aurum-frontend",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.8",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@tanstack/react-query": "^5.104.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.31.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-router-dom": "^7.18.3",
|
||||
"lucide-react": "^1.48.0",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-router-dom": "^7.18.4",
|
||||
"recharts": "^3.10.1",
|
||||
"tailwind-merge": "^3.6.0"
|
||||
"tailwind-merge": "^3.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@types/node": "^26.4.1",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.7",
|
||||
"@types/node": "^26.6.3",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"jsdom": "^30.0.1",
|
||||
"jsdom": "^30.1.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.2.2",
|
||||
"vitest": "^5.0.0"
|
||||
"vite": "^8.3.1",
|
||||
"vitest": "^5.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "6.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz",
|
||||
"integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.0.tgz",
|
||||
"integrity": "sha512-s6vMrO4hds+skR2OMmdwSORZiiEbqhAjd56W/0gc/x4PYpTibh/ey5Oy9ywkZ+ycX9EgTcJruhE1T/KkbU7LNA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^3.3.0",
|
||||
"@csstools/css-color-parser": "^4.1.10",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0",
|
||||
"lru-cache": "^11.5.2"
|
||||
"@csstools/css-calc": "^3.4.1",
|
||||
"@csstools/css-color-parser": "^4.2.4",
|
||||
"@csstools/css-parser-algorithms": "^4.0.1",
|
||||
"@csstools/css-tokenizer": "^4.0.2",
|
||||
"lru-cache": "^11.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz",
|
||||
"integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==",
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.1.tgz",
|
||||
"integrity": "sha512-NT4s3yZLjovPpliRpTvdsdzyPjqRqiCZj9MxnarBihbaY5MbAG7DWAJcrLlhmC7xKTNCXsTELcqB8YsqpLnUFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bidi-js": "^1.0.3",
|
||||
"bidi-js": "^1.1.0",
|
||||
"css-tree": "^3.2.1",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2"
|
||||
"lru-cache": "^11.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bramus/specificity": {
|
||||
@@ -77,9 +77,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz",
|
||||
"integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==",
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz",
|
||||
"integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -97,9 +97,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz",
|
||||
"integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.1.tgz",
|
||||
"integrity": "sha512-EtC7SoN1j6J4E4DCwg5QgbO5TGxgxIA1RXqe+W+qUM+BUcezx9wT+/tiQ/WO2yCX4i5X+Cuf9ciJ22aP4UEwWw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -116,14 +116,14 @@
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
"@csstools/css-parser-algorithms": "^4.0.1",
|
||||
"@csstools/css-tokenizer": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz",
|
||||
"integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==",
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-DyefytAZ735mX4Dq/WcDAXFtXhaEFvme0ZS9tVEBAc2whxUthXr0R0L2rmEPm59SrMBkGrFzQviBXMs/UtnABQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -137,21 +137,21 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^6.1.1",
|
||||
"@csstools/css-calc": "^3.3.0"
|
||||
"@csstools/color-helpers": "^6.1.2",
|
||||
"@csstools/css-calc": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
"@csstools/css-parser-algorithms": "^4.0.1",
|
||||
"@csstools/css-tokenizer": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
||||
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.1.tgz",
|
||||
"integrity": "sha512-ShL8BqPfbKJrJiKFH0xBbN0i7Nrh9HXYRuF+pzyj93R/BL2YAsUxJeqANErzqM+0JGl7vjHp+3OIgd/DL69YIA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -168,13 +168,13 @@
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
"@csstools/css-tokenizer": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.12.tgz",
|
||||
"integrity": "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==",
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz",
|
||||
"integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -197,9 +197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
||||
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz",
|
||||
"integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -267,9 +267,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -285,13 +285,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.144.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz",
|
||||
"integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==",
|
||||
"version": "0.151.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz",
|
||||
"integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
"url": "https://github.com/sponsors/oxc-project"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
@@ -320,10 +320,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm-eabi": {
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz",
|
||||
"integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz",
|
||||
"integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -338,9 +355,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz",
|
||||
"integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -355,9 +372,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz",
|
||||
"integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -372,9 +389,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz",
|
||||
"integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -389,9 +406,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz",
|
||||
"integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz",
|
||||
"integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -406,9 +423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz",
|
||||
"integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz",
|
||||
"integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -426,9 +443,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz",
|
||||
"integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz",
|
||||
"integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -446,9 +463,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz",
|
||||
"integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz",
|
||||
"integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -466,9 +483,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz",
|
||||
"integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz",
|
||||
"integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -486,9 +503,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz",
|
||||
"integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz",
|
||||
"integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -506,9 +523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz",
|
||||
"integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz",
|
||||
"integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -526,9 +543,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz",
|
||||
"integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -543,9 +560,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz",
|
||||
"integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz",
|
||||
"integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -560,9 +577,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz",
|
||||
"integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz",
|
||||
"integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -946,9 +963,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.101.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz",
|
||||
"integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==",
|
||||
"version": "5.104.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.104.0.tgz",
|
||||
"integrity": "sha512-JrC2r/JQlXt7khBSdUpsgxNvybzOg+aITa+ARRMlP2AFo93Y8vIqz077rp+e61mJetSZ0T6AJbwW1JHer1vrPQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -956,12 +973,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.101.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz",
|
||||
"integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==",
|
||||
"version": "5.104.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.104.0.tgz",
|
||||
"integrity": "sha512-e1TZmDCQnWIfiDVryIHeA6Idj+Lfx1hORLOhXS/l5wcgvtVD4yYqbTDl378sGQKIi2cSgb0TEMC9cKK8GGoJEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.101.4"
|
||||
"@tanstack/query-core": "5.104.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -1060,19 +1077,19 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz",
|
||||
"integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==",
|
||||
"version": "26.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz",
|
||||
"integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
"undici-types": "~8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
||||
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
|
||||
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1080,13 +1097,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.7.tgz",
|
||||
"integrity": "sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==",
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
|
||||
"integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
"@types/react": "^19.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
@@ -1466,14 +1483,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz",
|
||||
"integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz",
|
||||
"integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@vitest/spy": "5.0.0",
|
||||
"@vitest/spy": "5.0.2",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^1.2.3"
|
||||
},
|
||||
@@ -1494,19 +1511,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker/node_modules/magic-string": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.3.tgz",
|
||||
"integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz",
|
||||
"integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz",
|
||||
"integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -1871,16 +1888,16 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz",
|
||||
"integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.6.0"
|
||||
"@exodus/bytes": "^1.15.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
@@ -1920,32 +1937,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz",
|
||||
"integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==",
|
||||
"version": "30.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.1.tgz",
|
||||
"integrity": "sha512-FahmoPK5vbPc+jxV1iErMHmAZypCZ942NHF4+qqaWAuvaKKTBZxawnmAtrbGWLU7MtlxfqIP0qw6aSI+aWGtLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^6.0.5",
|
||||
"@asamuzakjp/dom-selector": "^8.3.0",
|
||||
"@asamuzakjp/css-color": "^7.0.0",
|
||||
"@asamuzakjp/dom-selector": "^9.2.1",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.7",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.13",
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"css-tree": "^3.2.1",
|
||||
"data-urls": "^7.0.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^6.0.0",
|
||||
"html-encoding-sniffer": "^7.0.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2",
|
||||
"parse5": "^8.0.1",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.2",
|
||||
"undici": "^8.9.0",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"undici": "^8.10.2",
|
||||
"w3c-xmlserializer": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^17.1.0",
|
||||
"whatwg-url": "^17.1.1",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2234,9 +2250,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||
"version": "11.5.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
|
||||
"integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
@@ -2244,9 +2260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "1.31.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.31.0.tgz",
|
||||
"integrity": "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg==",
|
||||
"version": "1.48.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.48.0.tgz",
|
||||
"integrity": "sha512-R0CIKY/fXiC6y9xRBADgsK+VW2p/pcTJOMhLZf1T+uG+vJUvyUR02nGOgmIIC7MPkiNK4Ox8QDnbqF8rJYWPZQ==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -2270,9 +2286,9 @@
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"version": "3.3.19",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
|
||||
"integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2336,9 +2352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.26",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||
"version": "8.5.28",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
|
||||
"integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2356,7 +2372,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.17",
|
||||
"nanoid": "^3.3.18",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -2375,24 +2391,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
|
||||
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
|
||||
"integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
"scheduler": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.8"
|
||||
"react": "^19.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
@@ -2426,9 +2442,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz",
|
||||
"integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==",
|
||||
"version": "7.18.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz",
|
||||
"integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
@@ -2448,12 +2464,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz",
|
||||
"integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==",
|
||||
"version": "7.18.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz",
|
||||
"integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.18.3"
|
||||
"react-router": "7.18.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -2525,13 +2541,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz",
|
||||
"integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz",
|
||||
"integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.144.0",
|
||||
"@oxc-project/types": "=0.151.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -2541,20 +2557,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.2.4",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.4",
|
||||
"@rolldown/binding-darwin-x64": "1.2.4",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.4",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.4",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.4",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.4",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.4",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.4",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.4",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.4",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.4",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.4",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.4"
|
||||
"@rolldown/binding-android-arm-eabi": "1.2.11",
|
||||
"@rolldown/binding-android-arm64": "1.2.11",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.11",
|
||||
"@rolldown/binding-darwin-x64": "1.2.11",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.11",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.11",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.11",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.11",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.11",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.11",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.11",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.11",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.11",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.11",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
@@ -2571,9 +2588,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
|
||||
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
@@ -2582,13 +2599,6 @@
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -2599,13 +2609,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
|
||||
@@ -2613,17 +2616,10 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
|
||||
"integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz",
|
||||
"integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2786,9 +2782,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
||||
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2824,16 +2820,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
|
||||
"integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz",
|
||||
"integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.33.0",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.26",
|
||||
"rolldown": "~1.2.4",
|
||||
"picomatch": "^4.0.7",
|
||||
"postcss": "^8.5.28",
|
||||
"rolldown": "~1.2.9",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
@@ -2850,7 +2846,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.4.0 || ^0.5.0",
|
||||
"@vitejs/devtools": "^0.7.1",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
@@ -3175,14 +3171,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz",
|
||||
"integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz",
|
||||
"integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/mocker": "5.0.0",
|
||||
"@vitest/mocker": "5.0.2",
|
||||
"chai": "^6.2.2",
|
||||
"es-module-lexer": "^2.3.2",
|
||||
"expect-type": "^1.4.0",
|
||||
@@ -3190,10 +3186,10 @@
|
||||
"obug": "^2.1.4",
|
||||
"picomatch": "^4.0.7",
|
||||
"std-env": "^4.2.0",
|
||||
"tinybench": "6.1.4",
|
||||
"tinyexec": "1.3.0",
|
||||
"tinybench": "^6.1.4",
|
||||
"tinyexec": "^1.3.0",
|
||||
"tinyglobby": "^0.2.17",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
"why-is-node-running": "^3.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
@@ -3208,12 +3204,12 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "5.0.0",
|
||||
"@vitest/browser-preview": "5.0.0",
|
||||
"@vitest/browser-playwright": "5.0.2",
|
||||
"@vitest/browser-preview": "5.0.2",
|
||||
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
|
||||
"@vitest/coverage-istanbul": "5.0.0",
|
||||
"@vitest/coverage-v8": "5.0.0",
|
||||
"@vitest/ui": "5.0.0",
|
||||
"@vitest/coverage-istanbul": "5.0.2",
|
||||
"@vitest/coverage-v8": "5.0.2",
|
||||
"@vitest/ui": "5.0.2",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
||||
@@ -3268,16 +3264,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz",
|
||||
"integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
@@ -3301,9 +3297,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "17.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz",
|
||||
"integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==",
|
||||
"version": "17.1.2",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.2.tgz",
|
||||
"integrity": "sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3316,20 +3312,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz",
|
||||
"integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=20.11"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "aurum-frontend",
|
||||
"private": true,
|
||||
"version": "1.1.7",
|
||||
"version": "1.1.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -11,25 +11,25 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@tanstack/react-query": "^5.104.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.31.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-router-dom": "^7.18.3",
|
||||
"lucide-react": "^1.48.0",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-router-dom": "^7.18.4",
|
||||
"recharts": "^3.10.1",
|
||||
"tailwind-merge": "^3.6.0"
|
||||
"tailwind-merge": "^3.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@types/node": "^26.4.1",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.7",
|
||||
"@types/node": "^26.6.3",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"jsdom": "^30.0.1",
|
||||
"jsdom": "^30.1.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.2.2",
|
||||
"vitest": "^5.0.0"
|
||||
"vite": "^8.3.1",
|
||||
"vitest": "^5.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
119
frontend/src/lib/bankPresets.test.ts
Normal file
119
frontend/src/lib/bankPresets.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyPresetMapping, detectPreset, findPreset, normalizeHeader, rowFilterRejection, type ImportMapping } from "@/lib/bankPresets";
|
||||
import { parseAmount, parseCsv, parseDateWithFormat } from "@/lib/csv";
|
||||
|
||||
const EMPTY: ImportMapping = { date: "", amount: "", description: "", merchant: "", notes: "", category: "" };
|
||||
|
||||
// Header rows as the banks actually emit them (see the per-preset comments in
|
||||
// lib/bankPresets.ts for where each export comes from).
|
||||
const TBANK_CSV = [
|
||||
'"Дата операции";"Дата платежа";"Номер карты";"Статус";"Сумма операции";"Валюта операции";"Сумма платежа";"Валюта платежа";"Кэшбэк";"Категория";"MCC";"Описание";"Бонусы (включая кэшбэк)"',
|
||||
'"24.10.2018 17:04:25";"24.10.2018";"*8305";"OK";"-882,04";"RUB";"-882,04";"RUB";"";"Переводы/иб";"";"Пополнение счета Тинькофф Брокер";"0,00"',
|
||||
'"25.10.2018 09:12:00";"25.10.2018";"*8305";"FAILED";"-1500,00";"RUB";"-1500,00";"RUB";"";"Супермаркеты";"5411";"Пятёрочка";"0,00"',
|
||||
].join("\n");
|
||||
|
||||
const MONOBANK_CSV = [
|
||||
// Latin "i" in "Дата i час" is deliberate — that's what the real file has.
|
||||
'"Дата i час операції","Деталі операції",MCC,"Сума в валюті картки (UAH)","Сума в валюті операції",Валюта,Курс,"Сума комісій (UAH)","Сума кешбеку (UAH)","Залишок після операції"',
|
||||
'"01.03.2024 12:30:45","Сільпо",5411,-250.50,-250.50,UAH,—,—,2.50,10000.00',
|
||||
].join("\n");
|
||||
|
||||
const PRIVAT_CSV = [
|
||||
"Дата;Час;Категорія;Картка;Опис операції;Сума в валюті картки;Валюта картки;Сума в валюті транзакції;Валюта транзакції;Залишок на кінець періоду;Валюта залишку",
|
||||
"05.02.2024;14:05;Продукти;5168****1234;АТБ;-320,00;UAH;-320,00;UAH;1500,00;UAH",
|
||||
].join("\n");
|
||||
|
||||
describe("normalizeHeader", () => {
|
||||
it("folds case, whitespace, BOM and the Latin i", () => {
|
||||
expect(normalizeHeader(" Дата i час ")).toBe("дата і час");
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectPreset", () => {
|
||||
it("recognises T-Bank, monobank and PrivatBank header rows", () => {
|
||||
expect(detectPreset(parseCsv(TBANK_CSV)[0])?.id).toBe("tbank");
|
||||
expect(detectPreset(parseCsv(MONOBANK_CSV)[0])?.id).toBe("monobank");
|
||||
expect(detectPreset(parseCsv(PRIVAT_CSV)[0])?.id).toBe("privatbank");
|
||||
});
|
||||
|
||||
it("returns null for a generic export", () => {
|
||||
expect(detectPreset(["Date", "Amount", "Description"])).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyPresetMapping", () => {
|
||||
it("maps T-Bank onto the card-currency amount and keeps the category", () => {
|
||||
const headers = parseCsv(TBANK_CSV)[0];
|
||||
const mapping = applyPresetMapping(findPreset("tbank")!, headers, EMPTY);
|
||||
expect(mapping).toMatchObject({ date: "Дата операции", amount: "Сумма платежа", description: "Описание", category: "Категория" });
|
||||
});
|
||||
|
||||
it("matches monobank's currency-suffixed amount header by prefix", () => {
|
||||
const headers = parseCsv(MONOBANK_CSV)[0];
|
||||
const mapping = applyPresetMapping(findPreset("monobank")!, headers, EMPTY);
|
||||
expect(mapping.amount).toBe("Сума в валюті картки (UAH)");
|
||||
expect(mapping.date).toBe("Дата i час операції");
|
||||
});
|
||||
|
||||
it("leaves fields the preset doesn't cover on the fallback", () => {
|
||||
const headers = parseCsv(PRIVAT_CSV)[0];
|
||||
const mapping = applyPresetMapping(findPreset("privatbank")!, headers, { ...EMPTY, notes: "Картка" });
|
||||
expect(mapping.notes).toBe("Картка");
|
||||
expect(mapping.category).toBe("Категорія");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rowFilterRejection", () => {
|
||||
it("drops T-Bank rows whose status is not OK and reports the status", () => {
|
||||
const [headers, ok, failed] = parseCsv(TBANK_CSV);
|
||||
const preset = findPreset("tbank")!;
|
||||
expect(rowFilterRejection(preset, headers, ok)).toBeNull();
|
||||
expect(rowFilterRejection(preset, headers, failed)).toBe("FAILED");
|
||||
});
|
||||
|
||||
it("keeps everything when the preset has no filter or the column is absent", () => {
|
||||
const [headers, row] = parseCsv(MONOBANK_CSV);
|
||||
expect(rowFilterRejection(findPreset("monobank")!, headers, row)).toBeNull();
|
||||
expect(rowFilterRejection(findPreset("tbank")!, ["Дата"], ["01.01.2024"])).toBeNull();
|
||||
expect(rowFilterRejection(null, headers, row)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("preset formats against real sample rows", () => {
|
||||
it("parses T-Bank's timestamped date and comma-decimal amount", () => {
|
||||
const [, row] = parseCsv(TBANK_CSV);
|
||||
const preset = findPreset("tbank")!;
|
||||
expect(parseDateWithFormat(row[0], preset.dateFormat)).toBe("2018-10-24");
|
||||
expect(parseAmount(row[6], preset.amountFormat)).toBe(-882.04);
|
||||
});
|
||||
|
||||
it("parses monobank's timestamped date and dot-decimal amount", () => {
|
||||
const [, row] = parseCsv(MONOBANK_CSV);
|
||||
const preset = findPreset("monobank")!;
|
||||
expect(parseDateWithFormat(row[0], preset.dateFormat)).toBe("2024-03-01");
|
||||
expect(parseAmount(row[3], preset.amountFormat)).toBe(-250.5);
|
||||
// monobank writes empty cells as an em dash — must not parse as 0.
|
||||
expect(parseAmount(row[6], preset.amountFormat)).toBeNull();
|
||||
});
|
||||
|
||||
it("parses PrivatBank's plain date and comma amount", () => {
|
||||
const [, row] = parseCsv(PRIVAT_CSV);
|
||||
const preset = findPreset("privatbank")!;
|
||||
expect(parseDateWithFormat(row[0], preset.dateFormat)).toBe("2024-02-05");
|
||||
expect(parseAmount(row[5], preset.amountFormat)).toBe(-320);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseDateWithFormat with a time suffix", () => {
|
||||
it("accepts an optional trailing time in every format", () => {
|
||||
expect(parseDateWithFormat("2024-01-05T09:30", "YYYY-MM-DD")).toBe("2024-01-05");
|
||||
expect(parseDateWithFormat("05/01/2024 09:30:15", "DD/MM/YYYY")).toBe("2024-01-05");
|
||||
expect(parseDateWithFormat("01/05/2024 9:30", "MM/DD/YYYY")).toBe("2024-01-05");
|
||||
expect(parseDateWithFormat("05-01-2024 09:30", "DD-MM-YYYY")).toBe("2024-01-05");
|
||||
});
|
||||
|
||||
it("still rejects trailing garbage that isn't a time", () => {
|
||||
expect(parseDateWithFormat("05.01.2024 abc", "DD.MM.YYYY")).toBeNull();
|
||||
expect(parseDateWithFormat("05.01.2024 09", "DD.MM.YYYY")).toBeNull();
|
||||
});
|
||||
});
|
||||
152
frontend/src/lib/bankPresets.ts
Normal file
152
frontend/src/lib/bankPresets.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
// Bank-specific import presets for the CSV wizard (pages/CsvImportPage.tsx).
|
||||
//
|
||||
// A preset is nothing more than a pre-filled answer to the wizard's "map"
|
||||
// step: which column is the date, which is the amount, what the date/amount
|
||||
// formats are, and — the one thing the generic mapping can't express — which
|
||||
// rows to drop (a declined card payment still shows up in the export). The
|
||||
// wizard itself stays bank-agnostic: presets only feed it defaults, and the
|
||||
// user can still override any of them on the map screen.
|
||||
//
|
||||
// Only banks whose personal-account CSV export has a known, stable header
|
||||
// layout are listed. Banks that export PDF/XLSX only (Sber, Kaspi, Halyk,
|
||||
// maib…) go through the generic mapping via a third-party converter instead.
|
||||
import type { AmountFormat, DateFormat } from "@/lib/csv";
|
||||
|
||||
/** Column mapping as the wizard understands it — header *names*, not
|
||||
* indexes, so re-decoding the file under another encoding can re-resolve
|
||||
* them. Empty string = not mapped. */
|
||||
export interface ImportMapping {
|
||||
date: string;
|
||||
amount: string;
|
||||
description: string;
|
||||
merchant: string;
|
||||
notes: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
/** Matches a header cell by normalised prefix — see normalizeHeader(). A
|
||||
* prefix, not equality, because some banks put the account currency in the
|
||||
* header itself ("Сума в валюті картки (UAH)") and it changes per card. */
|
||||
export type HeaderMatcher = string[];
|
||||
|
||||
export interface BankPreset {
|
||||
id: string;
|
||||
/** Proper name, shown as-is — bank names aren't translated. */
|
||||
label: string;
|
||||
/** Every one of these must be present for detectPreset() to pick this
|
||||
* preset. Keep it to the columns the mapping actually needs, so a bank
|
||||
* adding a trailing column doesn't break detection. */
|
||||
detect: HeaderMatcher[];
|
||||
mapping: Partial<Record<keyof ImportMapping, HeaderMatcher>>;
|
||||
dateFormat: DateFormat;
|
||||
amountFormat: AmountFormat;
|
||||
/** Rows whose `column` value fails `accept` are skipped before parsing —
|
||||
* e.g. a "Статус" column that reads FAILED for a declined payment. */
|
||||
rowFilter?: { column: HeaderMatcher; accept: (value: string) => boolean };
|
||||
}
|
||||
|
||||
/** Lower-cased, trimmed, BOM-stripped, whitespace-collapsed header text.
|
||||
* Also folds the Latin "i" into the Cyrillic "і": monobank's Ukrainian header
|
||||
* literally reads "Дата i час операції" with a Latin i in the middle, and
|
||||
* nobody typing a matcher by hand would guess that. */
|
||||
export function normalizeHeader(header: string): string {
|
||||
return header
|
||||
.replace(/^\uFEFF/, "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/i/g, "і");
|
||||
}
|
||||
|
||||
function findHeader(headers: string[], matcher: HeaderMatcher): string {
|
||||
const candidates = matcher.map(normalizeHeader);
|
||||
return headers.find((header) => candidates.some((candidate) => normalizeHeader(header).startsWith(candidate))) ?? "";
|
||||
}
|
||||
|
||||
export const BANK_PRESETS: readonly BankPreset[] = [
|
||||
{
|
||||
// Web version → Операции → «Выгрузка операций» → CSV. Semicolon-separated,
|
||||
// every field quoted, comma decimal mark, dates carry a time of day.
|
||||
// "Сумма платежа" is the amount in the card's own currency (a foreign
|
||||
// purchase shows the original amount in "Сумма операции"), which is what
|
||||
// the account balance actually moved by.
|
||||
id: "tbank",
|
||||
label: "Т-Банк",
|
||||
detect: [["дата операции"], ["сумма платежа"], ["описание"], ["статус"]],
|
||||
mapping: {
|
||||
date: ["дата операции"],
|
||||
amount: ["сумма платежа"],
|
||||
description: ["описание"],
|
||||
category: ["категория"],
|
||||
},
|
||||
dateFormat: "DD.MM.YYYY",
|
||||
amountFormat: "comma-decimal",
|
||||
rowFilter: { column: ["статус"], accept: (value) => value.trim().toUpperCase() === "OK" },
|
||||
},
|
||||
{
|
||||
// App → card → Виписка → CSV, Ukrainian UI language. Comma-separated,
|
||||
// UTF-8, dot decimal mark, empty cells written as an em dash ("—").
|
||||
// The amount header embeds the card currency, hence the prefix match.
|
||||
id: "monobank",
|
||||
label: "monobank",
|
||||
detect: [["дата і час операції"], ["деталі операції"], ["сума в валюті картки"]],
|
||||
mapping: {
|
||||
date: ["дата і час операції"],
|
||||
amount: ["сума в валюті картки"],
|
||||
description: ["деталі операції"],
|
||||
},
|
||||
dateFormat: "DD.MM.YYYY",
|
||||
amountFormat: "dot-decimal",
|
||||
},
|
||||
{
|
||||
// next.privat24.ua → card → Виписки → CSV. Date and time come in
|
||||
// separate columns, so the plain date parses as-is; the amount in the
|
||||
// card's currency is what the balance moved by.
|
||||
id: "privatbank",
|
||||
label: "ПриватБанк",
|
||||
detect: [["дата"], ["опис операції"], ["сума в валюті картки"]],
|
||||
mapping: {
|
||||
date: ["дата"],
|
||||
amount: ["сума в валюті картки"],
|
||||
description: ["опис операції"],
|
||||
category: ["категорія"],
|
||||
},
|
||||
dateFormat: "DD.MM.YYYY",
|
||||
amountFormat: "auto",
|
||||
},
|
||||
];
|
||||
|
||||
export function findPreset(id: string): BankPreset | null {
|
||||
return BANK_PRESETS.find((preset) => preset.id === id) ?? null;
|
||||
}
|
||||
|
||||
/** The first preset whose every `detect` matcher finds a header. Order in
|
||||
* BANK_PRESETS matters only if two banks ever share a header set. */
|
||||
export function detectPreset(headers: string[]): BankPreset | null {
|
||||
return BANK_PRESETS.find((preset) => preset.detect.every((matcher) => findHeader(headers, matcher) !== "")) ?? null;
|
||||
}
|
||||
|
||||
/** Resolves a preset's matchers against the actual header row. Fields the
|
||||
* preset doesn't cover fall back to `fallback` (the wizard's generic
|
||||
* header guesses), so a preset only has to describe what it knows. */
|
||||
export function applyPresetMapping(preset: BankPreset, headers: string[], fallback: ImportMapping): ImportMapping {
|
||||
const resolved = { ...fallback };
|
||||
(Object.keys(preset.mapping) as (keyof ImportMapping)[]).forEach((field) => {
|
||||
const matcher = preset.mapping[field];
|
||||
if (matcher) resolved[field] = findHeader(headers, matcher);
|
||||
});
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/** When `preset.rowFilter` says this row is not a real transaction, returns
|
||||
* the offending cell value (for the "skipped because…" message); null means
|
||||
* keep the row. Also null when the preset has no filter or its column is
|
||||
* missing from the header row — an absent status column is not a reason
|
||||
* to drop data. */
|
||||
export function rowFilterRejection(preset: BankPreset | null, headers: string[], cells: string[]): string | null {
|
||||
if (!preset?.rowFilter) return null;
|
||||
const idx = headers.indexOf(findHeader(headers, preset.rowFilter.column));
|
||||
if (idx < 0) return null;
|
||||
const value = cells[idx] ?? "";
|
||||
return preset.rowFilter.accept(value) ? null : value.trim();
|
||||
}
|
||||
@@ -57,12 +57,17 @@ export function parseCsv(text: string): string[][] {
|
||||
export const DATE_FORMATS = ["YYYY-MM-DD", "DD.MM.YYYY", "DD/MM/YYYY", "MM/DD/YYYY", "DD-MM-YYYY"] as const;
|
||||
export type DateFormat = (typeof DATE_FORMATS)[number];
|
||||
|
||||
// Every pattern tolerates an optional trailing time of day ("24.10.2018
|
||||
// 17:04:25", "2024-01-05T09:30") — bank exports routinely timestamp each
|
||||
// operation (T-Bank, monobank), and the wizard only stores the calendar
|
||||
// date anyway.
|
||||
const TIME_SUFFIX = /(?:[ T]\d{1,2}:\d{2}(?::\d{2})?)?$/.source;
|
||||
const DATE_PATTERNS: Record<DateFormat, { regex: RegExp; order: ["y" | "m" | "d", "y" | "m" | "d", "y" | "m" | "d"] }> = {
|
||||
"YYYY-MM-DD": { regex: /^(\d{4})-(\d{1,2})-(\d{1,2})$/, order: ["y", "m", "d"] },
|
||||
"DD.MM.YYYY": { regex: /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/, order: ["d", "m", "y"] },
|
||||
"DD/MM/YYYY": { regex: /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/, order: ["d", "m", "y"] },
|
||||
"MM/DD/YYYY": { regex: /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/, order: ["m", "d", "y"] },
|
||||
"DD-MM-YYYY": { regex: /^(\d{1,2})-(\d{1,2})-(\d{4})$/, order: ["d", "m", "y"] },
|
||||
"YYYY-MM-DD": { regex: new RegExp(/^(\d{4})-(\d{1,2})-(\d{1,2})/.source + TIME_SUFFIX), order: ["y", "m", "d"] },
|
||||
"DD.MM.YYYY": { regex: new RegExp(/^(\d{1,2})\.(\d{1,2})\.(\d{4})/.source + TIME_SUFFIX), order: ["d", "m", "y"] },
|
||||
"DD/MM/YYYY": { regex: new RegExp(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/.source + TIME_SUFFIX), order: ["d", "m", "y"] },
|
||||
"MM/DD/YYYY": { regex: new RegExp(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/.source + TIME_SUFFIX), order: ["m", "d", "y"] },
|
||||
"DD-MM-YYYY": { regex: new RegExp(/^(\d{1,2})-(\d{1,2})-(\d{4})/.source + TIME_SUFFIX), order: ["d", "m", "y"] },
|
||||
};
|
||||
|
||||
/** Parses `raw` per `format`, returning an ISO "YYYY-MM-DD" string, or null
|
||||
|
||||
@@ -264,6 +264,12 @@ const ru = {
|
||||
"transactions.import.duplicateCheckFailed": "Не удалось проверить, есть ли уже такие транзакции на счёте — при импорте возможны дубли.",
|
||||
"transactions.import.duplicatesFound": "Найдено похожих на уже существующие: {{count}}. По умолчанию они не импортируются.",
|
||||
"transactions.import.includeDuplicates": "Всё равно импортировать их",
|
||||
"transactions.import.presetLabel": "Банк / формат файла",
|
||||
"transactions.import.preset.auto": "Определить автоматически",
|
||||
"transactions.import.presetHint": "Готовые профили: Т-Банк, monobank, ПриватБанк — колонки и форматы подставятся сами. Выгрузки других банков импортируются через ручное сопоставление колонок.",
|
||||
"transactions.import.presetDetected": "Профиль банка: {{bank}}. Колонки и форматы подставлены автоматически — проверьте и при необходимости поправьте.",
|
||||
"transactions.import.presetNotDetected": "Формат банка не распознан — сопоставьте колонки вручную или выберите банк из списка.",
|
||||
"transactions.import.errorRowFiltered": "операция не проведена (статус «{{value}}»)",
|
||||
"transactions.allTypes": "Все типы",
|
||||
"transactions.expense": "Расходы",
|
||||
"transactions.income": "Доходы",
|
||||
@@ -783,6 +789,12 @@ const en: Record<keyof typeof ru, string> = {
|
||||
"transactions.import.duplicateCheckFailed": "Couldn't check what's already on this account — importing may create duplicates.",
|
||||
"transactions.import.duplicatesFound": "Found {{count}} that look like duplicates of existing transactions. They're left out by default.",
|
||||
"transactions.import.includeDuplicates": "Import them anyway",
|
||||
"transactions.import.presetLabel": "Bank / file format",
|
||||
"transactions.import.preset.auto": "Detect automatically",
|
||||
"transactions.import.presetHint": "Built-in profiles: T-Bank, monobank, PrivatBank — columns and formats are pre-filled. Exports from other banks go through manual column mapping.",
|
||||
"transactions.import.presetDetected": "Bank profile: {{bank}}. Columns and formats were pre-filled — review and adjust if needed.",
|
||||
"transactions.import.presetNotDetected": "Bank format not recognised — map the columns by hand or pick a bank from the list.",
|
||||
"transactions.import.errorRowFiltered": "operation was not completed (status “{{value}}”)",
|
||||
"transactions.allTypes": "All types",
|
||||
"transactions.expense": "Expenses",
|
||||
"transactions.income": "Income",
|
||||
|
||||
@@ -8,6 +8,15 @@ import { useAccounts } from "@/hooks/useAccounts";
|
||||
import { useCategories } from "@/hooks/useCategories";
|
||||
import { useBulkCreateTransactions, useTransactionsForDuplicateCheck } from "@/hooks/useTransactions";
|
||||
import { translateCategoryName } from "@/lib/categoryLabels";
|
||||
import {
|
||||
BANK_PRESETS,
|
||||
applyPresetMapping,
|
||||
detectPreset,
|
||||
findPreset,
|
||||
rowFilterRejection,
|
||||
type BankPreset,
|
||||
type ImportMapping,
|
||||
} from "@/lib/bankPresets";
|
||||
import {
|
||||
AMOUNT_FORMATS,
|
||||
DATE_FORMATS,
|
||||
@@ -35,14 +44,12 @@ const NONE = "";
|
||||
const ENCODINGS = ["utf-8", "windows-1251", "windows-1252", "koi8-r"] as const;
|
||||
type Encoding = (typeof ENCODINGS)[number];
|
||||
|
||||
interface Mapping {
|
||||
date: string;
|
||||
amount: string;
|
||||
description: string;
|
||||
merchant: string;
|
||||
notes: string;
|
||||
category: string;
|
||||
}
|
||||
// Header names, not indexes — see lib/bankPresets.ts's ImportMapping.
|
||||
type Mapping = ImportMapping;
|
||||
|
||||
// "auto" lets detectPreset() pick a bank profile from the header row; a
|
||||
// preset id pins one regardless of what the headers look like.
|
||||
const PRESET_AUTO = "auto";
|
||||
|
||||
interface SkippedRow {
|
||||
row: number;
|
||||
@@ -72,6 +79,26 @@ function guessMapping(headerRow: string[]): Mapping {
|
||||
};
|
||||
}
|
||||
|
||||
/** The bank profile in force for a given header row: the pinned one when
|
||||
* the user picked a bank, otherwise whatever the headers match (or null —
|
||||
* plain generic mapping). Pure, so both the file handler and the
|
||||
* header-change effect below resolve it the same way. */
|
||||
function resolvePreset(headers: string[], presetChoice: string): BankPreset | null {
|
||||
return presetChoice === PRESET_AUTO ? detectPreset(headers) : findPreset(presetChoice);
|
||||
}
|
||||
|
||||
/** Map-step defaults for a header row: the preset's answers where it has
|
||||
* them, the generic header guesses everywhere else. */
|
||||
function resolveDefaults(headers: string[], preset: BankPreset | null): { mapping: Mapping; dateFormat: DateFormat; amountFormat: AmountFormat } {
|
||||
const guessed = guessMapping(headers);
|
||||
if (!preset) return { mapping: guessed, dateFormat: "YYYY-MM-DD", amountFormat: "auto" };
|
||||
return {
|
||||
mapping: applyPresetMapping(preset, headers, guessed),
|
||||
dateFormat: preset.dateFormat,
|
||||
amountFormat: preset.amountFormat,
|
||||
};
|
||||
}
|
||||
|
||||
export function CsvImportPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -85,6 +112,7 @@ export function CsvImportPage() {
|
||||
const [fileName, setFileName] = useState("");
|
||||
const [fileBuffer, setFileBuffer] = useState<ArrayBuffer | null>(null);
|
||||
const [encoding, setEncoding] = useState<Encoding>("utf-8");
|
||||
const [presetChoice, setPresetChoice] = useState<string>(PRESET_AUTO);
|
||||
const [mapping, setMapping] = useState<Mapping>({ date: "", amount: "", description: "", merchant: "", notes: "", category: "" });
|
||||
const [dateFormat, setDateFormat] = useState<DateFormat>("YYYY-MM-DD");
|
||||
const [amountFormat, setAmountFormat] = useState<AmountFormat>("auto");
|
||||
@@ -105,7 +133,10 @@ export function CsvImportPage() {
|
||||
return { headers: headerRow, dataRows: rest };
|
||||
}, [fileBuffer, encoding]);
|
||||
|
||||
// Owns mapping initialisation, keyed on the header row's *contents*.
|
||||
const activePreset = useMemo(() => resolvePreset(headers, presetChoice), [headers, presetChoice]);
|
||||
|
||||
// Owns mapping initialisation, keyed on the header row's *contents* and
|
||||
// the chosen bank profile.
|
||||
// Changing the encoding re-decodes that row, and a column picked under the
|
||||
// previous encoding then names a header that no longer exists:
|
||||
// headers.indexOf() returns -1, the dropdown renders blank while
|
||||
@@ -114,15 +145,25 @@ export function CsvImportPage() {
|
||||
// reading of a mapping whose columns are all gone. When the decoded
|
||||
// headers come out identical (an all-ASCII header row survives every
|
||||
// encoding here), the key doesn't change, this doesn't run, and manual
|
||||
// column picks stay put.
|
||||
// column picks stay put. Switching the bank profile re-runs it on
|
||||
// purpose: the whole point of picking a bank is to replace the mapping.
|
||||
// JSON.stringify, not join(): joining on a separator would read
|
||||
// ["ab","c"] and ["a","bc"] as one and the same header row.
|
||||
const headersKey = JSON.stringify(headers);
|
||||
useEffect(() => {
|
||||
if (headers.length === 0) return;
|
||||
setMapping(guessMapping(headers));
|
||||
const preset = resolvePreset(headers, presetChoice);
|
||||
const defaults = resolveDefaults(headers, preset);
|
||||
setMapping(defaults.mapping);
|
||||
// Formats are only overwritten when a bank profile dictates them; with
|
||||
// no profile, a date format the user picked by hand survives an
|
||||
// encoding switch (the fresh-file reset lives in handleFileSelected).
|
||||
if (preset) {
|
||||
setDateFormat(defaults.dateFormat);
|
||||
setAmountFormat(defaults.amountFormat);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [headersKey]);
|
||||
}, [headersKey, presetChoice]);
|
||||
|
||||
const categoryLookup = useMemo(() => {
|
||||
const map: Record<"income" | "expense", Map<string, number>> = { income: new Map(), expense: new Map() };
|
||||
@@ -206,6 +247,15 @@ export function CsvImportPage() {
|
||||
const rawNotes = notesIdx >= 0 ? (cells[notesIdx] ?? "").trim() : "";
|
||||
const rawCategory = categoryIdx >= 0 ? (cells[categoryIdx] ?? "").trim() : "";
|
||||
|
||||
// Bank-profile row filter first (e.g. T-Bank's "Статус" = FAILED):
|
||||
// a declined payment parses perfectly well as a date + amount, and
|
||||
// nothing downstream could tell it apart from a real expense.
|
||||
const rejectedStatus = rowFilterRejection(activePreset, headers, cells);
|
||||
if (rejectedStatus !== null) {
|
||||
skippedRows.push({ row: rowNumber, reason: t("transactions.import.errorRowFiltered", { value: rejectedStatus || "—" }) });
|
||||
return;
|
||||
}
|
||||
|
||||
const isoDate = parseDateWithFormat(rawDate, dateFormat);
|
||||
if (!isoDate) {
|
||||
skippedRows.push({ row: rowNumber, reason: t("transactions.import.errorBadDate", { value: rawDate || "—" }) });
|
||||
@@ -251,7 +301,7 @@ export function CsvImportPage() {
|
||||
duplicateCount: duplicateRows.length,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [step, headers, dataRows, mapping, dateFormat, amountFormat, categoryLookup, accountId, existingKeys, includeDuplicates, t]);
|
||||
}, [step, headers, dataRows, mapping, dateFormat, amountFormat, categoryLookup, accountId, existingKeys, includeDuplicates, activePreset, t]);
|
||||
|
||||
async function handleFileSelected(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = event.target.files?.[0];
|
||||
@@ -277,11 +327,16 @@ export function CsvImportPage() {
|
||||
return;
|
||||
}
|
||||
const [headerRow] = rows;
|
||||
// Full reset for a fresh file — the header-change effect above skips
|
||||
// files whose header row is byte-identical to the previous one, so
|
||||
// formats from the last import can't be left over here.
|
||||
const defaults = resolveDefaults(headerRow, resolvePreset(headerRow, presetChoice));
|
||||
setFileName(file.name);
|
||||
setFileBuffer(buffer);
|
||||
setEncoding(detectedEncoding);
|
||||
setMapping(guessMapping(headerRow));
|
||||
setAmountFormat("auto");
|
||||
setMapping(defaults.mapping);
|
||||
setDateFormat(defaults.dateFormat);
|
||||
setAmountFormat(defaults.amountFormat);
|
||||
setIncludeDuplicates(false);
|
||||
setStep("map");
|
||||
}
|
||||
@@ -307,6 +362,23 @@ export function CsvImportPage() {
|
||||
|
||||
const mappingComplete = Boolean(mapping.date && mapping.amount && mapping.description);
|
||||
|
||||
// Same control on the upload and map steps: picking the bank up front is
|
||||
// the "I know what this file is" path, changing it on the map step is the
|
||||
// "auto-detect guessed wrong" path.
|
||||
const presetSelect = (id: string) => (
|
||||
<div>
|
||||
<Label htmlFor={id}>{t("transactions.import.presetLabel")}</Label>
|
||||
<Select id={id} value={presetChoice} onChange={(event) => setPresetChoice(event.target.value)} className="sm:w-72">
|
||||
<option value={PRESET_AUTO}>{t("transactions.import.preset.auto")}</option>
|
||||
{BANK_PRESETS.map((preset) => (
|
||||
<option key={preset.id} value={preset.id}>
|
||||
{preset.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -346,6 +418,8 @@ export function CsvImportPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm text-text-secondary">{t("transactions.import.uploadHint")}</p>
|
||||
{presetSelect("upload-preset")}
|
||||
<p className="text-xs text-text-muted">{t("transactions.import.presetHint")}</p>
|
||||
<Button onClick={() => fileInputRef.current?.click()} disabled={!accountId}>
|
||||
<Upload size={16} />
|
||||
{t("transactions.import.chooseFile")}
|
||||
@@ -364,6 +438,13 @@ export function CsvImportPage() {
|
||||
<span className="text-xs text-text-muted">{fileName}</span>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{presetSelect("map-preset")}
|
||||
<p className={`text-xs ${activePreset ? "text-success" : "text-text-muted"}`}>
|
||||
{activePreset
|
||||
? t("transactions.import.presetDetected", { bank: activePreset.label })
|
||||
: t("transactions.import.presetNotDetected")}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="map-encoding">{t("transactions.import.encodingLabel")}</Label>
|
||||
<Select id="map-encoding" value={encoding} onChange={(event) => setEncoding(event.target.value as Encoding)} className="sm:w-56">
|
||||
|
||||
Reference in New Issue
Block a user