commit 66f6fa138dad420986ea6c963ac16ec3a36555a7 Author: Nicolaj Hartmann Date: Sun Jan 18 02:52:30 2026 +0100 Initial commit: Books accounting system with EventFlow CQRS Backend (.NET 10): - EventFlow CQRS/Event Sourcing with PostgreSQL - GraphQL.NET API with mutations and queries - Custom ReadModelSqlGenerator for snake_case PostgreSQL columns - Hangfire for background job processing - Integration tests with isolated test databases Frontend (React/Vite): - Initial project structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..173764d --- /dev/null +++ b/.gitignore @@ -0,0 +1,204 @@ +# ============================================ +# .NET / C# +# ============================================ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ +*.user +*.rsuser +*.suo +*.userosscache +*.sln.docstates +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.idb +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.vspscc +*.vssscc +.builds +*.DotSettings.user + +# NuGet +*.nupkg +*.snupkg +**/[Pp]ackages/* +!**/[Pp]ackages/build/ +.nuget/ +project.lock.json +project.fragment.lock.json +artifacts/ + +# .NET Core +*.PublishPersistentProperties.json + +# ============================================ +# Node.js / Frontend +# ============================================ +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.pnpm-store/ +.npm/ +.yarn/ +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite +.vite/ +dist/ +dist-ssr/ +*.local + +# Build outputs +build/ +.next/ +.nuxt/ +.output/ +.cache/ + +# ============================================ +# Docker +# ============================================ +**/docker-compose.override.yml +**/.docker/ + +# ============================================ +# Kubernetes +# ============================================ +*.kubeconfig +kubeconfig +**/charts/*.tgz +**/*.decrypted.yaml +**/secrets.yaml +**/secrets.yml +!**/secrets.example.yaml + +# Helm +**/Chart.lock + +# ============================================ +# IDEs and Editors +# ============================================ + +# JetBrains Rider / IntelliJ +.idea/ +*.sln.iml + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets +.history/ +*.vsix + +# Visual Studio +.vs/ +*.VC.db +*.VC.VC.opendb + +# ============================================ +# OS Generated +# ============================================ + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk + +# Linux +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +# ============================================ +# Environment & Secrets +# ============================================ +.env +.env.* +!.env.example +!.env.*.example +*.pem +*.key +*.p12 +*.pfx +appsettings.Development.json +appsettings.Local.json +secrets.json + +# ============================================ +# Testing +# ============================================ +coverage/ +*.lcov +.nyc_output/ +TestResults/ +*.trx + +# ============================================ +# Misc +# ============================================ +*.log +*.tmp +*.temp +*.swp +*.swo +*.bak +*.backup +.sass-cache/ +*.css.map +*.js.map diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa9b278 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Books - Dansk Bogføringssystem + +Et komplet bogføringssystem til danske virksomheder med support for regnskabsår, momsindberetning og SKAT-compliance. + +## Projektstruktur + +``` +books/ +├── frontend/ # React/TypeScript frontend +│ ├── src/ +│ │ ├── components/ +│ │ ├── pages/ +│ │ ├── stores/ +│ │ ├── hooks/ +│ │ ├── lib/ +│ │ └── types/ +│ ├── package.json +│ └── vite.config.ts +│ +├── backend/ # .NET 8 Web API +│ ├── Books.Api/ +│ │ ├── Controllers/ +│ │ ├── Models/ +│ │ ├── Services/ +│ │ └── Program.cs +│ ├── Books.slnx +│ └── BACKEND_REQUIREMENTS.md +│ +└── README.md +``` + +## Teknologi Stack + +### Frontend +- React 18 med TypeScript +- Ant Design komponenter +- Zustand state management +- Vite build tool +- Day.js til dato-håndtering + +### Backend +- .NET 10 Web API +- C# 14 +- Entity Framework Core +- PostgreSQL database +- JWT authentication +- SLNX solution format + +## Kom i gang + +### Frontend +```bash +cd frontend +npm install +npm run dev +``` + +### Backend +```bash +cd backend +dotnet restore +dotnet run --project Books.Api +``` + +## Features + +- [x] Regnskabsår (Fiscal Years) - opret, luk, lås +- [x] Regnskabsperioder - månedlig, kvartalsvis, halvårlig, årlig +- [x] Kontoplan med danske standardkonti +- [x] Hurtig bogføring interface +- [ ] Årsafslutning med lukkeposter +- [ ] Dynamiske åbningsbalancer +- [ ] Momsindberetning +- [ ] SKAT integration + +## Licens + +Proprietary - Alle rettigheder forbeholdes. diff --git a/account-suggestions/accounts-with-suggestions.json b/account-suggestions/accounts-with-suggestions.json new file mode 100644 index 0000000..3cc9023 --- /dev/null +++ b/account-suggestions/accounts-with-suggestions.json @@ -0,0 +1,1602 @@ +[ + { + "id": 121791149, + "number": 2000, + "name": "Vareforbrug", + "categoryName": "Variable omkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "varer", + "indkøb", + "lager", + "goods", + "inventory", + "purchase", + "varekøb" + ], + "regionVariants": { + "category": "varekøb", + "EU": 2050, + "WORLD": 2150 + } + }, + { + "id": 121791197, + "number": 2050, + "name": "EU-erhvervelser varer (rubrik A-varer)", + "categoryName": "Variable omkostninger", + "defaultVatCode": "IEUV", + "region": "EU", + "suggestions": [ + "eu", + "europa", + "germany", + "france", + "italien", + "spain", + "netherlands", + "eu-erhvervelse" + ], + "vatRubric": "EU_BOX_A_GOODS", + "regionVariants": { + "category": "varekøb", + "DK": 2000, + "WORLD": 2150 + } + }, + { + "id": 121791199, + "number": 2100, + "name": "EU-erhvervelser ydelser (rubrik A-ydelser)", + "categoryName": "Variable omkostninger", + "defaultVatCode": "IEUY", + "region": "EU", + "suggestions": [ + "eu", + "europa", + "ydelse", + "service", + "eu-ydelse" + ], + "vatRubric": "EU_BOX_A_SERVICES", + "regionVariants": { + "category": "ydelseskøb", + "WORLD": 2200 + } + }, + { + "id": 121791200, + "number": 2150, + "name": "Varekøb verden", + "categoryName": "Variable omkostninger", + "defaultVatCode": "IVV", + "region": "WORLD", + "suggestions": [ + "usa", + "uk", + "kina", + "china", + "verden", + "import", + "world" + ], + "regionVariants": { + "category": "varekøb", + "DK": 2000, + "EU": 2050 + } + }, + { + "id": 121791201, + "number": 2200, + "name": "Ydelseskøb verden", + "categoryName": "Variable omkostninger", + "defaultVatCode": "IVY", + "region": "WORLD", + "suggestions": [ + "usa", + "uk", + "kina", + "verden", + "service", + "ydelse", + "world" + ], + "regionVariants": { + "category": "ydelseskøb", + "EU": 2100 + } + }, + { + "id": 121791150, + "number": 2250, + "name": "Fragt med moms", + "categoryName": "Variable omkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "fragt", + "levering", + "shipping", + "delivery", + "postnord", + "gls", + "dao", + "bring", + "ups", + "dhl" + ], + "regionVariants": { + "category": "fragt", + "DK_VATFREE": 2300, + "EU": 2350 + } + }, + { + "id": 121790982, + "number": 2300, + "name": "Fragt uden moms", + "categoryName": "Variable omkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "fragt", + "shipping", + "delivery", + "momsfri" + ], + "regionVariants": { + "category": "fragt", + "DK": 2250, + "EU": 2350 + } + }, + { + "id": 121791198, + "number": 2350, + "name": "Fragt - EU", + "categoryName": "Variable omkostninger", + "defaultVatCode": "IEUV", + "region": "EU", + "suggestions": [ + "fragt", + "eu", + "shipping", + "dhl", + "ups", + "fedex" + ], + "vatRubric": "EU_BOX_A_GOODS", + "regionVariants": { + "category": "fragt", + "DK": 2250, + "DK_VATFREE": 2300 + } + }, + { + "id": 121790983, + "number": 2400, + "name": "Valutakursdifferencer, import", + "categoryName": "Variable omkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "valuta", + "kurs", + "currency", + "exchange" + ] + }, + { + "id": 121790984, + "number": 2450, + "name": "Varelagerregulering", + "categoryName": "Variable omkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "lager", + "regulering", + "inventory" + ] + }, + { + "id": 121791151, + "number": 2800, + "name": "Fremmed arbejde", + "categoryName": "Variable omkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "underleverandør", + "freelance", + "contractor", + "subcontractor", + "fremmed arbejde" + ] + }, + { + "id": 121790989, + "number": 3000, + "name": "AM-indkomst", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "løn", + "salary", + "wage", + "payroll", + "am-indkomst" + ] + }, + { + "id": 121790990, + "number": 3020, + "name": "Arbejdsgiver ATP", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "atp", + "arbejdsgiver" + ] + }, + { + "id": 121790991, + "number": 3040, + "name": "Medarbejder ATP", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "atp", + "medarbejder" + ] + }, + { + "id": 121790992, + "number": 3060, + "name": "Sygepenge mv.", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "sygepenge", + "sickness", + "sygedag" + ] + }, + { + "id": 121790993, + "number": 3070, + "name": "Personalegoder, herunder fri telefon", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "personalegode", + "fri telefon", + "benefit", + "goder" + ] + }, + { + "id": 121790994, + "number": 3080, + "name": "B-honorar", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "b-honorar", + "honorar", + "fee" + ] + }, + { + "id": 121790995, + "number": 3090, + "name": "Barsel", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "barsel", + "maternity", + "fødsel" + ] + }, + { + "id": 121790996, + "number": 3100, + "name": "Feriepenge og SH", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "ferie", + "feriepenge", + "sh", + "vacation", + "holiday" + ] + }, + { + "id": 121790997, + "number": 3120, + "name": "Pension", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "pension", + "pensionsbidrag", + "retirement" + ] + }, + { + "id": 121790998, + "number": 3140, + "name": "Diæter/rejsegodtgørelse", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "diæt", + "rejsegodtgørelse", + "per diem", + "allowance" + ] + }, + { + "id": 121790999, + "number": 3160, + "name": "Kørsel i egen bil (kilometergodtgørelse)", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "kørsel", + "kilometer", + "km", + "mileage", + "km-godtgørelse", + "bil" + ] + }, + { + "id": 121791000, + "number": 3180, + "name": "AER/AES/ATP-finansieringsbidrag", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "aer", + "aes", + "atp", + "finansieringsbidrag" + ] + }, + { + "id": 121791152, + "number": 3200, + "name": "Arbejdstøj", + "categoryName": "Lønomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "arbejdstøj", + "uniform", + "workwear", + "tøj" + ] + }, + { + "id": 121791153, + "number": 3240, + "name": "Mad under kursus/møder mv., fuldt fradrag", + "categoryName": "Lønomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "mad", + "kursus", + "møde", + "food", + "catering", + "frokost" + ] + }, + { + "id": 121791002, + "number": 3260, + "name": "Gaver til personalet, fuldt fradrag", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "gave", + "personale", + "gift", + "employee" + ] + }, + { + "id": 121791154, + "number": 3280, + "name": "Uddannelsesudgifter", + "categoryName": "Lønomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "kursus", + "uddannelse", + "course", + "training", + "efteruddannelse" + ] + }, + { + "id": 121791155, + "number": 3300, + "name": "Diverse vedr. ansatte med moms", + "categoryName": "Lønomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "diverse", + "personale", + "ansatte", + "moms" + ] + }, + { + "id": 121791003, + "number": 3320, + "name": "Diverse vedr. ansatte uden moms", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "diverse", + "personale", + "ansatte", + "momsfri" + ] + }, + { + "id": 121791004, + "number": 3340, + "name": "Regulering feriepenge", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "ferie", + "regulering", + "feriepenge" + ] + }, + { + "id": 121791005, + "number": 3355, + "name": "Frokostordning til ansatte", + "categoryName": "Lønomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "frokost", + "frokostordning", + "lunch", + "kantine" + ] + }, + { + "id": 121791156, + "number": 4000, + "name": "Annoncer og reklame", + "categoryName": "Salgsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "annonce", + "reklame", + "marketing", + "ads", + "google", + "facebook", + "linkedin", + "meta", + "advertising" + ] + }, + { + "id": 121791157, + "number": 4020, + "name": "Udsmykning i forbindelse med arrangementer/events", + "categoryName": "Salgsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "udsmykning", + "event", + "arrangement", + "decoration" + ] + }, + { + "id": 121791158, + "number": 4040, + "name": "Hotel, personale, fuldt fradrag", + "categoryName": "Salgsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "hotel", + "overnatning", + "accommodation", + "booking.com", + "hotels.com", + "personale" + ] + }, + { + "id": 121791159, + "number": 4060, + "name": "Hotel, forretningsforbindelser, delvis fradrag", + "categoryName": "Salgsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "hotel", + "overnatning", + "kunde", + "client", + "forretningsforbindelse" + ] + }, + { + "id": 121791008, + "number": 4080, + "name": "Konferencer", + "categoryName": "Salgsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "konference", + "conference", + "seminar", + "event" + ] + }, + { + "id": 121791160, + "number": 4100, + "name": "Messer", + "categoryName": "Salgsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "messe", + "fair", + "exhibition", + "udstilling" + ] + }, + { + "id": 121791202, + "number": 4120, + "name": "Repræsentation, restaurant, personale, fuldt fradrag", + "categoryName": "Salgsomkostninger", + "defaultVatCode": "REP", + "region": "DK", + "suggestions": [ + "restaurant", + "frokost", + "middag", + "lunch", + "dinner", + "café", + "personale" + ] + }, + { + "id": 121791203, + "number": 4140, + "name": "Repræsentation, restaurant, forretningsforbindelser, delvis fradrag", + "categoryName": "Salgsomkostninger", + "defaultVatCode": "REP", + "region": "DK", + "suggestions": [ + "restaurant", + "repræsentation", + "kunde", + "client", + "forretningsforbindelse" + ] + }, + { + "id": 121791161, + "number": 4160, + "name": "Mad i virksomheden til forretningsforbindelser, delvis fradrag", + "categoryName": "Salgsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "mad", + "virksomhed", + "forretningsforbindelse", + "catering" + ] + }, + { + "id": 121791009, + "number": 4180, + "name": "Repræsentation, gaver og blomster, delvis fradrag", + "categoryName": "Salgsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "gave", + "blomster", + "gift", + "flowers", + "repræsentation" + ] + }, + { + "id": 121791162, + "number": 4200, + "name": "Anden fradragsberettiget repræsentation med moms", + "categoryName": "Salgsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "repræsentation", + "representation" + ] + }, + { + "id": 121791010, + "number": 4220, + "name": "Repræsentation, diverse", + "categoryName": "Salgsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "repræsentation", + "diverse" + ] + }, + { + "id": 121791163, + "number": 4240, + "name": "Øvrige personaleomkostninger", + "categoryName": "Salgsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "personale", + "personnel", + "staff" + ] + }, + { + "id": 121791011, + "number": 4260, + "name": "Ej fradragsberettiget andel", + "categoryName": "Salgsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "ikke-fradrag", + "non-deductible" + ] + }, + { + "id": 121791012, + "number": 4280, + "name": "Rejseomkostninger", + "categoryName": "Salgsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "rejse", + "travel", + "tur", + "trip" + ] + }, + { + "id": 121791164, + "number": 5000, + "name": "Husleje", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "husleje", + "leje", + "rent", + "kontor", + "lokale" + ], + "regionVariants": { + "category": "husleje", + "DK_VATFREE": 5010 + } + }, + { + "id": 121791015, + "number": 5010, + "name": "Husleje uden moms", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "husleje", + "leje", + "rent", + "momsfri" + ], + "regionVariants": { + "category": "husleje", + "DK": 5000 + } + }, + { + "id": 121791165, + "number": 5025, + "name": "El", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "el", + "strøm", + "electricity", + "power", + "ørsted", + "norlys", + "ewii", + "andel" + ] + }, + { + "id": 121791166, + "number": 5030, + "name": "Vand", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "vand", + "water", + "vandværk" + ] + }, + { + "id": 121791167, + "number": 5035, + "name": "Varme", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "varme", + "gas", + "fjernvarme", + "heating", + "naturgas" + ] + }, + { + "id": 121791016, + "number": 5040, + "name": "Elafgift", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "elafgift", + "afgift", + "tax" + ] + }, + { + "id": 124507843, + "number": 5045, + "name": "Naturgas- og bygasafgift", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "gas", + "afgift", + "naturgas" + ] + }, + { + "id": 124519865, + "number": 5050, + "name": "Vandafgift", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "vand", + "afgift", + "vandafgift" + ] + }, + { + "id": 121791168, + "number": 5060, + "name": "Rengøring og affaldshåndtering", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "rengøring", + "cleaning", + "affald", + "waste", + "renovation" + ] + }, + { + "id": 121791169, + "number": 5080, + "name": "Reparation og vedligeholdelse", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "reparation", + "vedligeholdelse", + "maintenance", + "repair" + ] + }, + { + "id": 121791017, + "number": 5100, + "name": "Ejendomsskat", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "ejendomsskat", + "property tax", + "skat" + ] + }, + { + "id": 121791170, + "number": 5140, + "name": "Mødelokaler", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "mødelokale", + "meeting room", + "konference" + ] + }, + { + "id": 121791171, + "number": 5160, + "name": "Dekoration", + "categoryName": "Lokaleomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "dekoration", + "decoration", + "indretning" + ] + }, + { + "id": 121791172, + "number": 6000, + "name": "Billeje (gulplade)", + "categoryName": "Kørsel og rejser", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "billeje", + "car rental", + "leje", + "hertz", + "avis", + "europcar", + "sixt", + "enterprise" + ] + }, + { + "id": 121791173, + "number": 6020, + "name": "Brændstof (gulplade)", + "categoryName": "Kørsel og rejser", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "benzin", + "diesel", + "fuel", + "brændstof", + "ok", + "shell", + "q8", + "circle k", + "ingo" + ] + }, + { + "id": 121791174, + "number": 6040, + "name": "Vedligeholdelse af bil (gulplade)", + "categoryName": "Kørsel og rejser", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "bil", + "vedligeholdelse", + "car", + "service", + "værksted", + "dæk" + ] + }, + { + "id": 121791021, + "number": 6060, + "name": "Vægtafgift og forsikringer", + "categoryName": "Kørsel og rejser", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "vægtafgift", + "forsikring", + "car insurance", + "bilforsikring" + ] + }, + { + "id": 121791175, + "number": 6080, + "name": "Parkering (gulplade)", + "categoryName": "Kørsel og rejser", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "parkering", + "parking", + "p-afgift", + "easypark", + "parkman", + "apcoa" + ], + "regionVariants": { + "category": "parkering", + "DK_VATFREE": 6085 + } + }, + { + "id": 121791022, + "number": 6085, + "name": "Parkering uden moms", + "categoryName": "Kørsel og rejser", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "parkering", + "parking", + "momsfri" + ], + "regionVariants": { + "category": "parkering", + "DK": 6080 + } + }, + { + "id": 121791176, + "number": 6100, + "name": "Broafgift", + "categoryName": "Kørsel og rejser", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "bro", + "bridge", + "storebælt", + "øresund", + "toll", + "afgift" + ] + }, + { + "id": 121791023, + "number": 6120, + "name": "Taxa", + "categoryName": "Kørsel og rejser", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "taxa", + "taxi", + "uber", + "bolt", + "dantaxi", + "viggo", + "4x27" + ] + }, + { + "id": 121791024, + "number": 6140, + "name": "Tog", + "categoryName": "Kørsel og rejser", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "tog", + "dsb", + "train", + "rejsekort", + "billet" + ] + }, + { + "id": 121791025, + "number": 6160, + "name": "Fly", + "categoryName": "Kørsel og rejser", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "fly", + "flight", + "sas", + "norwegian", + "ryanair", + "lufthansa", + "flybillet" + ] + }, + { + "id": 121791026, + "number": 6180, + "name": "Bus", + "categoryName": "Kørsel og rejser", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "bus", + "movia", + "arriva", + "flixbus" + ] + }, + { + "id": 121791177, + "number": 6200, + "name": "Færge", + "categoryName": "Kørsel og rejser", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "færge", + "ferry", + "molslinjen", + "scandlines", + "bornholmslinjen" + ] + }, + { + "id": 121791027, + "number": 6400, + "name": "Diverse transportomkostninger uden moms", + "categoryName": "Kørsel og rejser", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "transport", + "diverse", + "transportation" + ] + }, + { + "id": 121791178, + "number": 7005, + "name": "Revision og regnskabsmæssig assistance", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "revisor", + "revision", + "audit", + "regnskab", + "pwc", + "deloitte", + "ey", + "kpmg", + "bdo" + ] + }, + { + "id": 121791179, + "number": 7010, + "name": "Advokat", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "advokat", + "lawyer", + "legal", + "juridisk", + "attorney" + ] + }, + { + "id": 121791180, + "number": 7020, + "name": "Bogføringsassistance", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "bogføring", + "bookkeeping", + "regnskab", + "bogholder" + ] + }, + { + "id": 121791181, + "number": 7040, + "name": "Konsulentbistand", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "konsulent", + "consultant", + "rådgivning", + "advisory", + "rådgiver" + ] + }, + { + "id": 121791182, + "number": 7060, + "name": "Kontingenter inkl. moms", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "kontingent", + "medlemskab", + "membership", + "forening" + ] + }, + { + "id": 121791030, + "number": 7080, + "name": "Kontingenter ekskl. moms", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "kontingent", + "medlemskab", + "momsfri" + ] + }, + { + "id": 121791031, + "number": 7100, + "name": "Aviser", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "avis", + "newspaper", + "berlingske", + "politiken", + "jyllandsposten", + "børsen", + "dagblad" + ] + }, + { + "id": 121791183, + "number": 7120, + "name": "Faglitteratur", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "bog", + "book", + "litteratur", + "amazon", + "saxo", + "tales", + "faglitteratur" + ] + }, + { + "id": 121791032, + "number": 7160, + "name": "Erhvervsforsikringer", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "forsikring", + "insurance", + "tryg", + "topdanmark", + "if", + "codan", + "gjensidige", + "alm brand" + ] + }, + { + "id": 121791185, + "number": 7180, + "name": "Fragt og kørsel", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "fragt", + "kørsel", + "shipping", + "transport" + ] + }, + { + "id": 121791186, + "number": 7200, + "name": "Kontorartikler og tryksager", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "kontor", + "office", + "papir", + "printer", + "lyreco", + "staples", + "kontorartikler" + ] + }, + { + "id": 121791033, + "number": 7220, + "name": "Porto og gebyrer", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "porto", + "gebyr", + "postage", + "fee", + "postnord" + ] + }, + { + "id": 121791187, + "number": 7240, + "name": "Telefoni", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "telefon", + "mobil", + "phone", + "mobile", + "telia", + "telenor", + "3", + "telmore", + "cbs", + "lebara" + ] + }, + { + "id": 121791034, + "number": 7260, + "name": "Beskatning af fri telefoni", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "telefon", + "beskatning", + "fri telefon" + ] + }, + { + "id": 121791189, + "number": 7300, + "name": "Internet og webhotel", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "internet", + "hosting", + "domain", + "web", + "simply", + "one.com", + "cloudflare", + "aws", + "azure" + ] + }, + { + "id": 121791190, + "number": 7320, + "name": "Køb af software", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "software", + "app", + "abonnement", + "subscription", + "saas", + "adobe", + "microsoft", + "slack", + "notion", + "google workspace", + "dropbox" + ] + }, + { + "id": 121791035, + "number": 7360, + "name": "Offentlige bøder og gebyrer", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "bøde", + "gebyr", + "fine", + "offentlig" + ] + }, + { + "id": 121791036, + "number": 7380, + "name": "Registrerede kassedifferencer", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "kasse", + "difference", + "cash" + ] + }, + { + "id": 121791191, + "number": 7400, + "name": "Betalingsløsning", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "betaling", + "payment", + "stripe", + "mobilepay", + "nets", + "clearhaus", + "paypal" + ] + }, + { + "id": 121791037, + "number": 7420, + "name": "Indløsere", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "indløser", + "acquirer", + "nets", + "clearhaus" + ] + }, + { + "id": 121791038, + "number": 7440, + "name": "Licens", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "licens", + "license", + "rettighed" + ] + }, + { + "id": 121791192, + "number": 7460, + "name": "Diverse inkl. moms", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "diverse", + "moms", + "miscellaneous" + ] + }, + { + "id": 121791039, + "number": 7480, + "name": "Diverse ekskl. moms", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "diverse", + "momsfri", + "miscellaneous" + ] + }, + { + "id": 121791204, + "number": 7500, + "name": "Generalforsamling, bestyrelsesmøder ude i byen, fuld fradrag", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "REP", + "region": "DK", + "suggestions": [ + "generalforsamling", + "bestyrelse", + "møde", + "board", + "meeting" + ] + }, + { + "id": 121791040, + "number": 7520, + "name": "Generalforsamling i virksomhedens lokaler", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "generalforsamling", + "møde", + "lokale" + ] + }, + { + "id": 121791193, + "number": 7540, + "name": "Bestyrelsesmøder i virksomhedens lokaler", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "bestyrelse", + "board", + "møde", + "lokale" + ] + }, + { + "id": 121791205, + "number": 7560, + "name": "Bestyrelsesmøder ude i byen", + "categoryName": "Administrationsomkostninger", + "defaultVatCode": "REP", + "region": "DK", + "suggestions": [ + "bestyrelse", + "board", + "møde", + "restaurant" + ] + }, + { + "id": 121791194, + "number": 8040, + "name": "Småanskaffelser (straksafskrivning)", + "categoryName": "Afskrivninger", + "defaultVatCode": "I25", + "region": "DK", + "suggestions": [ + "computer", + "laptop", + "møbler", + "furniture", + "udstyr", + "equipment", + "apple", + "dell", + "lenovo", + "småanskaffelse" + ] + }, + { + "id": 121791206, + "number": 8050, + "name": "Småanskaffelser med omvendt betalingspligt", + "categoryName": "Afskrivninger", + "defaultVatCode": "OBPK", + "region": "DK", + "suggestions": [ + "småanskaffelse", + "omvendt betalingspligt", + "reverse charge" + ] + }, + { + "id": 121791053, + "number": 9200, + "name": "Bankrenter", + "categoryName": "Renteudgifter", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "rente", + "bank", + "interest", + "bankrenter" + ] + }, + { + "id": 121791054, + "number": 9210, + "name": "Leverandører mv.", + "categoryName": "Renteudgifter", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "rente", + "leverandør", + "supplier", + "interest" + ] + }, + { + "id": 121791055, + "number": 9220, + "name": "Ikke-fradragsberettigede renter", + "categoryName": "Renteudgifter", + "defaultVatCode": null, + "region": "DK", + "suggestions": [ + "rente", + "ikke-fradrag", + "non-deductible" + ] + } +] \ No newline at end of file diff --git a/account-suggestions/accounts.json b/account-suggestions/accounts.json new file mode 100644 index 0000000..0caf567 --- /dev/null +++ b/account-suggestions/accounts.json @@ -0,0 +1 @@ +[{"id":121791149,"name":"Vareforbrug","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":2000,"usageKey":"DefaultPurchase","categoryName":"Variable omkostninger"},{"id":121791197,"name":"EU-erhvervelser varer (rubrik A-varer)","defaultVatTypeId":7546698,"defaultVatCode":"IEUV","defaultVatTypeRegion":"EU","defaultVatName":"Varekøb EU (rubrik A - varer)","typeKey":"Expense","number":2050,"usageKey":"EUBoxAGoods","categoryName":"Variable omkostninger"},{"id":121791199,"name":"EU-erhvervelser ydelser (rubrik A-ydelser)","defaultVatTypeId":7546699,"defaultVatCode":"IEUY","defaultVatTypeRegion":"EU","defaultVatName":"Ydelseskøb EU (rubrik A - ydelser)","typeKey":"Expense","number":2100,"usageKey":"EUBoxAServices","categoryName":"Variable omkostninger"},{"id":121791200,"name":"Varekøb verden","defaultVatTypeId":7546700,"defaultVatCode":"IVV","defaultVatTypeRegion":"World","defaultVatName":"Varekøb fra verden","typeKey":"Expense","number":2150,"usageKey":"ForeignGoodsPurchases","categoryName":"Variable omkostninger"},{"id":121791201,"name":"Ydelseskøb verden","defaultVatTypeId":7546701,"defaultVatCode":"IVY","defaultVatTypeRegion":"World","defaultVatName":"Ydelseskøb fra verden","typeKey":"Expense","number":2200,"usageKey":"ForeignServicesPurchases","categoryName":"Variable omkostninger"},{"id":121791150,"name":"Fragt med moms","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":2250,"usageKey":"Shipping","categoryName":"Variable omkostninger"},{"id":121790982,"name":"Fragt uden moms","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":2300,"usageKey":"ShippingVatFree","categoryName":"Variable omkostninger"},{"id":121791198,"name":"Fragt - EU","defaultVatTypeId":7546698,"defaultVatCode":"IEUV","defaultVatTypeRegion":"EU","defaultVatName":"Varekøb EU (rubrik A - varer)","typeKey":"Expense","number":2350,"usageKey":"ShippingEU","categoryName":"Variable omkostninger"},{"id":121790983,"name":"Valutakursdifferencer, import","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":2400,"usageKey":"ExchangeRateDifferenceImport","categoryName":"Variable omkostninger"},{"id":121790984,"name":"Varelagerregulering","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":2450,"usageKey":"InventoryRegulation","categoryName":"Variable omkostninger"},{"id":121791151,"name":"Fremmed arbejde","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":2800,"usageKey":"ForeignWork","categoryName":"Variable omkostninger"},{"id":121790989,"name":"AM-indkomst","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3000,"usageKey":"AMIncome","categoryName":"Lønomkostninger"},{"id":121790990,"name":"Arbejdsgiver ATP","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3020,"usageKey":"EmployerATP","categoryName":"Lønomkostninger"},{"id":121790991,"name":"Medarbejder ATP","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3040,"usageKey":"EmployeeATP","categoryName":"Lønomkostninger"},{"id":121790992,"name":"Sygepenge mv.","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3060,"usageKey":"SickPay","categoryName":"Lønomkostninger"},{"id":121790993,"name":"Personalegoder, herunder fri telefon","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3070,"usageKey":"EmployeeBenefits","categoryName":"Lønomkostninger"},{"id":121790994,"name":"B-honorar","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3080,"usageKey":"BFees","categoryName":"Lønomkostninger"},{"id":121790995,"name":"Barsel","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3090,"usageKey":"Maternity","categoryName":"Lønomkostninger"},{"id":121790996,"name":"Feriepenge og SH","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3100,"usageKey":"Vacation","categoryName":"Lønomkostninger"},{"id":121790997,"name":"Pension","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3120,"usageKey":"Pension","categoryName":"Lønomkostninger"},{"id":121790998,"name":"Diæter/rejsegodtgørelse","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3140,"usageKey":"DietTravelAllowance","categoryName":"Lønomkostninger"},{"id":121790999,"name":"Kørsel i egen bil (kilometergodtgørelse)","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3160,"usageKey":"TransportExpenses","categoryName":"Lønomkostninger"},{"id":121791000,"name":"AER/AES/ATP-finansieringsbidrag","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3180,"usageKey":"FundingContrubution","categoryName":"Lønomkostninger"},{"id":121791152,"name":"Arbejdstøj","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":3200,"usageKey":"WorkClothes","categoryName":"Lønomkostninger"},{"id":121791001,"name":"Personaleforsikringer","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3220,"usageKey":"PersonnelInsurance","categoryName":"Lønomkostninger"},{"id":121791153,"name":"Mad under kursus/møder mv., fuldt fradrag","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":3240,"usageKey":"FoodWorkRelated","categoryName":"Lønomkostninger"},{"id":121791002,"name":"Gaver til personalet, fuldt fradrag","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3260,"usageKey":"PersonnelGifts","categoryName":"Lønomkostninger"},{"id":121791154,"name":"Uddannelsesudgifter","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":3280,"usageKey":"Education","categoryName":"Lønomkostninger"},{"id":121791155,"name":"Diverse vedr. ansatte med moms","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":3300,"usageKey":"VariousPersonnel","categoryName":"Lønomkostninger"},{"id":121791003,"name":"Diverse vedr. ansatte uden moms","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3320,"usageKey":"VariousPersonnelVatFree","categoryName":"Lønomkostninger"},{"id":121791004,"name":"Regulering feriepenge","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3340,"usageKey":"VacationRegulation","categoryName":"Lønomkostninger"},{"id":121791005,"name":"Frokostordning til ansatte","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":3355,"usageKey":"Lunch","categoryName":"Lønomkostninger"},{"id":121791156,"name":"Annoncer og reklame","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":4000,"usageKey":"Marketing","categoryName":"Salgsomkostninger"},{"id":121791157,"name":"Udsmykning i forbindelse med arrangementer/events","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":4020,"usageKey":"EventDecoration","categoryName":"Salgsomkostninger"},{"id":121791158,"name":"Hotel, personale, fuldt fradrag","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":4040,"usageKey":"HotelEmployees","categoryName":"Salgsomkostninger"},{"id":121791159,"name":"Hotel, forretningsforbindelser, delvis fradrag","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":4060,"usageKey":"HotelClient","categoryName":"Salgsomkostninger"},{"id":121791008,"name":"Konferencer","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":4080,"usageKey":"Conference","categoryName":"Salgsomkostninger"},{"id":121791160,"name":"Messer","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":4100,"usageKey":"Fair","categoryName":"Salgsomkostninger"},{"id":121791202,"name":"Repræsentation, restaurant, personale, fuldt fradrag","defaultVatTypeId":7546702,"defaultVatCode":"REP","defaultVatTypeRegion":"DK","defaultVatName":"Repræsentation (kvartmoms)","typeKey":"Expense","number":4120,"usageKey":"RestaurantEmployees","categoryName":"Salgsomkostninger"},{"id":121791203,"name":"Repræsentation, restaurant, forretningsforbindelser, delvis fradrag","defaultVatTypeId":7546702,"defaultVatCode":"REP","defaultVatTypeRegion":"DK","defaultVatName":"Repræsentation (kvartmoms)","typeKey":"Expense","number":4140,"usageKey":"RestaurantClient","categoryName":"Salgsomkostninger"},{"id":121791161,"name":"Mad i virksomheden til forretningsforbindelser, delvis fradrag","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":4160,"usageKey":"ClientFood","categoryName":"Salgsomkostninger"},{"id":121791009,"name":"Repræsentation, gaver og blomster, delvis fradrag","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":4180,"usageKey":"RepresentationGifts","categoryName":"Salgsomkostninger"},{"id":121791162,"name":"Anden fradragsberettiget repræsentation med moms","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":4200,"usageKey":"RepresentationDeductible","categoryName":"Salgsomkostninger"},{"id":121791010,"name":"Repræsentation, diverse","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":4220,"usageKey":"RepresentationVarious","categoryName":"Salgsomkostninger"},{"id":121791163,"name":"Øvrige personaleomkostninger","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":4240,"usageKey":"PersonnelExpenses","categoryName":"Salgsomkostninger"},{"id":121791011,"name":"Ej fradragsberettiget andel","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":4260,"usageKey":"NonDeductibleShare","categoryName":"Salgsomkostninger"},{"id":121791012,"name":"Rejseomkostninger","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":4280,"usageKey":"Travel","categoryName":"Salgsomkostninger"},{"id":121791164,"name":"Husleje","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":5000,"usageKey":"Rent","categoryName":"Lokaleomkostninger"},{"id":121791015,"name":"Husleje uden moms","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":5010,"usageKey":"RentVatFree","categoryName":"Lokaleomkostninger"},{"id":121791165,"name":"El","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":5025,"usageKey":"Electricity","categoryName":"Lokaleomkostninger"},{"id":121791166,"name":"Vand","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":5030,"usageKey":"Water","categoryName":"Lokaleomkostninger"},{"id":121791167,"name":"Varme","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":5035,"usageKey":"Heating","categoryName":"Lokaleomkostninger"},{"id":121791016,"name":"Elafgift","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":5040,"usageKey":"ElectricityTax","categoryName":"Lokaleomkostninger"},{"id":124507843,"name":"Naturgas- og bygasafgift","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":5045,"usageKey":"GasTax","categoryName":"Lokaleomkostninger"},{"id":124519865,"name":"Vandafgift","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":5050,"usageKey":"WaterTax","categoryName":"Lokaleomkostninger"},{"id":121791168,"name":"Rengøring og affaldshåndtering","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":5060,"usageKey":"Cleaning","categoryName":"Lokaleomkostninger"},{"id":121791169,"name":"Reparation og vedligeholdelse","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":5080,"usageKey":"Maintainance","categoryName":"Lokaleomkostninger"},{"id":121791017,"name":"Ejendomsskat","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":5100,"usageKey":"PropertyTax","categoryName":"Lokaleomkostninger"},{"id":121791018,"name":"Ejendomsforsikring","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":5120,"usageKey":"PropertyInsurance","categoryName":"Lokaleomkostninger"},{"id":121791170,"name":"Mødelokaler","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":5140,"usageKey":"MeetingRooms","categoryName":"Lokaleomkostninger"},{"id":121791171,"name":"Dekoration","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":5160,"usageKey":"PremisesDecoration","categoryName":"Lokaleomkostninger"},{"id":121791172,"name":"Billeje (gulplade)","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":6000,"usageKey":"CarRent","categoryName":"Kørsel og rejser"},{"id":121791173,"name":"Brændstof (gulplade)","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":6020,"usageKey":"CarFuel","categoryName":"Kørsel og rejser"},{"id":121791174,"name":"Vedligeholdelse af bil (gulplade)","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":6040,"usageKey":"CarMaintainance","categoryName":"Kørsel og rejser"},{"id":121791021,"name":"Vægtafgift og forsikringer","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":6060,"usageKey":"CarInsurance","categoryName":"Kørsel og rejser"},{"id":121791175,"name":"Parkering (gulplade)","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":6080,"usageKey":"CarParking","categoryName":"Kørsel og rejser"},{"id":121791022,"name":"Parkering uden moms","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":6085,"usageKey":"CarParkingVatFree","categoryName":"Kørsel og rejser"},{"id":121791176,"name":"Broafgift","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":6100,"usageKey":"BridgeToll","categoryName":"Kørsel og rejser"},{"id":121791023,"name":"Taxa","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":6120,"usageKey":"TransportationTaxi","categoryName":"Kørsel og rejser"},{"id":121791024,"name":"Tog","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":6140,"usageKey":"TransportationTrain","categoryName":"Kørsel og rejser"},{"id":121791025,"name":"Fly","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":6160,"usageKey":"TransportationFlying","categoryName":"Kørsel og rejser"},{"id":121791026,"name":"Bus","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":6180,"usageKey":"TransportationBus","categoryName":"Kørsel og rejser"},{"id":121791177,"name":"Færge","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":6200,"usageKey":"TransportationFerry","categoryName":"Kørsel og rejser"},{"id":121791027,"name":"Diverse transportomkostninger uden moms","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":6400,"usageKey":"TransportationVarious","categoryName":"Kørsel og rejser"},{"id":121791178,"name":"Revision og regnskabsmæssig assistance","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7005,"usageKey":"Auditor","categoryName":"Administrationsomkostninger"},{"id":121791179,"name":"Advokat","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7010,"usageKey":"Laywer","categoryName":"Administrationsomkostninger"},{"id":121791180,"name":"Bogføringsassistance","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7020,"usageKey":"BookkeepingAssistance","categoryName":"Administrationsomkostninger"},{"id":121791181,"name":"Konsulentbistand","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7040,"usageKey":"ConsultancyServices","categoryName":"Administrationsomkostninger"},{"id":121791182,"name":"Kontingenter inkl. moms","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7060,"usageKey":"FeesIncludingVAT","categoryName":"Administrationsomkostninger"},{"id":121791030,"name":"Kontingenter ekskl. moms","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7080,"usageKey":"FeesExcludingVAT","categoryName":"Administrationsomkostninger"},{"id":121791031,"name":"Aviser","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7100,"usageKey":"Newspapers","categoryName":"Administrationsomkostninger"},{"id":121791183,"name":"Faglitteratur","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7120,"usageKey":"ProfessionalLiterature","categoryName":"Administrationsomkostninger"},{"id":121791184,"name":"Anden litteratur","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7140,"usageKey":"OtherLiterature","categoryName":"Administrationsomkostninger"},{"id":121791032,"name":"Erhvervsforsikringer","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7160,"usageKey":"BusinessInsurance","categoryName":"Administrationsomkostninger"},{"id":121791185,"name":"Fragt og kørsel","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7180,"usageKey":"FreightAndTransportation","categoryName":"Administrationsomkostninger"},{"id":121791186,"name":"Kontorartikler og tryksager","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7200,"usageKey":"OfficeSuppliesAndPrinting","categoryName":"Administrationsomkostninger"},{"id":121791033,"name":"Porto og gebyrer","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7220,"usageKey":"SalesPaymentFee","categoryName":"Administrationsomkostninger"},{"id":121791187,"name":"Telefoni","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7240,"usageKey":"PhoneExpenses","categoryName":"Administrationsomkostninger"},{"id":121791034,"name":"Beskatning af fri telefoni","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7260,"usageKey":"TaxationOfFreeTelephony","categoryName":"Administrationsomkostninger"},{"id":121791188,"name":"Regnskabsprogram","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7280,"usageKey":"AccountingSoftware","categoryName":"Administrationsomkostninger"},{"id":121791189,"name":"Internet og webhotel","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7300,"usageKey":"InternetAndWebHosting","categoryName":"Administrationsomkostninger"},{"id":121791190,"name":"Køb af software","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7320,"usageKey":"PurchaseOfSoftware","categoryName":"Administrationsomkostninger"},{"id":121791035,"name":"Offentlige bøder og gebyrer","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7360,"usageKey":"PublicFinesAndFees","categoryName":"Administrationsomkostninger"},{"id":121791036,"name":"Registrerede kassedifferencer","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7380,"usageKey":"RecordedCashDifferences","categoryName":"Administrationsomkostninger"},{"id":121791191,"name":"Betalingsløsning","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7400,"usageKey":"PaymentSolutions","categoryName":"Administrationsomkostninger"},{"id":121791037,"name":"Indløsere","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7420,"usageKey":"Acquirers","categoryName":"Administrationsomkostninger"},{"id":121791038,"name":"Licens","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7440,"usageKey":"Licenses","categoryName":"Administrationsomkostninger"},{"id":121791192,"name":"Diverse inkl. moms","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7460,"usageKey":"MiscellaneousIncludingVAT","categoryName":"Administrationsomkostninger"},{"id":121791039,"name":"Diverse ekskl. moms","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7480,"usageKey":"MiscellaneousExcludingVAT","categoryName":"Administrationsomkostninger"},{"id":121791204,"name":"Generalforsamling, bestyrelsesmøder ude i byen, fuld fradrag","defaultVatTypeId":7546702,"defaultVatCode":"REP","defaultVatTypeRegion":"DK","defaultVatName":"Repræsentation (kvartmoms)","typeKey":"Expense","number":7500,"usageKey":"GeneralOutsideMeetingsDeductible","categoryName":"Administrationsomkostninger"},{"id":121791040,"name":"Generalforsamling i virksomhedens lokaler","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":7520,"usageKey":"GeneralPremisesMeetings","categoryName":"Administrationsomkostninger"},{"id":121791193,"name":"Bestyrelsesmøder i virksomhedens lokaler","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":7540,"usageKey":"BoardPremisesMeetings","categoryName":"Administrationsomkostninger"},{"id":121791205,"name":"Bestyrelsesmøder ude i byen","defaultVatTypeId":7546702,"defaultVatCode":"REP","defaultVatTypeRegion":"DK","defaultVatName":"Repræsentation (kvartmoms)","typeKey":"Expense","number":7560,"usageKey":"BoardOutsideMeetings","categoryName":"Administrationsomkostninger"},{"id":121791194,"name":"Småanskaffelser (straksafskrivning)","defaultVatTypeId":7546697,"defaultVatCode":"I25","defaultVatTypeRegion":"DK","defaultVatName":"Dansk købsmoms","typeKey":"Expense","number":8040,"usageKey":"SmallPurchases","categoryName":"Afskrivninger"},{"id":121791206,"name":"Småanskaffelser med omvendt betalingspligt","defaultVatTypeId":7546703,"defaultVatCode":"OBPK","defaultVatTypeRegion":"DK","defaultVatName":"Dansk køb med omvendt betalingspligt","typeKey":"Expense","number":8050,"usageKey":"SmallPurchasesReverseCharge","categoryName":"Afskrivninger"},{"id":121791053,"name":"Bankrenter","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":9200,"usageKey":"BankInterestExpense","categoryName":"Renteudgifter"},{"id":121791054,"name":"Leverandører mv.","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":9210,"usageKey":"SuppliersInterest","categoryName":"Renteudgifter"},{"id":121791055,"name":"Ikke-fradragsberettigede renter","defaultVatTypeId":null,"defaultVatCode":null,"defaultVatTypeRegion":null,"defaultVatName":null,"typeKey":"Expense","number":9220,"usageKey":"NonDeductibleInterest","categoryName":"Renteudgifter"}] \ No newline at end of file diff --git a/account-suggestions/accounts.toon b/account-suggestions/accounts.toon new file mode 100644 index 0000000..b8fad17 --- /dev/null +++ b/account-suggestions/accounts.toon @@ -0,0 +1,122 @@ +# Dinero Expense Accounts with Suggestions +# 104 accounts for AI-powered account suggestion + +meta: + source: Dinero API + organizationId: 582288 + accountType: expense + totalAccounts: 104 + removedAccounts: 3220|5120|7140|7280 + +regionVariants: + varekøb: DK=2000|EU=2050|WORLD=2150 + ydelseskøb: EU=2100|WORLD=2200 + fragt: DK=2250|DK_VATFREE=2300|EU=2350 + husleje: DK=5000|DK_VATFREE=5010 + parkering: DK=6080|DK_VATFREE=6085 + +accounts[104]{number,name,category,vatCode,region,vatRubric,suggestions}: + 2000,Vareforbrug,Variable omkostninger,I25,DK,,varer|indkøb|lager|goods|inventory|purchase|varekøb + 2050,EU-erhvervelser varer (rubrik A-varer),Variable omkostninger,IEUV,EU,EU_BOX_A_GOODS,eu|europa|germany|france|italien|spain|netherlands|eu-erhvervelse + 2100,EU-erhvervelser ydelser (rubrik A-ydelser),Variable omkostninger,IEUY,EU,EU_BOX_A_SERVICES,eu|europa|ydelse|service|eu-ydelse + 2150,Varekøb verden,Variable omkostninger,IVV,WORLD,,usa|uk|kina|china|verden|import|world + 2200,Ydelseskøb verden,Variable omkostninger,IVY,WORLD,,usa|uk|kina|verden|service|ydelse|world + 2250,Fragt med moms,Variable omkostninger,I25,DK,,fragt|levering|shipping|delivery|postnord|gls|dao|bring|ups|dhl + 2300,Fragt uden moms,Variable omkostninger,,DK,,fragt|shipping|delivery|momsfri + 2350,Fragt - EU,Variable omkostninger,IEUV,EU,EU_BOX_A_GOODS,fragt|eu|shipping|dhl|ups|fedex + 2400,Valutakursdifferencer import,Variable omkostninger,,DK,,valuta|kurs|currency|exchange + 2450,Varelagerregulering,Variable omkostninger,,DK,,lager|regulering|inventory + 2800,Fremmed arbejde,Variable omkostninger,I25,DK,,underleverandør|freelance|contractor|subcontractor|fremmed arbejde + 3000,AM-indkomst,Lønomkostninger,,DK,,løn|salary|wage|payroll|am-indkomst + 3020,Arbejdsgiver ATP,Lønomkostninger,,DK,,atp|arbejdsgiver + 3040,Medarbejder ATP,Lønomkostninger,,DK,,atp|medarbejder + 3060,Sygepenge mv.,Lønomkostninger,,DK,,sygepenge|sickness|sygedag + 3070,Personalegoder herunder fri telefon,Lønomkostninger,,DK,,personalegode|fri telefon|benefit|goder + 3080,B-honorar,Lønomkostninger,,DK,,b-honorar|honorar|fee + 3090,Barsel,Lønomkostninger,,DK,,barsel|maternity|fødsel + 3100,Feriepenge og SH,Lønomkostninger,,DK,,ferie|feriepenge|sh|vacation|holiday + 3120,Pension,Lønomkostninger,,DK,,pension|pensionsbidrag|retirement + 3140,Diæter/rejsegodtgørelse,Lønomkostninger,,DK,,diæt|rejsegodtgørelse|per diem|allowance + 3160,Kørsel i egen bil (kilometergodtgørelse),Lønomkostninger,,DK,,kørsel|kilometer|km|mileage|km-godtgørelse|bil + 3180,AER/AES/ATP-finansieringsbidrag,Lønomkostninger,,DK,,aer|aes|atp|finansieringsbidrag + 3200,Arbejdstøj,Lønomkostninger,I25,DK,,arbejdstøj|uniform|workwear|tøj + 3240,Mad under kursus/møder mv. fuldt fradrag,Lønomkostninger,I25,DK,,mad|kursus|møde|food|catering|frokost + 3260,Gaver til personalet fuldt fradrag,Lønomkostninger,,DK,,gave|personale|gift|employee + 3280,Uddannelsesudgifter,Lønomkostninger,I25,DK,,kursus|uddannelse|course|training|efteruddannelse + 3300,Diverse vedr. ansatte med moms,Lønomkostninger,I25,DK,,diverse|personale|ansatte|moms + 3320,Diverse vedr. ansatte uden moms,Lønomkostninger,,DK,,diverse|personale|ansatte|momsfri + 3340,Regulering feriepenge,Lønomkostninger,,DK,,ferie|regulering|feriepenge + 3355,Frokostordning til ansatte,Lønomkostninger,,DK,,frokost|frokostordning|lunch|kantine + 4000,Annoncer og reklame,Salgsomkostninger,I25,DK,,annonce|reklame|marketing|ads|google|facebook|linkedin|meta|advertising + 4020,Udsmykning i forbindelse med arrangementer/events,Salgsomkostninger,I25,DK,,udsmykning|event|arrangement|decoration + 4040,Hotel personale fuldt fradrag,Salgsomkostninger,I25,DK,,hotel|overnatning|accommodation|booking.com|hotels.com|personale + 4060,Hotel forretningsforbindelser delvis fradrag,Salgsomkostninger,I25,DK,,hotel|overnatning|kunde|client|forretningsforbindelse + 4080,Konferencer,Salgsomkostninger,,DK,,konference|conference|seminar|event + 4100,Messer,Salgsomkostninger,I25,DK,,messe|fair|exhibition|udstilling + 4120,Restaurant personale fuldt fradrag,Salgsomkostninger,REP,DK,,restaurant|frokost|middag|lunch|dinner|café|personale + 4140,Restaurant forretningsforbindelser delvis fradrag,Salgsomkostninger,REP,DK,,restaurant|repræsentation|kunde|client|forretningsforbindelse + 4160,Mad i virksomheden til forretningsforbindelser delvis fradrag,Salgsomkostninger,I25,DK,,mad|virksomhed|forretningsforbindelse|catering + 4180,Repræsentation gaver og blomster delvis fradrag,Salgsomkostninger,,DK,,gave|blomster|gift|flowers|repræsentation + 4200,Anden fradragsberettiget repræsentation med moms,Salgsomkostninger,I25,DK,,repræsentation|representation + 4220,Repræsentation diverse,Salgsomkostninger,,DK,,repræsentation|diverse + 4240,Øvrige personaleomkostninger,Salgsomkostninger,I25,DK,,personale|personnel|staff + 4260,Ej fradragsberettiget andel,Salgsomkostninger,,DK,,ikke-fradrag|non-deductible + 4280,Rejseomkostninger,Salgsomkostninger,,DK,,rejse|travel|tur|trip + 5000,Husleje,Lokaleomkostninger,I25,DK,,husleje|leje|rent|kontor|lokale + 5010,Husleje uden moms,Lokaleomkostninger,,DK,,husleje|leje|rent|momsfri + 5025,El,Lokaleomkostninger,I25,DK,,el|strøm|electricity|power|ørsted|norlys|ewii|andel + 5030,Vand,Lokaleomkostninger,I25,DK,,vand|water|vandværk + 5035,Varme,Lokaleomkostninger,I25,DK,,varme|gas|fjernvarme|heating|naturgas + 5040,Elafgift,Lokaleomkostninger,,DK,,elafgift|afgift|tax + 5045,Naturgas- og bygasafgift,Lokaleomkostninger,,DK,,gas|afgift|naturgas + 5050,Vandafgift,Lokaleomkostninger,,DK,,vand|afgift|vandafgift + 5060,Rengøring og affaldshåndtering,Lokaleomkostninger,I25,DK,,rengøring|cleaning|affald|waste|renovation + 5080,Reparation og vedligeholdelse,Lokaleomkostninger,I25,DK,,reparation|vedligeholdelse|maintenance|repair + 5100,Ejendomsskat,Lokaleomkostninger,,DK,,ejendomsskat|property tax|skat + 5140,Mødelokaler,Lokaleomkostninger,I25,DK,,mødelokale|meeting room|konference + 5160,Dekoration,Lokaleomkostninger,I25,DK,,dekoration|decoration|indretning + 6000,Billeje (gulplade),Kørsel og rejser,I25,DK,,billeje|car rental|leje|hertz|avis|europcar|sixt|enterprise + 6020,Brændstof (gulplade),Kørsel og rejser,I25,DK,,benzin|diesel|fuel|brændstof|ok|shell|q8|circle k|ingo + 6040,Vedligeholdelse af bil (gulplade),Kørsel og rejser,I25,DK,,bil|vedligeholdelse|car|service|værksted|dæk + 6060,Vægtafgift og forsikringer,Kørsel og rejser,,DK,,vægtafgift|forsikring|car insurance|bilforsikring + 6080,Parkering (gulplade),Kørsel og rejser,I25,DK,,parkering|parking|p-afgift|easypark|parkman|apcoa + 6085,Parkering uden moms,Kørsel og rejser,,DK,,parkering|parking|momsfri + 6100,Broafgift,Kørsel og rejser,I25,DK,,bro|bridge|storebælt|øresund|toll|afgift + 6120,Taxa,Kørsel og rejser,,DK,,taxa|taxi|uber|bolt|dantaxi|viggo|4x27 + 6140,Tog,Kørsel og rejser,,DK,,tog|dsb|train|rejsekort|billet + 6160,Fly,Kørsel og rejser,,DK,,fly|flight|sas|norwegian|ryanair|lufthansa|flybillet + 6180,Bus,Kørsel og rejser,,DK,,bus|movia|arriva|flixbus + 6200,Færge,Kørsel og rejser,I25,DK,,færge|ferry|molslinjen|scandlines|bornholmslinjen + 6400,Diverse transportomkostninger uden moms,Kørsel og rejser,,DK,,transport|diverse|transportation + 7005,Revision og regnskabsmæssig assistance,Administrationsomkostninger,I25,DK,,revisor|revision|audit|regnskab|pwc|deloitte|ey|kpmg|bdo + 7010,Advokat,Administrationsomkostninger,I25,DK,,advokat|lawyer|legal|juridisk|attorney + 7020,Bogføringsassistance,Administrationsomkostninger,I25,DK,,bogføring|bookkeeping|regnskab|bogholder + 7040,Konsulentbistand,Administrationsomkostninger,I25,DK,,konsulent|consultant|rådgivning|advisory|rådgiver + 7060,Kontingenter inkl. moms,Administrationsomkostninger,I25,DK,,kontingent|medlemskab|membership|forening + 7080,Kontingenter ekskl. moms,Administrationsomkostninger,,DK,,kontingent|medlemskab|momsfri + 7100,Aviser,Administrationsomkostninger,,DK,,avis|newspaper|berlingske|politiken|jyllandsposten|børsen|dagblad + 7120,Faglitteratur,Administrationsomkostninger,I25,DK,,bog|book|litteratur|amazon|saxo|tales|faglitteratur + 7160,Erhvervsforsikringer,Administrationsomkostninger,,DK,,forsikring|insurance|tryg|topdanmark|if|codan|gjensidige|alm brand + 7180,Fragt og kørsel,Administrationsomkostninger,I25,DK,,fragt|kørsel|shipping|transport + 7200,Kontorartikler og tryksager,Administrationsomkostninger,I25,DK,,kontor|office|papir|printer|lyreco|staples|kontorartikler + 7220,Porto og gebyrer,Administrationsomkostninger,,DK,,porto|gebyr|postage|fee|postnord + 7240,Telefoni,Administrationsomkostninger,I25,DK,,telefon|mobil|phone|mobile|telia|telenor|3|telmore|cbs|lebara + 7260,Beskatning af fri telefoni,Administrationsomkostninger,,DK,,telefon|beskatning|fri telefon + 7300,Internet og webhotel,Administrationsomkostninger,I25,DK,,internet|hosting|domain|web|simply|one.com|cloudflare|aws|azure + 7320,Køb af software,Administrationsomkostninger,I25,DK,,software|app|abonnement|subscription|saas|adobe|microsoft|slack|notion|google workspace|dropbox + 7360,Offentlige bøder og gebyrer,Administrationsomkostninger,,DK,,bøde|gebyr|fine|offentlig + 7380,Registrerede kassedifferencer,Administrationsomkostninger,,DK,,kasse|difference|cash + 7400,Betalingsløsning,Administrationsomkostninger,I25,DK,,betaling|payment|stripe|mobilepay|nets|clearhaus|paypal + 7420,Indløsere,Administrationsomkostninger,,DK,,indløser|acquirer|nets|clearhaus + 7440,Licens,Administrationsomkostninger,,DK,,licens|license|rettighed + 7460,Diverse inkl. moms,Administrationsomkostninger,I25,DK,,diverse|moms|miscellaneous + 7480,Diverse ekskl. moms,Administrationsomkostninger,,DK,,diverse|momsfri|miscellaneous + 7500,Generalforsamling bestyrelsesmøder ude i byen fuld fradrag,Administrationsomkostninger,REP,DK,,generalforsamling|bestyrelse|møde|board|meeting + 7520,Generalforsamling i virksomhedens lokaler,Administrationsomkostninger,,DK,,generalforsamling|møde|lokale + 7540,Bestyrelsesmøder i virksomhedens lokaler,Administrationsomkostninger,I25,DK,,bestyrelse|board|møde|lokale + 7560,Bestyrelsesmøder ude i byen,Administrationsomkostninger,REP,DK,,bestyrelse|board|møde|restaurant + 8040,Småanskaffelser (straksafskrivning),Afskrivninger,I25,DK,,computer|laptop|møbler|furniture|udstyr|equipment|apple|dell|lenovo|småanskaffelse + 8050,Småanskaffelser med omvendt betalingspligt,Afskrivninger,OBPK,DK,,småanskaffelse|omvendt betalingspligt|reverse charge + 9200,Bankrenter,Renteudgifter,,DK,,rente|bank|interest|bankrenter + 9210,Leverandører mv.,Renteudgifter,,DK,,rente|leverandør|supplier|interest + 9220,Ikke-fradragsberettigede renter,Renteudgifter,,DK,,rente|ikke-fradrag|non-deductible diff --git a/account-suggestions/fetch-accounts.sh b/account-suggestions/fetch-accounts.sh new file mode 100755 index 0000000..8eb2bed --- /dev/null +++ b/account-suggestions/fetch-accounts.sh @@ -0,0 +1,6 @@ +#!/bin/bash +curl -s 'https://app.dinero.dk/api/582288/accountsummary?accountTypeKey=expense&excludeNonPurchaseAccounts=true' \ + -H 'accept: application/json, text/plain, */*' \ + -H 'accept-language: da-DK,da;q=0.9,en-US;q=0.8,en;q=0.7' \ + -b '__cf_bm=nhKMmoysS7PiA25IM4vETcA8XZEbPMIiU6MsKhtS4Ww-1768687921-1.0.1.1-7iJsT6CmT7e33yCRa9KHRj1eHwKYTLZS1dQ85BF6F0oaLHsdmRYbrIj04bav0QzylZHsgXS42QvmkNf0EGNRyNSrSdWJ3PwR5urPSAO1kBw; .AspNetCore.Cookies.V3=chunks-2; .AspNetCore.Cookies.V3C1=CfDJ8IiuaGEVxBVJjTO5tUpz94LGEH_GPsO5w-lZSH_B4LtZQM-e_d5kNF5a3i1hSQmJIl-ah0ujdsvBsYkuwZvPmZ2DXwyXbMNmssK9lUZAIGUZtw8CHGnaFp-eiQ1T7WXmRsZn5LJl2hjW9_zG0KVvvWJmsPq-9rWsN4um08PLvUyReL1XhideBkFnF4x9-mNPyAPK6NIENtA62hJ-3kCR1PYRLc4-LUiZu0fTwe0FX92rJI1GH-1PHeYY0p2J2OnOyw2xY0TqMAAm-ghVN78x2H72P_7AAbntDgaelok3L5BSWDMOdAXJTUyXdcNA-BlDc-41uWpz2ZzCCLPSuSxFZ3Eszixsc2EUxwgbtjDMF1fvFEaoyc8JwWnzY9VdAOjBZ1bvbNC7L2tJn4R1LELPxR4RX95PoldKwg-3OFpQG1pPT4EmDC44YBDLo5EGTpUzc1ET8aDXQ1Z-ukpylb3AdQpITelv7Rbi5DJxKGzExduG5tU-dBwmlE-QsD_1pAgzo6CiINhZ5cx7lto5XMmiKilLYaoVplCHAZQfedajXXlQZYhQSFuexIdCy6YfaS9WM_lFVW-cZteobejlZ1azxpk9sgxdxEIrBiaaYCGku8Amppflo4jJnSJcWLsQtvLnHSCBRT1ngS_b-pAkh0GupX9BVtcZiBm3YitsWcLJL5SLQrftPnjrNRPBx_b20QL-h1JDISa-6FnbTJ7UjBSfHLk5TzelXTAzORFooTLIozGrh9RG1e5kb_t_L_jpR8fVcH2T5uNDshCU6IEpe4-5MSphFQoeMtqG-iSyMenLCX8CggmHLKs6_rSfiaBixlkI_abICV8OLBSjHNYPFxa7aldw_icRO2K4bxPIQ9e8-poGYp9p2AbstEElzupiZfVv2Z-u9E6KXv2LVn80_rwmgLVkA0YCNL0msei6MUqpNa4ewONONFzbA2jQfvRqpX107K7aCl2j3Rfx2ZYIsfNoU8dHbU8of0X1JtGljTQjxuwfmDejrUfRCLgVZphct4EA1tqsOCYyXOf2z465cCmMGMN3pU6-XOIDxr-lU5Quk3ytjpQmPipDa0iwKn52ngHs-YC-XPcYXWe33I5WSZKdf3yviUx3WeuCFEilcGANOWLqimZf2GfcAxy6p8TkHzj6n-QZVaRjUOpHGvpfdgCgM1XoCelSPHQOE0HQS-jUpNnQ7TgJBMat1MEJCJZk8ZPLjNwJNE1VP9tu50jWy7M9Y1a7AlKGro3kr_sWGBJ1Psn7-Bkn6QneNEu6mzwmYFy4kYtJguMzd2XNr4kTmjXcMraunWny_x_ue9mHJknrQOPNpHXg6kVr0DJTpIQRxfq9ZCLEH6xZ1P0LXyOK6eVDnEShzjj8veqqTehsnXI-EihGRkRi7qhtM8qiprc2-6RlEsihIz259Cw4H7WqvCUt-SEc9hIRIsIiim9xwLow1k58cxBkhSp0YC3PP_kH83BAX944ulwApnB8zzutk2T71NEZUSoT1nAPq4UgCDQ5-arOD0YgXPBSf-v0WIPrxIzYG4ciS2aLItdIKSO4yqAxNVbb6lSwJvejMqnTdjc5VquAjT7x5zLqF_WAKbpPjh8BMY_f9rBs1xNSIuZ1xQSsa5QcQohCBbhqVG7dCdmCKeJ57_x-xqJEg6UGpIZAXX-X1XKotvPSMr53y3klMwqTpE2o9pqh0dBrONOiWOp_672xJPnxaLMoqiuzr2-zXiped0Gp2_ASCeISL5d-o8x-uQpWOHttjMNIEoiTLf1VsRLFGbwbzskRBaOFug7hIdVFR9jXae9RQXbjRh5vcpp8f3xslRjvJgUEAubv1nJp8IEgPW8SaVodZ3YI4w5EMot0x3EC2_u-Bvfk4-egSqHso1Nhq7FvGuH3Avi9FLE7qNHVwwFNtku-d7bKU_CoPqGrQCcQFhUKWVujyCQFpgeOWhCm-slOoHxAgfKb03kiHWu3qv73vhB7LJYotX9ShI__DFzq17XQExiJhzsy6NB5o5ZLDwiZhiq8lj44YL1A2ZUUAcSlaaXbVc5caJxKtYLNr3OZDM33yLxuOpBhVkR6HMCVBmNxIP8o2ilaHl_Q2QUWz_sQrneoVJVY9Rxjj7FtMtHNkBSQy-dbuZ0dKlI7TeP0hv6Ri93I3JSThSQ9Ak0NTWL8oLMniX-Nu1BAeyH7oOKujAIj3e_hoYYVBLpaSKcHlNQdFjUYnOeP-oq6_JLQXlpbarNJtUWgrojelBFpp0ZhSzORm7dOi7uaKpl2FQ1s519rwmlb6nimC607YSBjKHT2NiXR7buhdgrrLtHd-lC6yKMK6d8PScUuYHlpIHd1QP6GFthhiCXA-jLNnn-WjFyssEJBFFHlNLTYTWO9SQTzhvFam20DR5uLZxE_zeNEoXK-yj_LQYVn1Ez7i43BIKDgTpH1UUTPIFp54R7DCLSbYgDc1k61qj4v9-NVF5Ba7ksyNbMJQoxzuEvnN7gIf1MYVRV7gj4QC7nxUInRgETLpXtvckiwwsDRRckCfxr_GJjOCLFCxD3Oopy2Jx85mFz7oeeT3BBm--cXiN0-GUVLGIoJaOuxr1PatjcxANEu86vDAGqW1rCl162ASMc8EDaGqRyM_-HLwyBmZ-bFgUxF-jBfHwhFJCt1nbmmeWeFg5OuJcz_ItNsXLL6ihfr6VMk-HXIisUTUtVgYY9jQsq8KV4lzagzBzE1VndQegHDetsOTYJ8YXYFRlR8gnC4naDmErOqpVHL_Jrj24ixw1m50oHHPEjDTKv5UOM1InUNkHbL718YPgZ4CPnS8utekS0O8n0pJJGgGgb9spgXflyDZexLy-Y1ZWPI5yZO4XcPSzMthxHfnmAy3-I3kSZmS9JCZf25lA2Zq8NDjzK0YqxDsL4fl-iJee0nnMSAjdpJyrBzpepaMZLcr1vfoQrkVM2ZBpTbkOjhXU3GK5PYzxhn1nTSDlWTecYqZniqaO6xUl86ejATyhq0h0nUvIdQj7Wqzp8pH_7ee8bZgtvwmCyAYxInGz62PfX46ON45bqITaapE741-SXd_J9Satn63A_fpT2j2uYDP5snEQDfWIl525ZLXAuGw_OlNwlvldKEqKx_JBkE_vJRQzxeZtS5T8JtaKxfo4Oi7duolQ27qbUIbwJfDCDudzIZx0ES2x7Qps5wWI_Xe548eyvMcqi3c7MuOJIVkuFQi5CGJr3utbV1Bo3owlpA7RY6284zxbgCCpTIllkybdBi1OaZDy8zZP_lEF4O9lbDz_v-SVD3JH1s7JAUqZWFLKduuNcD90eYS-cM1cI4SvquYE_Ox56eDNjOa59JASk7ZXEokDYIWMnssNKzt50X-wMaWC2FYR5STh74q9iX8c87Z1u4C0LzuEZnGM7-xG4spI6kZbwzntaN9_Rtfh1GcRrl0nYH4Oi6apGaf9hQxRXLP_cONU1psTVeM2Y_HtyYSNWTFuVEfoXdglNKmA_wKVc_T6K83XHhWyTj3dmXu4vTXd9-iOxeXM0KgwSlY54R4Lc2yK9Gd8d2MQMjtKfd0J2DuV6Ax4K1nODWmdEbmr0OU6VNMKQHqsgWCGeCNCdH3IeQ4N3-1upv4WUawD3k_eUi4xMGHhrdCjIK7thMhxvL_Sb3xLp2FGnnGHZqDZvHBtg6clq9VSrtObQaA_bfUUs5WmFL6Edz-jxjHfyfj5j_9PncYKEZw389c5HTk5Rm3Zs9POH9SFc2vMmXvtwydnusT_BM1qI7b_pzJ8BgmCVwg17RsiSp2t8QRC7QRg7OeCZZt6pY9FGvZhezU7_L2bRfvtF5-iFh2I-RbGYSOjQb1fQlRB-PB70Z1It592yyndi-zhZ9iBIt091qWCPGj6HXdb-I0241ry7aM4DetuQbugZYiQFDFlEKM17jH3gQwhFvNA6d9xbM0Yu_ZsPK5YkjBZ7Kg5i84UXtsQoH_1sPARsWoXUMuS08yJ8wEB7SYCKwxngasIQ; .AspNetCore.Cookies.V3C2=PKgaNUbIf-eGJFj232ziHoCCRWk6xZ-HbvYHzvRjHHgGTG5aMWmxS5CFoitBKPCnjv7pX6fk3CbPfyLiBkkWOk3TCWfWvV8TSnVfmaA3o0MHq_jrjL61KD3mS8ZXInwgTEh-aNAHWcMZruoN50qYEtKhzXr_3ANmSM3sLO-aipR7oNxzV8km1JhrI4ThCFZQlZ5tx2FaWkoetRDlegrrUVV4Fo1h9e7Q-TJX7SbXyc60IIV3CLKsrA1D2hJTZIafR9NaVUtih3Yv99Ou0qqiwhOGbTB6I0BwDxOdpQdms0e7wcNWMpY8Oz1QsewVvAEPX-ZTGJz4DAU0xxO173a1X_5xahUyb32qlpcmiBcBvMvDXEy36LELjFUBgq4k7I0W342uM_kHDKZALmOETqETRmUFTGmJF4j4cDlk_HnxAO6zC30dvEXRK2f9OB24Fmr5m5JbCK3J4fU4po8E67Ptf8evv9yTw-l8kfLE_lmF6ojN9VpC3DnXMgcH4yrA_F7NVdvCCmQ5-3BBR4Iyk3OOgcUSHghmv83fVNfFI545rNrvtBDcsflPpT1wRgB_Zi2w4JLG9XwhueZeAtOelzC2-0B37oD0G5MB5e6cwR8LOPAgdu3thpgsrebnkwoMmX50C7cIpkXPAXIuuZXsbtB0Zbcg2QqWVcq5_zA5Km5ZK_vaIzZYi-hEGXegVKYyCUYHOQFY07d7A_umkZaR7fikA7DBkeGRUqnqMBN3QAdqYGtKPZBqeW5r8VeuvjlcmHcj64TwwnkDREi6uWSd0Bjrp1R5R69Om38FMlyKlLD0OM6o7I_aVhhNLLoS4Mky9R_4W4Zf14fZIn3cScl_InQqk1ty-XagH5bT4MLjVIiRWVLr0fK-NdyU8b72sl5qY-Ks3jCmcFh8UXAxC9rf6SDAwEZkg230p-HRos9L4m8cK9gSfVgqA9FsxiGxvvbe-VZ8cgdCvL62JZ7UyrWzMTWKJsME-LIwaZBpeqKoAnXx58z-b2eVSpfQLG3uY347-Sk0Mh6OIpwnI9QfqxyulbMQ1oaGyj1Oc5N-G5xo0qLkEwtuaBsVWdTPV6nSmyzLNUVmKhDyvM2zu5RFd9jZULys6bYcN3haut8srU0UmZadsztwGHKhD88MG6aHyzkW6t7okXpXc40EMCZKKM21pZMUV9gSzbbi7pGt-49spZNl1-5_NlLddThyNDgYL1z6PfQ9D0DitGDZbp8_-d6DvhPETHQN0bNp512wXx8Y49zHVRKCheEgguRIvxCNZribbHoPU5l2uQ59kPRBwr-npds_unxkiCg3QOD8pO7RKl9nxuOqeAsDo0nyO0LWs2CQS6dzVBHHpNkKP9VNN37IR4xbc6mh_FiZkxsUbFydgNamgR-fHSV1WMk3paRE0vw5GAZYg144XReJblQIpIFz0O5k; User=Authenticated' \ + -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36' diff --git a/account-suggestions/process-accounts.js b/account-suggestions/process-accounts.js new file mode 100644 index 0000000..95ab132 --- /dev/null +++ b/account-suggestions/process-accounts.js @@ -0,0 +1,199 @@ +const fs = require('fs'); + +// Load accounts +const accounts = JSON.parse(fs.readFileSync('accounts.json', 'utf8')); + +// Accounts to remove +const removeNumbers = [3220, 5120, 7140, 7280]; + +// Suggestions mapping by account number +const suggestionsMap = { + // Variable omkostninger + 2000: ["varer", "indkøb", "lager", "goods", "inventory", "purchase", "varekøb"], + 2050: ["eu", "europa", "germany", "france", "italien", "spain", "netherlands", "eu-erhvervelse"], + 2100: ["eu", "europa", "ydelse", "service", "eu-ydelse"], + 2150: ["usa", "uk", "kina", "china", "verden", "import", "world"], + 2200: ["usa", "uk", "kina", "verden", "service", "ydelse", "world"], + 2250: ["fragt", "levering", "shipping", "delivery", "postnord", "gls", "dao", "bring", "ups", "dhl"], + 2300: ["fragt", "shipping", "delivery", "momsfri"], + 2350: ["fragt", "eu", "shipping", "dhl", "ups", "fedex"], + 2400: ["valuta", "kurs", "currency", "exchange"], + 2450: ["lager", "regulering", "inventory"], + 2800: ["underleverandør", "freelance", "contractor", "subcontractor", "fremmed arbejde"], + + // Lønomkostninger + 3000: ["løn", "salary", "wage", "payroll", "am-indkomst"], + 3020: ["atp", "arbejdsgiver"], + 3040: ["atp", "medarbejder"], + 3060: ["sygepenge", "sickness", "sygedag"], + 3070: ["personalegode", "fri telefon", "benefit", "goder"], + 3080: ["b-honorar", "honorar", "fee"], + 3090: ["barsel", "maternity", "fødsel"], + 3100: ["ferie", "feriepenge", "sh", "vacation", "holiday"], + 3120: ["pension", "pensionsbidrag", "retirement"], + 3140: ["diæt", "rejsegodtgørelse", "per diem", "allowance"], + 3160: ["kørsel", "kilometer", "km", "mileage", "km-godtgørelse", "bil"], + 3180: ["aer", "aes", "atp", "finansieringsbidrag"], + 3200: ["arbejdstøj", "uniform", "workwear", "tøj"], + 3240: ["mad", "kursus", "møde", "food", "catering", "frokost"], + 3260: ["gave", "personale", "gift", "employee"], + 3280: ["kursus", "uddannelse", "course", "training", "efteruddannelse"], + 3300: ["diverse", "personale", "ansatte", "moms"], + 3320: ["diverse", "personale", "ansatte", "momsfri"], + 3340: ["ferie", "regulering", "feriepenge"], + 3355: ["frokost", "frokostordning", "lunch", "kantine"], + + // Salgsomkostninger + 4000: ["annonce", "reklame", "marketing", "ads", "google", "facebook", "linkedin", "meta", "advertising"], + 4020: ["udsmykning", "event", "arrangement", "decoration"], + 4040: ["hotel", "overnatning", "accommodation", "booking.com", "hotels.com", "personale"], + 4060: ["hotel", "overnatning", "kunde", "client", "forretningsforbindelse"], + 4080: ["konference", "conference", "seminar", "event"], + 4100: ["messe", "fair", "exhibition", "udstilling"], + 4120: ["restaurant", "frokost", "middag", "lunch", "dinner", "café", "personale"], + 4140: ["restaurant", "repræsentation", "kunde", "client", "forretningsforbindelse"], + 4160: ["mad", "virksomhed", "forretningsforbindelse", "catering"], + 4180: ["gave", "blomster", "gift", "flowers", "repræsentation"], + 4200: ["repræsentation", "representation"], + 4220: ["repræsentation", "diverse"], + 4240: ["personale", "personnel", "staff"], + 4260: ["ikke-fradrag", "non-deductible"], + 4280: ["rejse", "travel", "tur", "trip"], + + // Lokaleomkostninger + 5000: ["husleje", "leje", "rent", "kontor", "lokale"], + 5010: ["husleje", "leje", "rent", "momsfri"], + 5025: ["el", "strøm", "electricity", "power", "ørsted", "norlys", "ewii", "andel"], + 5030: ["vand", "water", "vandværk"], + 5035: ["varme", "gas", "fjernvarme", "heating", "naturgas"], + 5040: ["elafgift", "afgift", "tax"], + 5045: ["gas", "afgift", "naturgas"], + 5050: ["vand", "afgift", "vandafgift"], + 5060: ["rengøring", "cleaning", "affald", "waste", "renovation"], + 5080: ["reparation", "vedligeholdelse", "maintenance", "repair"], + 5100: ["ejendomsskat", "property tax", "skat"], + 5140: ["mødelokale", "meeting room", "konference"], + 5160: ["dekoration", "decoration", "indretning"], + + // Kørsel og rejser + 6000: ["billeje", "car rental", "leje", "hertz", "avis", "europcar", "sixt", "enterprise"], + 6020: ["benzin", "diesel", "fuel", "brændstof", "ok", "shell", "q8", "circle k", "ingo"], + 6040: ["bil", "vedligeholdelse", "car", "service", "værksted", "dæk"], + 6060: ["vægtafgift", "forsikring", "car insurance", "bilforsikring"], + 6080: ["parkering", "parking", "p-afgift", "easypark", "parkman", "apcoa"], + 6085: ["parkering", "parking", "momsfri"], + 6100: ["bro", "bridge", "storebælt", "øresund", "toll", "afgift"], + 6120: ["taxa", "taxi", "uber", "bolt", "dantaxi", "viggo", "4x27"], + 6140: ["tog", "dsb", "train", "rejsekort", "billet"], + 6160: ["fly", "flight", "sas", "norwegian", "ryanair", "lufthansa", "flybillet"], + 6180: ["bus", "movia", "arriva", "flixbus"], + 6200: ["færge", "ferry", "molslinjen", "scandlines", "bornholmslinjen"], + 6400: ["transport", "diverse", "transportation"], + + // Administrationsomkostninger + 7005: ["revisor", "revision", "audit", "regnskab", "pwc", "deloitte", "ey", "kpmg", "bdo"], + 7010: ["advokat", "lawyer", "legal", "juridisk", "attorney"], + 7020: ["bogføring", "bookkeeping", "regnskab", "bogholder"], + 7040: ["konsulent", "consultant", "rådgivning", "advisory", "rådgiver"], + 7060: ["kontingent", "medlemskab", "membership", "forening"], + 7080: ["kontingent", "medlemskab", "momsfri"], + 7100: ["avis", "newspaper", "berlingske", "politiken", "jyllandsposten", "børsen", "dagblad"], + 7120: ["bog", "book", "litteratur", "amazon", "saxo", "tales", "faglitteratur"], + 7160: ["forsikring", "insurance", "tryg", "topdanmark", "if", "codan", "gjensidige", "alm brand"], + 7180: ["fragt", "kørsel", "shipping", "transport"], + 7200: ["kontor", "office", "papir", "printer", "lyreco", "staples", "kontorartikler"], + 7220: ["porto", "gebyr", "postage", "fee", "postnord"], + 7240: ["telefon", "mobil", "phone", "mobile", "telia", "telenor", "3", "telmore", "cbs", "lebara"], + 7260: ["telefon", "beskatning", "fri telefon"], + 7300: ["internet", "hosting", "domain", "web", "simply", "one.com", "cloudflare", "aws", "azure"], + 7320: ["software", "app", "abonnement", "subscription", "saas", "adobe", "microsoft", "slack", "notion", "google workspace", "dropbox"], + 7360: ["bøde", "gebyr", "fine", "offentlig"], + 7380: ["kasse", "difference", "cash"], + 7400: ["betaling", "payment", "stripe", "mobilepay", "nets", "clearhaus", "paypal"], + 7420: ["indløser", "acquirer", "nets", "clearhaus"], + 7440: ["licens", "license", "rettighed"], + 7460: ["diverse", "moms", "miscellaneous"], + 7480: ["diverse", "momsfri", "miscellaneous"], + 7500: ["generalforsamling", "bestyrelse", "møde", "board", "meeting"], + 7520: ["generalforsamling", "møde", "lokale"], + 7540: ["bestyrelse", "board", "møde", "lokale"], + 7560: ["bestyrelse", "board", "møde", "restaurant"], + + // Afskrivninger + 8040: ["computer", "laptop", "møbler", "furniture", "udstyr", "equipment", "apple", "dell", "lenovo", "småanskaffelse"], + 8050: ["småanskaffelse", "omvendt betalingspligt", "reverse charge"], + + // Renteudgifter + 9200: ["rente", "bank", "interest", "bankrenter"], + 9210: ["rente", "leverandør", "supplier", "interest"], + 9220: ["rente", "ikke-fradrag", "non-deductible"] +}; + +// VAT rubric mapping for EU accounts +const vatRubricMap = { + 2050: "EU_BOX_A_GOODS", + 2100: "EU_BOX_A_SERVICES", + 2350: "EU_BOX_A_GOODS" +}; + +// Region variants mapping +const regionVariants = { + 2000: { category: "varekøb", EU: 2050, WORLD: 2150 }, + 2050: { category: "varekøb", DK: 2000, WORLD: 2150 }, + 2150: { category: "varekøb", DK: 2000, EU: 2050 }, + 2100: { category: "ydelseskøb", WORLD: 2200 }, + 2200: { category: "ydelseskøb", EU: 2100 }, + 2250: { category: "fragt", DK_VATFREE: 2300, EU: 2350 }, + 2300: { category: "fragt", DK: 2250, EU: 2350 }, + 2350: { category: "fragt", DK: 2250, DK_VATFREE: 2300 }, + 5000: { category: "husleje", DK_VATFREE: 5010 }, + 5010: { category: "husleje", DK: 5000 }, + 6080: { category: "parkering", DK_VATFREE: 6085 }, + 6085: { category: "parkering", DK: 6080 } +}; + +// Process accounts +const enrichedAccounts = accounts + .filter(acc => !removeNumbers.includes(acc.number)) + .map(acc => { + // Determine region + let region = "DK"; + if (acc.defaultVatTypeRegion === "EU") { + region = "EU"; + } else if (acc.defaultVatTypeRegion === "World") { + region = "WORLD"; + } + + // Build enriched account + const enriched = { + id: acc.id, + number: acc.number, + name: acc.name, + categoryName: acc.categoryName, + defaultVatCode: acc.defaultVatCode, + region: region, + suggestions: suggestionsMap[acc.number] || [] + }; + + // Add vatRubric for EU accounts + if (vatRubricMap[acc.number]) { + enriched.vatRubric = vatRubricMap[acc.number]; + } + + // Add related accounts for region variants + if (regionVariants[acc.number]) { + enriched.regionVariants = regionVariants[acc.number]; + } + + return enriched; + }); + +// Save output +fs.writeFileSync( + 'accounts-with-suggestions.json', + JSON.stringify(enrichedAccounts, null, 2), + 'utf8' +); + +console.log(`Processed ${enrichedAccounts.length} accounts (removed ${removeNumbers.length})`); +console.log('Saved to accounts-with-suggestions.json'); diff --git a/account-suggestions/test-invoices/Betalingspåmindelse_nr._2_fra_EWII.json b/account-suggestions/test-invoices/Betalingspåmindelse_nr._2_fra_EWII.json new file mode 100644 index 0000000..ca164f5 --- /dev/null +++ b/account-suggestions/test-invoices/Betalingspåmindelse_nr._2_fra_EWII.json @@ -0,0 +1,45 @@ +{ + "vendor": { + "name": "EWII A/S", + "address": "Kokbjerg 30, 6000 Kolding", + "country": "DK", + "vatNumber": "DK28297947" + }, + "invoice": { + "type": "reminder", + "number": null, + "date": "2024-06-06", + "dueDate": "2024-06-17", + "currency": "DKK", + "totalAmount": 3361.04, + "vatAmount": null, + "netAmount": null + }, + "customer": { + "name": "Nicolaj Helmer Hartmann", + "address": "Mejsevej 2, 8370 Hadsten", + "customerNumber": "400143507" + }, + "lineItems": [ + { + "description": "El restance - Rykker 2", + "amount": 3361.04 + } + ], + "suggestedAccount": { + "number": 5025, + "name": "El", + "vatCode": "I25", + "region": "DK", + "confidence": 0.95, + "reasoning": "EWII er el-leverandør, betalingspåmindelse vedrører el-forbrug" + }, + "alternativeAccounts": [ + { + "number": 5040, + "name": "Elafgift", + "confidence": 0.3, + "reasoning": "Kan indeholde elafgift, men primært el-forbrug" + } + ] +} diff --git a/account-suggestions/test-invoices/Betalingspåmindelse_nr._2_fra_EWII.pdf b/account-suggestions/test-invoices/Betalingspåmindelse_nr._2_fra_EWII.pdf new file mode 100644 index 0000000..3d03909 Binary files /dev/null and b/account-suggestions/test-invoices/Betalingspåmindelse_nr._2_fra_EWII.pdf differ diff --git a/account-suggestions/test-invoices/Invoice-44534E99-0004.pdf b/account-suggestions/test-invoices/Invoice-44534E99-0004.pdf new file mode 100644 index 0000000..7cbc6cb Binary files /dev/null and b/account-suggestions/test-invoices/Invoice-44534E99-0004.pdf differ diff --git a/account-suggestions/test-invoices/Invoice-MVPAIAKP-0002.pdf b/account-suggestions/test-invoices/Invoice-MVPAIAKP-0002.pdf new file mode 100644 index 0000000..e175bd9 Binary files /dev/null and b/account-suggestions/test-invoices/Invoice-MVPAIAKP-0002.pdf differ diff --git a/account-suggestions/test-invoices/Microsoft E1 - E0800QGJD1.json b/account-suggestions/test-invoices/Microsoft E1 - E0800QGJD1.json new file mode 100644 index 0000000..dee97f9 --- /dev/null +++ b/account-suggestions/test-invoices/Microsoft E1 - E0800QGJD1.json @@ -0,0 +1,50 @@ +{ + "vendor": { + "name": "Microsoft Ireland Operations Ltd", + "address": "One Microsoft Place, South County Business Park, Leopardstown, Dublin 18, D18 P521, Ireland", + "country": "IE", + "vatNumber": "IE8256796U" + }, + "invoice": { + "type": "invoice", + "number": "E0800QGJD1", + "date": "2024-01-03", + "dueDate": "2024-01-03", + "currency": "DKK", + "totalAmount": 69.90, + "vatAmount": 0.00, + "netAmount": 69.90, + "vatNote": "omvendt betalingspligt" + }, + "customer": { + "name": "Softwarehuset", + "address": "Mejsevej 2, 8370 Hadsten", + "vatNumber": "DK38059726" + }, + "lineItems": [ + { + "description": "Office 365 E1 - Månedligt abonnementsgebyr", + "period": "23-12-2023 - 22-01-2024", + "quantity": 1, + "unitPrice": 69.90, + "amount": 69.90 + } + ], + "suggestedAccount": { + "number": 2100, + "name": "EU-erhvervelser ydelser (rubrik A-ydelser)", + "vatCode": "IEUY", + "region": "EU", + "vatRubric": "EU_BOX_A_SERVICES", + "confidence": 0.97, + "reasoning": "Software/cloud-service fra EU (Irland) med omvendt betalingspligt - skal bogføres som EU-ydelseskøb" + }, + "alternativeAccounts": [ + { + "number": 7320, + "name": "Køb af software", + "confidence": 0.4, + "reasoning": "Ville være korrekt hvis leverandør var dansk, men EU-leverandør kræver EU-konto pga. momsindberetning" + } + ] +} diff --git a/account-suggestions/test-invoices/Microsoft E1 - E0800QGJD1.pdf b/account-suggestions/test-invoices/Microsoft E1 - E0800QGJD1.pdf new file mode 100644 index 0000000..da98696 Binary files /dev/null and b/account-suggestions/test-invoices/Microsoft E1 - E0800QGJD1.pdf differ diff --git a/account-suggestions/test-invoices/VACA_Mejlgade_2025-08-27_09_10_40.768_68aecb9075793e5d20cd70cc.pdf b/account-suggestions/test-invoices/VACA_Mejlgade_2025-08-27_09_10_40.768_68aecb9075793e5d20cd70cc.pdf new file mode 100644 index 0000000..967de57 Binary files /dev/null and b/account-suggestions/test-invoices/VACA_Mejlgade_2025-08-27_09_10_40.768_68aecb9075793e5d20cd70cc.pdf differ diff --git a/account-suggestions/test-invoices/Wolt+_2025-03-21_13_05_07.830_67dd64034768185bed932167.pdf b/account-suggestions/test-invoices/Wolt+_2025-03-21_13_05_07.830_67dd64034768185bed932167.pdf new file mode 100644 index 0000000..2697a59 Binary files /dev/null and b/account-suggestions/test-invoices/Wolt+_2025-03-21_13_05_07.830_67dd64034768185bed932167.pdf differ diff --git a/account-suggestions/test-invoices/e90191aa-5f53-4aaf-bd70-298420938495.pdf b/account-suggestions/test-invoices/e90191aa-5f53-4aaf-bd70-298420938495.pdf new file mode 100644 index 0000000..071c153 Binary files /dev/null and b/account-suggestions/test-invoices/e90191aa-5f53-4aaf-bd70-298420938495.pdf differ diff --git a/account-suggestions/test-invoices/fibia_receipt_72147924.json b/account-suggestions/test-invoices/fibia_receipt_72147924.json new file mode 100644 index 0000000..d7f5a0f --- /dev/null +++ b/account-suggestions/test-invoices/fibia_receipt_72147924.json @@ -0,0 +1,65 @@ +{ + "vendor": { + "name": "Waoo (Fibia P/S)", + "address": "Energivej 33, 4690 Haslev", + "country": "DK", + "vatNumber": "DK36058552" + }, + "invoice": { + "type": "invoice", + "number": "08338955", + "date": "2025-08-07", + "dueDate": "2025-09-01", + "currency": "DKK", + "totalAmount": 1284.00, + "vatAmount": 256.80, + "netAmount": 1027.20 + }, + "customer": { + "name": "Nicolaj Hartmann", + "address": "Mejsevej 2, Hadbjerg, 8370 Hadsten", + "customerNumber": "1299453", + "contractNumber": "861418" + }, + "lineItems": [ + { + "description": "Waoo Fiber 1000", + "period": "01.09.2025 - 30.09.2025", + "amount": 389.00 + }, + { + "description": "Waoo Fiber 1000", + "period": "01.10.2025 - 31.10.2025", + "amount": 389.00 + }, + { + "description": "Waoo Fiber 1000", + "period": "01.11.2025 - 30.11.2025", + "amount": 389.00 + }, + { + "description": "Abonnement Fast IP adresse", + "period": "01.09.2025 - 30.09.2025", + "amount": 39.00 + }, + { + "description": "Abonnement Fast IP adresse", + "period": "01.10.2025 - 31.10.2025", + "amount": 39.00 + }, + { + "description": "Abonnement Fast IP adresse", + "period": "01.11.2025 - 30.11.2025", + "amount": 39.00 + } + ], + "suggestedAccount": { + "number": 7300, + "name": "Internet og webhotel", + "vatCode": "I25", + "region": "DK", + "confidence": 0.98, + "reasoning": "Fiber internet abonnement fra dansk udbyder med 25% moms" + }, + "alternativeAccounts": [] +} diff --git a/account-suggestions/test-invoices/fibia_receipt_72147924.pdf b/account-suggestions/test-invoices/fibia_receipt_72147924.pdf new file mode 100644 index 0000000..64ed95f Binary files /dev/null and b/account-suggestions/test-invoices/fibia_receipt_72147924.pdf differ diff --git a/account-suggestions/test-invoices/invoice (4).pdf b/account-suggestions/test-invoices/invoice (4).pdf new file mode 100644 index 0000000..a43fc5a Binary files /dev/null and b/account-suggestions/test-invoices/invoice (4).pdf differ diff --git a/account-suggestions/test-invoices/invoice-F175490.pdf b/account-suggestions/test-invoices/invoice-F175490.pdf new file mode 100644 index 0000000..cbf9a38 Binary files /dev/null and b/account-suggestions/test-invoices/invoice-F175490.pdf differ diff --git a/account-suggestions/test-invoices/invoice-F191023.pdf b/account-suggestions/test-invoices/invoice-F191023.pdf new file mode 100644 index 0000000..96a51da Binary files /dev/null and b/account-suggestions/test-invoices/invoice-F191023.pdf differ diff --git a/account-suggestions/test-invoices/parking_receipt_2582441883.pdf b/account-suggestions/test-invoices/parking_receipt_2582441883.pdf new file mode 100644 index 0000000..e39b794 Binary files /dev/null and b/account-suggestions/test-invoices/parking_receipt_2582441883.pdf differ diff --git a/account-suggestions/test-invoices/receipt.png b/account-suggestions/test-invoices/receipt.png new file mode 100644 index 0000000..88037ff Binary files /dev/null and b/account-suggestions/test-invoices/receipt.png differ diff --git a/backend/BACKEND_REQUIREMENTS.md b/backend/BACKEND_REQUIREMENTS.md new file mode 100644 index 0000000..7ab1264 --- /dev/null +++ b/backend/BACKEND_REQUIREMENTS.md @@ -0,0 +1,355 @@ +# Backend Krav - Dansk Bogføringssystem + +## Overblik + +Dette dokument beskriver backend-kravene for det danske bogføringssystem. Frontend er implementeret i React/TypeScript med Ant Design og Zustand state management. + +--- + +## 1. Regnskabsår (Fiscal Years) + +### 1.1 Data Model + +```typescript +interface FiscalYear { + id: string; + companyId: string; + name: string; // "2025" eller "2024/2025" + startDate: string; // ISO date "YYYY-MM-DD" + endDate: string; // ISO date "YYYY-MM-DD" + status: 'open' | 'closed' | 'locked'; + openingBalancePosted: boolean; + closingDate?: string; // Når året blev lukket + closedBy?: string; // Bruger ID + createdAt: string; + updatedAt: string; +} +``` + +### 1.2 API Endpoints + +| Endpoint | Metode | Beskrivelse | +|----------|--------|-------------| +| `/api/fiscal-years` | GET | Hent alle regnskabsår for aktiv virksomhed | +| `/api/fiscal-years` | POST | Opret nyt regnskabsår | +| `/api/fiscal-years/:id` | GET | Hent specifikt regnskabsår | +| `/api/fiscal-years/:id` | PATCH | Opdater regnskabsår | +| `/api/fiscal-years/:id/close` | POST | Luk regnskabsår (årsafslutning) | +| `/api/fiscal-years/:id/reopen` | POST | Genåbn lukket regnskabsår | +| `/api/fiscal-years/:id/lock` | POST | Lås regnskabsår permanent | + +### 1.3 Forretningsregler + +#### Oprettelse +- Nye regnskabsår må IKKE overlappe med eksisterende +- Regnskabsår der "rører" ved grænsen (slutter/starter samme dag) er TILLADT +- Understøt både kalenderår (jan-dec) og skæve regnskabsår (fx jul-jun) +- Valider at regnskabsåret er mellem 300-400 dage (advarsel, ikke fejl) + +#### Årsafslutning (Year-End Closing) +**KRITISK**: Ved årsafslutning skal backend: + +1. **Validere** at regnskabsåret kan lukkes: + - Ikke allerede låst + - Advar om åbne perioder (men tillad lukning) + - Advar om ikke-afstemte transaktioner + +2. **Oprette lukkeposter** (closing entries): + - Luk alle indtægtskonti til resultatoverførselskonto + - Luk alle udgiftskonti til resultatoverførselskonto + - Gem som normale transaktioner med særlig markering (`isClosingEntry: true`) + +3. **Generere åbningsbalancer** til næste regnskabsår: + - Beregn slutsaldo for alle balancekonti (aktiver, passiver, egenkapital) + - Nulstil resultatkonti (indtægter, udgifter, vareforbrug, personale, finansielle) + - Opret åbningsposteringer i næste regnskabsår (`isOpeningBalance: true`) + +4. **Opdatere status**: + - Sæt regnskabsår status til 'closed' eller 'locked' + - Luk alle tilhørende regnskabsperioder + - Sæt `openingBalancePosted: true` på næste regnskabsår + +#### Genåbning +- Kun 'closed' regnskabsår kan genåbnes (ikke 'locked') +- Ved genåbning skal åbningsbalancer i næste år opdateres automatisk +- Log hvem der genåbnede og hvornår + +### 1.4 Dynamiske Åbningsbalancer + +Frontend forventer at åbningsbalancer opdateres automatisk når der bogføres i et tidligere år (som Dinero): + +``` +Scenarie: +1. Regnskabsår 2024 er lukket, 2025 er åbent +2. Revisor finder fejl og bogfører korrektion i 2024 +3. Backend skal automatisk genberegne åbningsbalancen for 2025 +``` + +--- + +## 2. Regnskabsperioder (Accounting Periods) + +### 2.1 Data Model + +```typescript +interface AccountingPeriod { + id: string; + fiscalYearId: string; + companyId: string; + name: string; // "Januar 2025", "Q1 2025" + periodNumber: number; // 1-12 for månedlig + startDate: string; + endDate: string; + status: 'future' | 'open' | 'closed' | 'locked'; + closedAt?: string; + closedBy?: string; + reopenedAt?: string; + reopenedBy?: string; + lockedAt?: string; + lockedBy?: string; + createdAt: string; + updatedAt: string; +} + +type PeriodFrequency = 'monthly' | 'quarterly' | 'half-yearly' | 'yearly'; +``` + +### 2.2 API Endpoints + +| Endpoint | Metode | Beskrivelse | +|----------|--------|-------------| +| `/api/periods` | GET | Hent perioder (filtrer på fiscalYearId) | +| `/api/periods/:id` | PATCH | Opdater periode | +| `/api/periods/:id/close` | POST | Luk periode | +| `/api/periods/:id/reopen` | POST | Genåbn periode | +| `/api/periods/:id/lock` | POST | Lås periode | + +### 2.3 Forretningsregler + +- Perioder genereres automatisk ved oprettelse af regnskabsår +- Understøt: månedlig (12), kvartalsvis (4), halvårlig (2), årlig (1) +- Perioder kan kun lukkes i rækkefølge (periode 2 kræver periode 1 lukket) +- Låste perioder kan IKKE genåbnes + +--- + +## 3. Transaktioner med Periode-Reference + +### 3.1 Udvidet Transaction Model + +```typescript +interface Transaction { + id: string; + companyId: string; + fiscalYearId: string; // PÅKRÆVET - hvilket regnskabsår + periodId: string; // PÅKRÆVET - hvilken periode + journalEntryNumber: number; + date: string; + description: string; + reference?: string; + lines: TransactionLine[]; + isVoided: boolean; + isReconciled: boolean; + isClosingEntry?: boolean; // NY - markering for lukkepost + isOpeningBalance?: boolean; // NY - markering for åbningsbalance + createdAt: string; + updatedAt: string; + createdBy: string; +} +``` + +### 3.2 Forretningsregler for Bogføring + +```typescript +interface PostingValidation { + allowed: boolean; + reason?: string; + reasonDanish?: string; +} +``` + +Backend skal validere ved bogføring: +1. Find periode for transaktionsdato +2. Tjek om perioden er åben +3. Tjek om regnskabsåret er åbent +4. Returner fejl med dansk besked hvis ikke tilladt + +--- + +## 4. Kontoplan (Chart of Accounts) + +### 4.1 Data Model + +```typescript +interface Account { + id: string; + companyId: string; + accountNumber: string; // "1000", "3900" + name: string; + type: AccountType; + parentId?: string; + description?: string; + vatCodeId?: string; + isActive: boolean; + isSystemAccount: boolean; // Kan ikke slettes + balance: number; // Beregnet felt + createdAt: string; + updatedAt: string; +} + +type AccountType = + | 'asset' // Aktiver + | 'liability' // Passiver + | 'equity' // Egenkapital + | 'revenue' // Indtægter + | 'cogs' // Vareforbrug + | 'expense' // Udgifter + | 'personnel' // Personaleomkostninger + | 'financial' // Finansielle poster + | 'extraordinary'; // Ekstraordinære poster +``` + +### 4.2 Systemkonti + +Følgende konti skal oprettes automatisk: +- `3900` - Overført resultat (equity) - bruges til årsafslutning +- Åbningsbalancekonto for primo-posteringer + +--- + +## 5. Moms (VAT) + +### 5.1 Data Model + +```typescript +interface VATCode { + id: string; + companyId: string; + code: string; // "S25", "K25" + name: string; + rate: number; // 0.25 for 25% + type: 'sales' | 'purchase' | 'eu_sales' | 'eu_purchase' | 'reverse_charge'; + accountId: string; // Momskonto + isActive: boolean; +} + +interface VATPeriod { + id: string; + companyId: string; + startDate: string; + endDate: string; + dueDate: string; // Indberetningsfrist + status: 'open' | 'submitted' | 'paid'; + salesVAT: number; // Beregnet + purchaseVAT: number; // Beregnet + netVAT: number; // salesVAT - purchaseVAT + submittedAt?: string; + paidAt?: string; +} +``` + +### 5.2 SKAT Integration (Fremtidig) + +- Momsindberetning til SKAT via NemVirksomhed API +- Kvartalsvise eller månedlige momsperioder baseret på virksomhedsstørrelse + +--- + +## 6. Virksomhed (Company) + +### 6.1 Data Model + +```typescript +interface Company { + id: string; + name: string; + cvr: string; // Dansk CVR-nummer + address?: string; + postalCode?: string; + city?: string; + country: string; // Default "DK" + fiscalYearStart: number; // Måned 1-12 (default 1 for januar) + currency: string; // Default "DKK" + vatRegistered: boolean; + vatPeriodFrequency: 'monthly' | 'quarterly' | 'half-yearly'; + createdAt: string; + updatedAt: string; +} +``` + +--- + +## 7. Fejlhåndtering + +Alle fejlbeskeder skal være tilgængelige på både engelsk og dansk: + +```typescript +interface APIError { + code: string; + message: string; + messageDanish: string; + details?: Record; +} +``` + +### Eksempler på fejlkoder + +| Kode | Engelsk | Dansk | +|------|---------|-------| +| `PERIOD_LOCKED` | Period is locked | Perioden er låst | +| `FISCAL_YEAR_LOCKED` | Fiscal year is locked | Regnskabsåret er låst | +| `OVERLAP_EXISTS` | Overlaps with existing fiscal year | Overlapper med eksisterende regnskabsår | +| `UNBALANCED_ENTRY` | Debit and credit must be equal | Debet og kredit skal være ens | +| `NO_COMPANY_SELECTED` | No company selected | Ingen virksomhed valgt | + +--- + +## 8. Prioriteret Implementeringsrækkefølge + +### Fase 1: Grundlæggende (MVP) +1. Company CRUD +2. Account CRUD med standard kontoplan +3. Transaction CRUD med validering +4. FiscalYear CRUD + +### Fase 2: Periode-management +1. AccountingPeriod generering og CRUD +2. Periode-validering ved bogføring +3. Periode lukning/åbning + +### Fase 3: Årsafslutning +1. Closing entries generering og bogføring +2. Opening balance beregning og bogføring +3. Dynamisk åbningsbalance-opdatering + +### Fase 4: Moms +1. VATCode CRUD +2. VATPeriod generering +3. Momsberegning og -rapportering + +--- + +## 9. Tekniske Krav + +### Authentication +- JWT-baseret authentication +- Multi-tenant support (bruger kan have adgang til flere virksomheder) + +### Database +- PostgreSQL anbefales +- Soft delete på alle entiteter +- Audit trail (createdBy, updatedBy, deletedBy) + +### API Format +- RESTful JSON API +- GraphQL som alternativ (valgfrit) +- Pagination på liste-endpoints +- Filtering og sorting support + +### Valuta +- Alle beløb i øre/cents (integers) for at undgå floating point problemer +- Frontend konverterer til/fra DKK ved visning + +--- + +*Sidst opdateret: 17. januar 2026* diff --git a/backend/Books.Api.Tests/Books.Api.Tests.csproj b/backend/Books.Api.Tests/Books.Api.Tests.csproj new file mode 100644 index 0000000..883f366 --- /dev/null +++ b/backend/Books.Api.Tests/Books.Api.Tests.csproj @@ -0,0 +1,37 @@ + + + + net10.0 + 14 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Books.Api.Tests/GraphQL/CompanyGraphQLTests.cs b/backend/Books.Api.Tests/GraphQL/CompanyGraphQLTests.cs new file mode 100644 index 0000000..612046a --- /dev/null +++ b/backend/Books.Api.Tests/GraphQL/CompanyGraphQLTests.cs @@ -0,0 +1,260 @@ +using Books.Api.Domain.Companies; +using Books.Api.EventFlow.Repositories; +using Books.Api.Tests.Helpers; +using Books.Api.Tests.Infrastructure; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; + +namespace Books.Api.Tests.GraphQL; + +/// +/// Integration tests for Company GraphQL operations. +/// Each test class runs with its own isolated database. +/// +[Trait("Category", "Integration")] +public class CompanyGraphQLTests(TestWebApplicationFactory factory) + : IntegrationTestBase(factory) +{ + [Fact] + public async Task Query_Companies_ReturnsEmptyList_WhenNoCompaniesExist() + { + // Arrange + var graphqlClient = new GraphQLTestClient(Client); + + // Act + var response = await graphqlClient.QueryAsync(""" + query { + companies { + id + name + } + } + """); + + // Assert + response.EnsureNoErrors(); + response.Data.Should().NotBeNull(); + response.Data!.Companies.Should().BeEmpty(); + } + + [Fact] + public async Task Mutation_CreateCompany_CreatesCompanySuccessfully() + { + // Arrange + var graphqlClient = new GraphQLTestClient(Client); + + // Act + var response = await graphqlClient.MutateAsync(""" + mutation CreateCompany($input: CreateCompanyInput!) { + createCompany(input: $input) { + id + name + cvr + country + currency + fiscalYearStartMonth + } + } + """, + new + { + input = new + { + name = "Test Virksomhed A/S", + cvr = "12345678", + country = "DK", + currency = "DKK", + fiscalYearStartMonth = 1 + } + }); + + // Assert + response.EnsureNoErrors(); + response.Data.Should().NotBeNull(); + response.Data!.CreateCompany.Should().NotBeNull(); + response.Data.CreateCompany!.Name.Should().Be("Test Virksomhed A/S"); + response.Data.CreateCompany.Cvr.Should().Be("12345678"); + response.Data.CreateCompany.Country.Should().Be("DK"); + response.Data.CreateCompany.Currency.Should().Be("DKK"); + response.Data.CreateCompany.FiscalYearStartMonth.Should().Be(1); + } + + [Fact] + public async Task Query_Company_ReturnsCompany_AfterCreation() + { + // Arrange + var graphqlClient = new GraphQLTestClient(Client); + + // Create a company first + var createResponse = await graphqlClient.MutateAsync(""" + mutation CreateCompany($input: CreateCompanyInput!) { + createCompany(input: $input) { + id + name + } + } + """, + new + { + input = new + { + name = "Query Test Virksomhed", + cvr = "87654321" + } + }); + + createResponse.EnsureNoErrors(); + var companyId = createResponse.Data!.CreateCompany!.Id; + + // Act - Query the company by ID (with eventual consistency) + var company = await Eventually.GetAsync(async () => + { + var response = await graphqlClient.QueryAsync(""" + query GetCompany($id: ID!) { + company(id: $id) { + id + name + cvr + } + } + """, + new { id = companyId }); + + return response.Data?.Company; + }); + + // Assert + company.Should().NotBeNull(); + company!.Name.Should().Be("Query Test Virksomhed"); + company.Cvr.Should().Be("87654321"); + } + + [Fact] + public async Task Query_Companies_ReturnsAllCompanies_AfterMultipleCreations() + { + // Arrange + var graphqlClient = new GraphQLTestClient(Client); + + // Create multiple companies with unique names for this test + var company1Name = $"ListTest-Virksomhed-{Guid.NewGuid():N}"; + var company2Name = $"ListTest-Virksomhed-{Guid.NewGuid():N}"; + + await graphqlClient.MutateAsync($$""" + mutation { createCompany(input: { name: "{{company1Name}}" }) { id } } + """); + + await graphqlClient.MutateAsync($$""" + mutation { createCompany(input: { name: "{{company2Name}}" }) { id } } + """); + + // Act - Wait for eventual consistency (at least 2 companies with our prefix) + var companies = await Eventually.GetAsync( + async () => + { + var response = await graphqlClient.QueryAsync(""" + query { + companies { + id + name + } + } + """); + var allCompanies = response.Data?.Companies ?? []; + var ourCompanies = allCompanies.Where(c => c.Name.StartsWith("ListTest-")).ToList(); + return ourCompanies.Count >= 2 ? ourCompanies : null; + }); + + // Assert - verify our specific companies are present + companies.Should().Contain(c => c.Name == company1Name); + companies.Should().Contain(c => c.Name == company2Name); + } + + [Fact] + public async Task Mutation_UpdateCompany_UpdatesCompanySuccessfully() + { + // Arrange + var graphqlClient = new GraphQLTestClient(Client); + + // Create a company first + var createResponse = await graphqlClient.MutateAsync(""" + mutation { createCompany(input: { name: "Original Name" }) { id } } + """); + + createResponse.EnsureNoErrors(); + var companyId = createResponse.Data!.CreateCompany!.Id; + + // Wait for the company to be created + await Eventually.GetAsync(async () => + { + var repository = GetService(); + return await repository.GetByIds([CompanyId.With(companyId)]); + }); + + // Act - Update the company + var updateResponse = await graphqlClient.MutateAsync(""" + mutation UpdateCompany($id: ID!, $input: UpdateCompanyInput!) { + updateCompany(id: $id, input: $input) { + id + name + cvr + } + } + """, + new + { + id = companyId, + input = new + { + name = "Updated Name", + cvr = "11111111" + } + }); + + // Assert + updateResponse.EnsureNoErrors(); + + // Wait for eventual consistency and verify update + var updatedCompany = await Eventually.GetAsync(async () => + { + var repository = GetService(); + var companies = await repository.GetByIds([CompanyId.With(companyId)]); + var company = companies.First(); + return company?.Name == "Updated Name" ? company : null; + }); + + updatedCompany.Should().NotBeNull(); + updatedCompany!.Name.Should().Be("Updated Name"); + updatedCompany.Cvr.Should().Be("11111111"); + } + + // Response DTOs for deserialization + private class CompaniesResponse + { + public List Companies { get; set; } = []; + } + + private class CompanyResponse + { + public CompanyDto? Company { get; set; } + } + + private class CreateCompanyResponse + { + public CompanyDto? CreateCompany { get; set; } + } + + private class UpdateCompanyResponse + { + public CompanyDto? UpdateCompany { get; set; } + } + + private class CompanyDto + { + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? Cvr { get; set; } + public string? Country { get; set; } + public string? Currency { get; set; } + public int? FiscalYearStartMonth { get; set; } + } +} diff --git a/backend/Books.Api.Tests/Helpers/Eventually.cs b/backend/Books.Api.Tests/Helpers/Eventually.cs new file mode 100644 index 0000000..ec59ccc --- /dev/null +++ b/backend/Books.Api.Tests/Helpers/Eventually.cs @@ -0,0 +1,89 @@ +namespace Books.Api.Tests.Helpers; + +/// +/// Helper for testing eventually consistent systems. +/// Polls a condition until it's met or times out. +/// +public static class Eventually +{ + /// + /// Polls a condition until it returns true or times out. + /// + public static async Task AssertAsync( + Func> condition, + TimeSpan? timeout = null, + TimeSpan? pollInterval = null, + string? failMessage = null) + { + timeout ??= TimeSpan.FromSeconds(5); + pollInterval ??= TimeSpan.FromMilliseconds(50); + + var start = DateTime.UtcNow; + + while (DateTime.UtcNow - start < timeout) + { + if (await condition()) + return; + + await Task.Delay(pollInterval.Value); + } + + throw new TimeoutException( + failMessage ?? $"Condition was not met within {timeout}."); + } + + /// + /// Polls until a non-null result is returned or times out. + /// + public static async Task GetAsync( + Func> getter, + TimeSpan? timeout = null, + TimeSpan? pollInterval = null, + string? failMessage = null) where T : class + { + timeout ??= TimeSpan.FromSeconds(10); + pollInterval ??= TimeSpan.FromMilliseconds(100); + + var start = DateTime.UtcNow; + + while (DateTime.UtcNow - start < timeout) + { + var result = await getter(); + if (result != null) + return result; + + await Task.Delay(pollInterval.Value); + } + + throw new TimeoutException( + failMessage ?? $"Expected non-null result was not returned within {timeout}."); + } + + /// + /// Polls until a collection has the expected count or times out. + /// + public static async Task> GetListAsync( + Func>> getter, + int expectedCount, + TimeSpan? timeout = null, + TimeSpan? pollInterval = null, + string? failMessage = null) + { + timeout ??= TimeSpan.FromSeconds(10); + pollInterval ??= TimeSpan.FromMilliseconds(100); + + var start = DateTime.UtcNow; + + while (DateTime.UtcNow - start < timeout) + { + var result = await getter(); + if (result.Count >= expectedCount) + return result; + + await Task.Delay(pollInterval.Value); + } + + throw new TimeoutException( + failMessage ?? $"Expected {expectedCount} items but condition was not met within {timeout}."); + } +} diff --git a/backend/Books.Api.Tests/Helpers/GraphQLTestClient.cs b/backend/Books.Api.Tests/Helpers/GraphQLTestClient.cs new file mode 100644 index 0000000..0c092ce --- /dev/null +++ b/backend/Books.Api.Tests/Helpers/GraphQLTestClient.cs @@ -0,0 +1,125 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Books.Api.Tests.Helpers; + +/// +/// Helper client for executing GraphQL queries and mutations in tests. +/// +public class GraphQLTestClient(HttpClient httpClient) +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + /// + /// Executes a GraphQL query and returns the typed result. + /// + public async Task> QueryAsync( + string query, + object? variables = null, + CancellationToken cancellationToken = default) + { + return await ExecuteAsync(query, variables, cancellationToken); + } + + /// + /// Executes a GraphQL mutation and returns the typed result. + /// + public async Task> MutateAsync( + string mutation, + object? variables = null, + CancellationToken cancellationToken = default) + { + return await ExecuteAsync(mutation, variables, cancellationToken); + } + + /// + /// Executes a GraphQL operation and returns the raw JSON response. + /// + public async Task ExecuteRawAsync( + string query, + object? variables = null, + CancellationToken cancellationToken = default) + { + var request = new GraphQLRequest(query, variables); + var json = JsonSerializer.Serialize(request, JsonOptions); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync("/graphql", content, cancellationToken); + return await response.Content.ReadAsStringAsync(cancellationToken); + } + + private async Task> ExecuteAsync( + string query, + object? variables, + CancellationToken cancellationToken) + { + var request = new GraphQLRequest(query, variables); + var json = JsonSerializer.Serialize(request, JsonOptions); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync("/graphql", content, cancellationToken); + var responseJson = await response.Content.ReadAsStringAsync(cancellationToken); + + var result = JsonSerializer.Deserialize>(responseJson, JsonOptions); + return result ?? throw new InvalidOperationException("Failed to deserialize GraphQL response"); + } +} + +public record GraphQLRequest( + [property: JsonPropertyName("query")] string Query, + [property: JsonPropertyName("variables")] object? Variables = null); + +public class GraphQLResponse +{ + [JsonPropertyName("data")] + public T? Data { get; set; } + + [JsonPropertyName("errors")] + public List? Errors { get; set; } + + public bool HasErrors => Errors?.Count > 0; + + public void EnsureNoErrors() + { + if (HasErrors) + { + var errorDetails = Errors!.Select(e => + { + var msg = e.Message; + if (e.Extensions != null) + { + foreach (var ext in e.Extensions) + { + msg += $"{Environment.NewLine} [{ext.Key}]: {ext.Value}"; + } + } + return msg; + }); + var messages = string.Join(Environment.NewLine, errorDetails); + throw new GraphQLException($"GraphQL errors occurred:{Environment.NewLine}{messages}", Errors!); + } + } +} + +public class GraphQLError +{ + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + [JsonPropertyName("path")] + public List? Path { get; set; } + + [JsonPropertyName("extensions")] + public Dictionary? Extensions { get; set; } +} + +public class GraphQLException(string message, List errors) : Exception(message) +{ + public List Errors { get; } = errors; +} diff --git a/backend/Books.Api.Tests/Infrastructure/IntegrationTestBase.cs b/backend/Books.Api.Tests/Infrastructure/IntegrationTestBase.cs new file mode 100644 index 0000000..17e5866 --- /dev/null +++ b/backend/Books.Api.Tests/Infrastructure/IntegrationTestBase.cs @@ -0,0 +1,38 @@ +using EventFlow; +using EventFlow.Aggregates; +using Microsoft.Extensions.DependencyInjection; + +namespace Books.Api.Tests.Infrastructure; + +/// +/// Base class for integration tests with isolated database per test class. +/// Uses IClassFixture pattern - each test class gets its own database. +/// +public abstract class IntegrationTestBase + : IClassFixture, IDisposable +{ + protected readonly TestWebApplicationFactory Factory; + protected readonly HttpClient Client; + private readonly IServiceScope _scope; + + protected IntegrationTestBase(TestWebApplicationFactory factory) + { + Factory = factory; + Client = factory.CreateGraphQLClient(); + _scope = factory.Services.CreateScope(); + } + + protected IServiceProvider Services => _scope.ServiceProvider; + + protected ICommandBus CommandBus => Services.GetRequiredService(); + protected IAggregateStore AggregateStore => Services.GetRequiredService(); + + protected T GetService() where T : notnull + => Services.GetRequiredService(); + + public void Dispose() + { + _scope?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/backend/Books.Api.Tests/Infrastructure/TestDatabase.cs b/backend/Books.Api.Tests/Infrastructure/TestDatabase.cs new file mode 100644 index 0000000..b2fe42a --- /dev/null +++ b/backend/Books.Api.Tests/Infrastructure/TestDatabase.cs @@ -0,0 +1,121 @@ +using Npgsql; + +namespace Books.Api.Tests.Infrastructure; + +/// +/// Creates an isolated PostgreSQL database for each test run. +/// The database is automatically dropped when disposed. +/// +public class TestDatabase : IDisposable +{ + public const string DefaultConnectionString = + "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=postgres;Include Error Detail=true"; + + private readonly string _masterConnectionString; + private bool _disposed; + + public string DatabaseName { get; } + + public string ConnectionString + { + get + { + var builder = new NpgsqlConnectionStringBuilder(_masterConnectionString) + { + Database = DatabaseName + }; + return builder.ToString(); + } + } + + public TestDatabase(string? masterConnectionString = null) + { + _masterConnectionString = masterConnectionString ?? GetConnectionStringFromEnvironment(); + DatabaseName = $"books_test_{Guid.NewGuid():N}"; + + CreateDatabase(); + } + + private static string GetConnectionStringFromEnvironment() + { + return Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING") + ?? DefaultConnectionString; + } + + private void CreateDatabase() + { + try + { + using var connection = new NpgsqlConnection(_masterConnectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = $"CREATE DATABASE \"{DatabaseName}\""; + command.ExecuteNonQuery(); + + Console.WriteLine($"[TestDatabase] Created test database: {DatabaseName}"); + } + catch (Exception e) + { + throw new InvalidOperationException( + $"Failed to create test database '{DatabaseName}' using connection string: {_masterConnectionString}", + e); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + + if (disposing) + { + try + { + // Step 1: Terminate all active connections to prevent "database in use" errors + using (var connection = new NpgsqlConnection(_masterConnectionString)) + { + connection.Open(); + + using var terminateCommand = connection.CreateCommand(); + terminateCommand.CommandText = $@" + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '{DatabaseName}' + AND pid <> pg_backend_pid(); + "; + terminateCommand.ExecuteNonQuery(); + } + + // Step 2: Drop the database + using (var connection = new NpgsqlConnection(_masterConnectionString)) + { + connection.Open(); + + using var dropCommand = connection.CreateCommand(); + dropCommand.CommandText = $"DROP DATABASE IF EXISTS \"{DatabaseName}\""; + dropCommand.ExecuteNonQuery(); + } + + Console.WriteLine($"[TestDatabase] Dropped test database: {DatabaseName}"); + } + catch (Exception e) + { + // Log but don't throw - we don't want cleanup failures to mask test failures + Console.WriteLine($"[TestDatabase] Warning: Failed to drop test database {DatabaseName}: {e.Message}"); + } + } + + _disposed = true; + } + + ~TestDatabase() + { + Dispose(false); + } +} diff --git a/backend/Books.Api.Tests/Infrastructure/TestWebApplicationFactory.cs b/backend/Books.Api.Tests/Infrastructure/TestWebApplicationFactory.cs new file mode 100644 index 0000000..2660be9 --- /dev/null +++ b/backend/Books.Api.Tests/Infrastructure/TestWebApplicationFactory.cs @@ -0,0 +1,146 @@ +using Books.Api.EventFlow.Infrastructure; +using EventFlow.PostgreSql; +using EventFlow.PostgreSql.Connections; +using EventFlow.PostgreSql.EventStores; +using Hangfire; +using Hangfire.InMemory; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Npgsql; + +namespace Books.Api.Tests.Infrastructure; + +/// +/// WebApplicationFactory configured for integration testing with: +/// - Isolated database per test class +/// - In-memory Hangfire (no real job processing) +/// - Full GraphQL endpoint support +/// +public class TestWebApplicationFactory : WebApplicationFactory +{ + static TestWebApplicationFactory() + { + // Enable legacy timestamp behavior for Npgsql 6.0+ + // Must be set before any Npgsql connections are created + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + } + + private readonly TestDatabase _testDatabase = new(); + + public string TestDatabaseName => _testDatabase.DatabaseName; + public string ConnectionString => _testDatabase.ConnectionString; + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + // Set environment to Test to avoid Development-only middleware (like Altair) + builder.UseEnvironment("Test"); + + builder.ConfigureAppConfiguration((context, config) => + { + // Clear all default configuration sources + config.Sources.Clear(); + + // Add test configuration with isolated database + var testConfig = new Dictionary + { + ["ConnectionStrings:Default"] = _testDatabase.ConnectionString, + ["AllowedHosts"] = "*" + }; + + config.AddInMemoryCollection(testConfig); + + Console.WriteLine($"[TestWebApplicationFactory] Using test database: {_testDatabase.DatabaseName}"); + Console.WriteLine($"[TestWebApplicationFactory] Connection string: {_testDatabase.ConnectionString}"); + }); + + builder.ConfigureServices(services => + { + // Run database migrations on the test database BEFORE services are built + // This ensures the test database has the correct schema + MigrateTestDatabase(); + + // Replace the NpgsqlDataSource to use the test database + services.RemoveAll(); + services.AddSingleton(new NpgsqlDataSourceBuilder(_testDatabase.ConnectionString).Build()); + + // CRITICAL: Replace EventFlow's PostgreSQL configuration with test connection string + // This ensures the event store uses the test database, not the original + services.RemoveAll(); + services.AddSingleton( + PostgreSqlConfiguration.New.SetConnectionString(_testDatabase.ConnectionString)); + + // Replace only Hangfire storage (not EventFlow.Hangfire integration services) + // We keep EventFlow.Hangfire services like IQueueNameProvider, HangfireJobScheduler + var hangfireStorageDescriptors = services + .Where(d => + d.ServiceType.FullName?.Contains("Hangfire.JobStorage") == true || + d.ServiceType.FullName?.Contains("Hangfire.Server.BackgroundJobServer") == true || + d.ImplementationType?.FullName?.Contains("Hangfire.PostgreSql") == true) + .ToList(); + + foreach (var descriptor in hangfireStorageDescriptors) + { + services.Remove(descriptor); + } + + // Add Hangfire with in-memory storage + services.AddHangfire(config => config + .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseInMemoryStorage()); + + services.AddHangfireServer(options => + { + options.WorkerCount = 1; + options.Queues = ["default"]; + }); + }); + } + + protected override IHost CreateHost(IHostBuilder builder) + { + var host = base.CreateHost(builder); + + // Run EventFlow PostgreSQL migrations after host is built + using var scope = host.Services.CreateScope(); + var eventFlowMigrator = scope.ServiceProvider.GetRequiredService(); + EventFlowEventStoresPostgreSql.MigrateDatabaseAsync(eventFlowMigrator, CancellationToken.None).Wait(); + + return host; + } + + private void MigrateTestDatabase() + { + // Run DbUp migrations for read model tables + DatabaseMigrator.Migrate(_testDatabase.ConnectionString); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + // Reset Hangfire's static LogProvider before disposing to prevent + // ObjectDisposedException when the next test class creates a new factory. + Hangfire.Logging.LogProvider.SetCurrentLogProvider(null); + + _testDatabase.Dispose(); + } + + base.Dispose(disposing); + } + + /// + /// Creates an HttpClient configured for GraphQL requests. + /// + public HttpClient CreateGraphQLClient() + { + var client = CreateClient(); + client.BaseAddress = new Uri("http://localhost/graphql"); + return client; + } +} diff --git a/backend/Books.Api.Tests/UnitTest1.cs b/backend/Books.Api.Tests/UnitTest1.cs new file mode 100644 index 0000000..b5c44c6 --- /dev/null +++ b/backend/Books.Api.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Books.Api.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/backend/Books.Api.Tests/xunit.runner.json b/backend/Books.Api.Tests/xunit.runner.json new file mode 100644 index 0000000..dd80f43 --- /dev/null +++ b/backend/Books.Api.Tests/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} diff --git a/backend/Books.Api/Books.Api.csproj b/backend/Books.Api/Books.Api.csproj new file mode 100644 index 0000000..7115076 --- /dev/null +++ b/backend/Books.Api/Books.Api.csproj @@ -0,0 +1,46 @@ + + + + net10.0 + 14 + enable + enable + true + latest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Books.Api/Books.Api.http b/backend/Books.Api/Books.Api.http new file mode 100644 index 0000000..44ce7a1 --- /dev/null +++ b/backend/Books.Api/Books.Api.http @@ -0,0 +1,6 @@ +@Books.Api_HostAddress = http://localhost:5142 + +GET {{Books.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/backend/Books.Api/Commands/Companies/CompanyCommandHandlers.cs b/backend/Books.Api/Commands/Companies/CompanyCommandHandlers.cs new file mode 100644 index 0000000..85614a9 --- /dev/null +++ b/backend/Books.Api/Commands/Companies/CompanyCommandHandlers.cs @@ -0,0 +1,50 @@ +using Books.Api.Domain.Companies; +using EventFlow.Commands; + +namespace Books.Api.Commands.Companies; + +public class CreateCompanyCommandHandler : CommandHandler +{ + public override Task ExecuteAsync( + CompanyAggregate aggregate, + CreateCompanyCommand command, + CancellationToken cancellationToken) + { + aggregate.Create( + command.Name, + command.Cvr, + command.Address, + command.PostalCode, + command.City, + command.Country, + command.FiscalYearStartMonth, + command.Currency, + command.VatRegistered, + command.VatPeriodFrequency); + + return Task.CompletedTask; + } +} + +public class UpdateCompanyCommandHandler : CommandHandler +{ + public override Task ExecuteAsync( + CompanyAggregate aggregate, + UpdateCompanyCommand command, + CancellationToken cancellationToken) + { + aggregate.Update( + command.Name, + command.Cvr, + command.Address, + command.PostalCode, + command.City, + command.Country, + command.FiscalYearStartMonth, + command.Currency, + command.VatRegistered, + command.VatPeriodFrequency); + + return Task.CompletedTask; + } +} diff --git a/backend/Books.Api/Commands/Companies/CompanyCommands.cs b/backend/Books.Api/Commands/Companies/CompanyCommands.cs new file mode 100644 index 0000000..d1aa404 --- /dev/null +++ b/backend/Books.Api/Commands/Companies/CompanyCommands.cs @@ -0,0 +1,56 @@ +using Books.Api.Domain.Companies; +using EventFlow.Commands; + +namespace Books.Api.Commands.Companies; + +public class CreateCompanyCommand( + CompanyId aggregateId, + string name, + string? cvr, + string? address, + string? postalCode, + string? city, + string country, + int fiscalYearStartMonth, + string currency, + bool vatRegistered, + string? vatPeriodFrequency) + : Command(aggregateId) +{ + public string Name { get; } = name; + public string? Cvr { get; } = cvr; + public string? Address { get; } = address; + public string? PostalCode { get; } = postalCode; + public string? City { get; } = city; + public string Country { get; } = country; + public int FiscalYearStartMonth { get; } = fiscalYearStartMonth; + public string Currency { get; } = currency; + public bool VatRegistered { get; } = vatRegistered; + public string? VatPeriodFrequency { get; } = vatPeriodFrequency; +} + +public class UpdateCompanyCommand( + CompanyId aggregateId, + string name, + string? cvr, + string? address, + string? postalCode, + string? city, + string country, + int fiscalYearStartMonth, + string currency, + bool vatRegistered, + string? vatPeriodFrequency) + : Command(aggregateId) +{ + public string Name { get; } = name; + public string? Cvr { get; } = cvr; + public string? Address { get; } = address; + public string? PostalCode { get; } = postalCode; + public string? City { get; } = city; + public string Country { get; } = country; + public int FiscalYearStartMonth { get; } = fiscalYearStartMonth; + public string Currency { get; } = currency; + public bool VatRegistered { get; } = vatRegistered; + public string? VatPeriodFrequency { get; } = vatPeriodFrequency; +} diff --git a/backend/Books.Api/Controllers/WeatherForecastController.cs b/backend/Books.Api/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..23126ca --- /dev/null +++ b/backend/Books.Api/Controllers/WeatherForecastController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Books.Api.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = + [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} diff --git a/backend/Books.Api/Database/Migrations/001_Initial.sql b/backend/Books.Api/Database/Migrations/001_Initial.sql new file mode 100644 index 0000000..4e16bc1 --- /dev/null +++ b/backend/Books.Api/Database/Migrations/001_Initial.sql @@ -0,0 +1,110 @@ +-- 001_Initial.sql +-- Creates read model tables for Books API +-- NOTE: Column names use snake_case (PostgreSQL convention) +-- Custom ReadModelSqlGenerator converts C# PascalCase to snake_case + +-- Company read models +CREATE TABLE IF NOT EXISTS company_read_models ( + -- EventFlow standard columns + aggregate_id VARCHAR(255) PRIMARY KEY, + create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_aggregate_sequence_number INT NOT NULL DEFAULT 1, + + -- Business columns (snake_case) + name VARCHAR(255) NOT NULL, + cvr VARCHAR(8), + address VARCHAR(500), + postal_code VARCHAR(10), + city VARCHAR(100), + country VARCHAR(2) NOT NULL DEFAULT 'DK', + fiscal_year_start_month SMALLINT NOT NULL DEFAULT 1, + currency VARCHAR(3) NOT NULL DEFAULT 'DKK', + vat_registered BOOLEAN NOT NULL DEFAULT FALSE, + vat_period_frequency VARCHAR(20) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_company_cvr ON company_read_models(cvr) WHERE cvr IS NOT NULL; + +-- Fiscal year read models +CREATE TABLE IF NOT EXISTS fiscal_year_read_models ( + aggregate_id VARCHAR(255) PRIMARY KEY, + create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_aggregate_sequence_number INT NOT NULL DEFAULT 1, + + company_id VARCHAR(255) NOT NULL, + name VARCHAR(50) NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'open', + opening_balance_posted BOOLEAN NOT NULL DEFAULT FALSE, + closing_date TIMESTAMPTZ, + closed_by VARCHAR(255), + + CONSTRAINT fk_fiscal_year_company + FOREIGN KEY (company_id) REFERENCES company_read_models(aggregate_id) ON DELETE CASCADE, + CONSTRAINT chk_fiscal_year_status + CHECK (status IN ('open', 'closed', 'locked')) +); + +CREATE INDEX IF NOT EXISTS idx_fiscal_year_company ON fiscal_year_read_models(company_id); +CREATE INDEX IF NOT EXISTS idx_fiscal_year_dates ON fiscal_year_read_models(start_date, end_date); + +-- Accounting period read models +CREATE TABLE IF NOT EXISTS accounting_period_read_models ( + aggregate_id VARCHAR(255) PRIMARY KEY, + create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_aggregate_sequence_number INT NOT NULL DEFAULT 1, + + fiscal_year_id VARCHAR(255) NOT NULL, + company_id VARCHAR(255) NOT NULL, + name VARCHAR(50) NOT NULL, + period_number SMALLINT NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'future', + closed_at TIMESTAMPTZ, + closed_by VARCHAR(255), + reopened_at TIMESTAMPTZ, + reopened_by VARCHAR(255), + locked_at TIMESTAMPTZ, + locked_by VARCHAR(255), + + CONSTRAINT fk_period_fiscal_year + FOREIGN KEY (fiscal_year_id) REFERENCES fiscal_year_read_models(aggregate_id) ON DELETE CASCADE, + CONSTRAINT chk_period_status + CHECK (status IN ('future', 'open', 'closed', 'locked')) +); + +CREATE INDEX IF NOT EXISTS idx_period_fiscal_year ON accounting_period_read_models(fiscal_year_id); +CREATE INDEX IF NOT EXISTS idx_period_company ON accounting_period_read_models(company_id); + +-- Account read models (chart of accounts) +CREATE TABLE IF NOT EXISTS account_read_models ( + aggregate_id VARCHAR(255) PRIMARY KEY, + create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_aggregate_sequence_number INT NOT NULL DEFAULT 1, + + company_id VARCHAR(255) NOT NULL, + account_number VARCHAR(10) NOT NULL, + name VARCHAR(255) NOT NULL, + account_type VARCHAR(20) NOT NULL, + parent_id VARCHAR(255), + description TEXT, + vat_code_id VARCHAR(255), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + is_system_account BOOLEAN NOT NULL DEFAULT FALSE, + + CONSTRAINT fk_account_company + FOREIGN KEY (company_id) REFERENCES company_read_models(aggregate_id) ON DELETE CASCADE, + CONSTRAINT chk_account_type + CHECK (account_type IN ('asset', 'liability', 'equity', 'revenue', 'cogs', 'expense', 'personnel', 'financial', 'extraordinary')) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_account_number + ON account_read_models(company_id, account_number); +CREATE INDEX IF NOT EXISTS idx_account_company ON account_read_models(company_id); +CREATE INDEX IF NOT EXISTS idx_account_type ON account_read_models(account_type); diff --git a/backend/Books.Api/Domain/Companies/CompanyAggregate.cs b/backend/Books.Api/Domain/Companies/CompanyAggregate.cs new file mode 100644 index 0000000..1c15816 --- /dev/null +++ b/backend/Books.Api/Domain/Companies/CompanyAggregate.cs @@ -0,0 +1,81 @@ +using Books.Api.Domain.Companies.Events; +using EventFlow.Aggregates; + +namespace Books.Api.Domain.Companies; + +public class CompanyAggregate(CompanyId id) : AggregateRoot(id) +{ + private bool _isCreated; + + public void Apply(CompanyCreatedEvent e) => _isCreated = true; + + public void Apply(CompanyUpdatedEvent e) { } + + public void Create( + string name, + string? cvr, + string? address, + string? postalCode, + string? city, + string country, + int fiscalYearStartMonth, + string currency, + bool vatRegistered, + string? vatPeriodFrequency) + { + if (_isCreated) + throw new DomainException("Company already exists"); + + if (string.IsNullOrWhiteSpace(name)) + throw new DomainException("Company name is required"); + + if (fiscalYearStartMonth < 1 || fiscalYearStartMonth > 12) + throw new DomainException("Fiscal year start month must be between 1 and 12"); + + Emit(new CompanyCreatedEvent( + name.Trim(), + cvr?.Trim(), + address?.Trim(), + postalCode?.Trim(), + city?.Trim(), + country, + fiscalYearStartMonth, + currency, + vatRegistered, + vatPeriodFrequency)); + } + + public void Update( + string name, + string? cvr, + string? address, + string? postalCode, + string? city, + string country, + int fiscalYearStartMonth, + string currency, + bool vatRegistered, + string? vatPeriodFrequency) + { + if (!_isCreated) + throw new DomainException("Company does not exist"); + + if (string.IsNullOrWhiteSpace(name)) + throw new DomainException("Company name is required"); + + if (fiscalYearStartMonth < 1 || fiscalYearStartMonth > 12) + throw new DomainException("Fiscal year start month must be between 1 and 12"); + + Emit(new CompanyUpdatedEvent( + name.Trim(), + cvr?.Trim(), + address?.Trim(), + postalCode?.Trim(), + city?.Trim(), + country, + fiscalYearStartMonth, + currency, + vatRegistered, + vatPeriodFrequency)); + } +} diff --git a/backend/Books.Api/Domain/Companies/CompanyId.cs b/backend/Books.Api/Domain/Companies/CompanyId.cs new file mode 100644 index 0000000..01cfbf1 --- /dev/null +++ b/backend/Books.Api/Domain/Companies/CompanyId.cs @@ -0,0 +1,8 @@ +using EventFlow.Core; + +namespace Books.Api.Domain.Companies; + +public class CompanyId : Identity +{ + public CompanyId(string value) : base(value) { } +} diff --git a/backend/Books.Api/Domain/Companies/Events/CompanyCreatedEvent.cs b/backend/Books.Api/Domain/Companies/Events/CompanyCreatedEvent.cs new file mode 100644 index 0000000..78dee22 --- /dev/null +++ b/backend/Books.Api/Domain/Companies/Events/CompanyCreatedEvent.cs @@ -0,0 +1,27 @@ +using EventFlow.Aggregates; + +namespace Books.Api.Domain.Companies.Events; + +public class CompanyCreatedEvent( + string name, + string? cvr, + string? address, + string? postalCode, + string? city, + string country, + int fiscalYearStartMonth, + string currency, + bool vatRegistered, + string? vatPeriodFrequency) : AggregateEvent +{ + public string Name { get; } = name; + public string? Cvr { get; } = cvr; + public string? Address { get; } = address; + public string? PostalCode { get; } = postalCode; + public string? City { get; } = city; + public string Country { get; } = country; + public int FiscalYearStartMonth { get; } = fiscalYearStartMonth; + public string Currency { get; } = currency; + public bool VatRegistered { get; } = vatRegistered; + public string? VatPeriodFrequency { get; } = vatPeriodFrequency; +} diff --git a/backend/Books.Api/Domain/Companies/Events/CompanyUpdatedEvent.cs b/backend/Books.Api/Domain/Companies/Events/CompanyUpdatedEvent.cs new file mode 100644 index 0000000..dbe9116 --- /dev/null +++ b/backend/Books.Api/Domain/Companies/Events/CompanyUpdatedEvent.cs @@ -0,0 +1,27 @@ +using EventFlow.Aggregates; + +namespace Books.Api.Domain.Companies.Events; + +public class CompanyUpdatedEvent( + string name, + string? cvr, + string? address, + string? postalCode, + string? city, + string country, + int fiscalYearStartMonth, + string currency, + bool vatRegistered, + string? vatPeriodFrequency) : AggregateEvent +{ + public string Name { get; } = name; + public string? Cvr { get; } = cvr; + public string? Address { get; } = address; + public string? PostalCode { get; } = postalCode; + public string? City { get; } = city; + public string Country { get; } = country; + public int FiscalYearStartMonth { get; } = fiscalYearStartMonth; + public string Currency { get; } = currency; + public bool VatRegistered { get; } = vatRegistered; + public string? VatPeriodFrequency { get; } = vatPeriodFrequency; +} diff --git a/backend/Books.Api/Domain/DomainException.cs b/backend/Books.Api/Domain/DomainException.cs new file mode 100644 index 0000000..7ab46ef --- /dev/null +++ b/backend/Books.Api/Domain/DomainException.cs @@ -0,0 +1,20 @@ +namespace Books.Api.Domain; + +public class DomainException : Exception +{ + public string Code { get; } + public string? MessageDanish { get; } + + public DomainException(string message) + : base(message) + { + Code = "DOMAIN_ERROR"; + } + + public DomainException(string code, string message, string messageDanish) + : base(message) + { + Code = code; + MessageDanish = messageDanish; + } +} diff --git a/backend/Books.Api/Endpoints/ApiEndpoints.cs b/backend/Books.Api/Endpoints/ApiEndpoints.cs new file mode 100644 index 0000000..e69de29 diff --git a/backend/Books.Api/EventFlow/Customs/Casing.cs b/backend/Books.Api/EventFlow/Customs/Casing.cs new file mode 100644 index 0000000..a6959ef --- /dev/null +++ b/backend/Books.Api/EventFlow/Customs/Casing.cs @@ -0,0 +1,31 @@ +using System.Reflection; + +namespace Books.Api.EventFlow.Customs; + +public static class Casing +{ + public static string ToSnakeCase(this string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + var stringBuilder = new System.Text.StringBuilder(); + for (var i = 0; i < input.Length; i++) + { + var c = input[i]; + if (char.IsUpper(c)) + { + if (i > 0) + { + stringBuilder.Append('_'); + } + stringBuilder.Append(char.ToLower(c)); + } + else + { + stringBuilder.Append(c); + } + } + return stringBuilder.ToString(); + } +} \ No newline at end of file diff --git a/backend/Books.Api/EventFlow/Customs/ReadModelSqlGenerator.cs b/backend/Books.Api/EventFlow/Customs/ReadModelSqlGenerator.cs new file mode 100644 index 0000000..2760bfc --- /dev/null +++ b/backend/Books.Api/EventFlow/Customs/ReadModelSqlGenerator.cs @@ -0,0 +1,221 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2025 Rasmus Mikkelsen +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Concurrent; +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; +using EventFlow.Extensions; +using EventFlow.ReadStores; +using EventFlow.Sql.ReadModels; +using EventFlow.Sql.ReadModels.Attributes; + +namespace Books.Api.EventFlow.Customs; + +/// +/// Custom SQL generator for PostgreSQL that uses snake_case column names +/// while keeping PascalCase property names for Dapper parameter binding. +/// +public class ReadModelSqlGenerator : IReadModelSqlGenerator +{ + private static readonly ConcurrentDictionary TableNames = new(); + private static readonly ConcurrentDictionary> PropertyInfos = new(); + private static readonly ConcurrentDictionary IdentityColumns = new(); + private static readonly ConcurrentDictionary IdentityProperties = new(); + private static readonly ConcurrentDictionary VersionColumns = new(); + + private readonly ConcurrentDictionary _insertSqls = new(); + private readonly ConcurrentDictionary _purgeSqls = new(); + private readonly ConcurrentDictionary _deleteSqls = new(); + private readonly ConcurrentDictionary _selectSqls = new(); + private readonly ConcurrentDictionary _updateSqls = new(); + + public string? CreateInsertSql() where TReadModel : IReadModel + { + var readModelType = typeof(TReadModel); + if (_insertSqls.TryGetValue(readModelType, out var sql)) + { + return sql; + } + + var properties = GetPropertyInfos(readModelType); + + // Column names in snake_case + var columnList = string.Join(", ", properties.Select(p => p.Name.ToSnakeCase())); + + // Parameter names use property names (PascalCase) for Dapper binding + var parameterList = string.Join(", ", properties.Select(p => $"@{p.Name}")); + + sql = $"INSERT INTO {GetTableName()} ({columnList}) VALUES ({parameterList})"; + + _insertSqls[readModelType] = sql; + + return sql; + } + + public string CreateSelectSql() where TReadModel : IReadModel + { + var readModelType = typeof(TReadModel); + if (_selectSqls.TryGetValue(readModelType, out var sql)) + { + return sql; + } + + var tableName = GetTableName(); + var identityColumn = GetIdentityColumn(); + var properties = GetPropertyInfos(readModelType); + + // Use explicit column aliases to map snake_case columns to PascalCase properties + var columnList = string.Join(", ", properties.Select(p => $"{p.Name.ToSnakeCase()} AS \"{p.Name}\"")); + + sql = $"SELECT {columnList} FROM {tableName} WHERE {identityColumn} = @EventFlowReadModelId"; + + _selectSqls[readModelType] = sql; + + return sql; + } + + public string CreateDeleteSql() where TReadModel : IReadModel + { + var readModelType = typeof(TReadModel); + if (_deleteSqls.TryGetValue(readModelType, out var sql)) + { + return sql; + } + + sql = $"DELETE FROM {GetTableName()} WHERE {GetIdentityColumn()} = @EventFlowReadModelId"; + _deleteSqls[readModelType] = sql; + + return sql; + } + + public string CreateUpdateSql() where TReadModel : IReadModel + { + var readModelType = typeof(TReadModel); + if (_updateSqls.TryGetValue(readModelType, out var sql)) + { + return sql; + } + + var identityColumn = GetIdentityColumn(); + var identityProperty = GetIdentityProperty(); + var versionColumn = GetVersionColumn(); + + var versionCheck = string.IsNullOrEmpty(versionColumn) + ? string.Empty + : $"AND {versionColumn} = @_PREVIOUS_VERSION"; + + var properties = GetPropertyInfos(readModelType); + var updateColumns = properties + .Where(p => p.Name.ToSnakeCase() != identityColumn) + .Select(p => $"{p.Name.ToSnakeCase()} = @{p.Name}"); + + var tableName = GetTableName(); + + sql = $"UPDATE {tableName} SET {string.Join(", ", updateColumns)} WHERE {identityColumn} = @{identityProperty} {versionCheck}"; + + _updateSqls[readModelType] = sql; + + return sql; + } + + public string CreatePurgeSql() where TReadModel : IReadModel + { + return _purgeSqls.GetOrCreate(typeof(TReadModel), t => $"DELETE FROM {GetTableName(t)}"); + } + + public string GetTableName() where TReadModel : IReadModel + { + return GetTableName(typeof(TReadModel)); + } + + private static string GetTableName(Type readModelType) + { + return TableNames.GetOrAdd( + readModelType, + t => + { + var tableAttribute = t.GetTypeInfo().GetCustomAttribute(false); + var table = string.IsNullOrEmpty(tableAttribute?.Name) + ? $"ReadModel-{t.Name.Replace("ReadModel", string.Empty)}" + : tableAttribute.Name; + return string.IsNullOrEmpty(tableAttribute?.Schema) + ? $"\"{table}\"" + : $"\"{tableAttribute?.Schema}\".\"{table}\""; + }); + } + + private static string GetIdentityColumn() + { + return IdentityColumns.GetOrAdd( + typeof(TReadModel), + t => + { + var propertyInfo = GetPropertyInfos(t) + .SingleOrDefault( + pi => pi.GetCustomAttributes().Any(a => a is SqlReadModelIdentityColumnAttribute)); + return (propertyInfo?.Name ?? "AggregateId").ToSnakeCase(); + }); + } + + private static string GetIdentityProperty() + { + return IdentityProperties.GetOrAdd( + typeof(TReadModel), + t => + { + var propertyInfo = GetPropertyInfos(t) + .SingleOrDefault( + pi => pi.GetCustomAttributes().Any(a => a is SqlReadModelIdentityColumnAttribute)); + return propertyInfo?.Name ?? "AggregateId"; + }); + } + + private static string GetVersionColumn() + { + return VersionColumns.GetOrAdd( + typeof(TReadModel), + t => + { + var propertyInfo = GetPropertyInfos(t) + .SingleOrDefault( + pi => pi.GetCustomAttributes().Any(a => a is SqlReadModelVersionColumnAttribute)); + if (propertyInfo != null) + { + return propertyInfo.Name.ToSnakeCase(); + } + + return GetPropertyInfos(t).Any(n => n.Name == "LastAggregateSequenceNumber") + ? "last_aggregate_sequence_number" + : string.Empty; + }); + } + + private static IReadOnlyCollection GetPropertyInfos(Type readModelType) + { + return PropertyInfos.GetOrAdd( + readModelType, + t => t.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => !p.GetCustomAttributes().Any(a => a is SqlReadModelIgnoreColumnAttribute)) + .OrderBy(p => p.Name) + .ToList()); + } +} diff --git a/backend/Books.Api/EventFlow/Extensions/ReadModelRegistrationExtensions.cs b/backend/Books.Api/EventFlow/Extensions/ReadModelRegistrationExtensions.cs new file mode 100644 index 0000000..1f4bce7 --- /dev/null +++ b/backend/Books.Api/EventFlow/Extensions/ReadModelRegistrationExtensions.cs @@ -0,0 +1,30 @@ +using Books.Api.EventFlow.ReadModels; +using Books.Api.EventFlow.Repositories; +using EventFlow; +using EventFlow.Extensions; +using EventFlow.PostgreSql.Extensions; +using EventFlow.Sql.ReadModels; +using ReadModelSqlGenerator = Books.Api.EventFlow.Customs.ReadModelSqlGenerator; + +namespace Books.Api.EventFlow.Extensions; + +public static class ReadModelRegistrationExtensions +{ + public static IEventFlowOptions AddReadModels(this IEventFlowOptions options) + { + return options + .UsePostgreSqlReadModel() + .RegisterServices( sr => sr.AddSingleton(new ReadModelSqlGenerator())); + } + + public static IServiceCollection AddRepositories(this IServiceCollection services) + { + // Register locators + services.AddTransient(); + + // Register repositories + services.AddScoped(); + + return services; + } +} diff --git a/backend/Books.Api/EventFlow/Infrastructure/DatabaseMigrator.cs b/backend/Books.Api/EventFlow/Infrastructure/DatabaseMigrator.cs new file mode 100644 index 0000000..f3ae5c8 --- /dev/null +++ b/backend/Books.Api/EventFlow/Infrastructure/DatabaseMigrator.cs @@ -0,0 +1,43 @@ +using System.Reflection; +using Dapper; +using DbUp; +using Npgsql; + +namespace Books.Api.EventFlow.Infrastructure; + +public static class DatabaseMigrator +{ + public static void Migrate(string connectionString) + { + EnsureDatabaseExists(connectionString); + + var upgrader = DeployChanges.To + .PostgresqlDatabase(connectionString) + .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly()) + .LogToConsole() + .Build(); + + var result = upgrader.PerformUpgrade(); + if (!result.Successful) + throw result.Error; + } + + private static void EnsureDatabaseExists(string connectionString) + { + var builder = new NpgsqlConnectionStringBuilder(connectionString); + var database = builder.Database; + builder.Database = "postgres"; + + using var connection = new NpgsqlConnection(builder.ConnectionString); + connection.Open(); + + var exists = connection.ExecuteScalar( + "SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = @db)", + new { db = database }); + + if (!exists) + { + connection.Execute($"CREATE DATABASE \"{database}\""); + } + } +} diff --git a/backend/Books.Api/EventFlow/Infrastructure/DispatchToSubscriberResilienceStrategy.cs b/backend/Books.Api/EventFlow/Infrastructure/DispatchToSubscriberResilienceStrategy.cs new file mode 100644 index 0000000..a917d2a --- /dev/null +++ b/backend/Books.Api/EventFlow/Infrastructure/DispatchToSubscriberResilienceStrategy.cs @@ -0,0 +1,238 @@ +using System.Reflection; +using Books.Api.Infrastructure; +using EventFlow.Aggregates; +using EventFlow.EventStores; +using EventFlow.Subscribers; +using Hangfire; + +namespace Books.Api.EventFlow.Infrastructure; + +#pragma warning disable CS9113 // Parameter is unread +public class DispatchToSubscriberResilienceStrategy( + ILogger logger, + IScheduler scheduler, + IServiceProvider serviceProvider, + IEventJsonSerializer eventJsonSerializer) : IDispatchToSubscriberResilienceStrategy +#pragma warning restore CS9113 +{ + public Task BeforeHandleEventAsync( + ISubscribe subscriberTo, + IDomainEvent domainEvent, + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task HandleEventFailedAsync( + ISubscribe subscriberTo, + IDomainEvent domainEvent, + Exception exception, + bool swallowException, + CancellationToken cancellationToken) + { + var subscriberType = GetSubscriberType(subscriberTo); + var eventType = domainEvent.EventType.Name; + var aggregateId = domainEvent.GetIdentity()?.Value ?? "unknown"; + + logger.LogError(exception, + "[RESILIENCE] Subscriber {SubscriberType} failed to handle event {EventType} for aggregate {AggregateId}", + subscriberType.Name, + eventType, + aggregateId); + + try + { + var serializedEvent = eventJsonSerializer.Serialize(domainEvent); + var domainEventInterface = domainEvent.GetType().GetTypeInfo() + .GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDomainEvent<,,>)); + + if (domainEventInterface != null) + { + scheduler.EnqueueJob( + job => job.RetryEventDispatchAsync( + subscriberType.AssemblyQualifiedName!, + domainEventInterface.AssemblyQualifiedName!, + serializedEvent.SerializedData, + serializedEvent.SerializedMetadata), + TimeSpan.FromSeconds(30)); + + logger.LogWarning( + "[RESILIENCE] Scheduled retry job for {SubscriberType} / {EventType} / {AggregateId}", + subscriberType.Name, + eventType, + aggregateId); + } + } + catch (Exception e) + { + logger.LogError(e, + "[RESILIENCE] Failed to schedule retry for {SubscriberType} / {EventType}. " + + "This event will NOT be retried and may cause data inconsistency.", + subscriberType.Name, + eventType); + } + + return Task.CompletedTask; + } + + public Task HandleEventSucceededAsync( + ISubscribe subscriberTo, + IDomainEvent domainEvent, + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task BeforeDispatchToSubscribersAsync( + IDomainEvent domainEvent, + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task DispatchToSubscribersSucceededAsync( + IDomainEvent domainEvent, + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task HandleDispatchToSubscribersFailedAsync( + IDomainEvent domainEvent, + IReadOnlyCollection domainEvents, + Exception exception, + CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + private static Type GetSubscriberType(ISubscribe subscriberTo) + { + if (subscriberTo is ISubscribeDecorator decorator) + { + return GetSubscriberType(decorator.InnerInstance as ISubscribe + ?? throw new InvalidOperationException("InnerInstance is not ISubscribe")); + } + + return subscriberTo.GetType(); + } +} + +public interface ISubscribeDecorator +{ + object InnerInstance { get; } +} + +public class SubscriberRetryJob( + ILogger logger, + IServiceProvider serviceProvider, + IEventJsonSerializer eventJsonSerializer) +{ + [AutomaticRetry(Attempts = 3, DelaysInSeconds = [30, 60, 120])] + public async Task RetryEventDispatchAsync( + string subscriberTypeName, + string domainEventTypeName, + string serializedData, + string serializedMetadata) + { + logger.LogInformation( + "[RETRY] Retrying event dispatch to {SubscriberType}", + subscriberTypeName); + try + { + var domainEvent = eventJsonSerializer.Deserialize(serializedData, serializedMetadata); + var subscriberType = Type.GetType(subscriberTypeName); + + if (subscriberType == null) + { + logger.LogError("[RETRY] Could not resolve subscriber type: {SubscriberType}", subscriberTypeName); + return; + } + + var subscriber = FindSubscriberInstance(subscriberType, domainEvent); + if (subscriber == null) + { + logger.LogError("[RETRY] Could not find subscriber instance for {SubscriberType}", subscriberTypeName); + return; + } + + var domainEventType = domainEvent.GetType(); + var handleAsyncMethod = subscriber.GetType().GetMethods() + .FirstOrDefault(m => m.Name == "HandleAsync" && + m.GetParameters().Any(p => p.ParameterType.IsAssignableFrom(domainEventType))); + + if (handleAsyncMethod != null) + { + await (Task)handleAsyncMethod.Invoke(subscriber, [domainEvent, CancellationToken.None])!; + logger.LogInformation("[RETRY] Successfully retried event dispatch to {SubscriberType}", subscriberTypeName); + } + else + { + logger.LogError( + "[RETRY] No matching HandleAsync method found for {SubscriberType} and {DomainEventType}", + subscriberTypeName, + domainEventTypeName); + } + } + catch (Exception ex) + { + logger.LogError(ex, "[RETRY] Failed to retry event dispatch to {SubscriberType}", subscriberTypeName); + throw; + } + } + + private object? FindSubscriberInstance(Type subscriberType, object domainEvent) + { + var interfaceType = ExtractSubscriberInterface(subscriberType, domainEvent); + if (interfaceType == null) + return null; + + var instances = serviceProvider.GetServices(interfaceType); + return instances.FirstOrDefault(x => + x?.GetType() == subscriberType || + HasInnerType(x, subscriberType)); + } + + private static Type? ExtractSubscriberInterface(Type subscriberType, object domainEvent) + { + var domainEventType = domainEvent.GetType(); + var interfaces = subscriberType.GetInterfaces() + .Where(i => i.IsGenericType && + (i.GetGenericTypeDefinition() == typeof(ISubscribeAsynchronousTo<,,>) || + i.GetGenericTypeDefinition() == typeof(ISubscribeSynchronousTo<,,>))); + + foreach (var iface in interfaces) + { + var genericArguments = iface.GetGenericArguments(); + var eventGenericArgs = domainEventType.GenericTypeArguments; + if (eventGenericArgs.Length >= 3 && genericArguments[2] == eventGenericArgs[2]) + { + return iface; + } + } + + return null; + } + + private static bool HasInnerType(object? obj, Type concreteType) + { + if (obj == null) return false; + + var decoratorType = obj.GetType(); + var field = decoratorType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .FirstOrDefault(x => x.Name.Contains("inner", StringComparison.InvariantCultureIgnoreCase)); + var property = decoratorType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance) + .FirstOrDefault(x => x.Name.Contains("inner", StringComparison.InvariantCultureIgnoreCase)); + + if (field?.GetValue(obj)?.GetType() == concreteType) + return true; + + if (property?.GetValue(obj)?.GetType() == concreteType) + return true; + + return false; + } +} diff --git a/backend/Books.Api/EventFlow/Infrastructure/ReadStoresResilienceStrategy.cs b/backend/Books.Api/EventFlow/Infrastructure/ReadStoresResilienceStrategy.cs new file mode 100644 index 0000000..6ea58cd --- /dev/null +++ b/backend/Books.Api/EventFlow/Infrastructure/ReadStoresResilienceStrategy.cs @@ -0,0 +1,95 @@ +using Books.Api.Infrastructure; +using EventFlow.Aggregates; +using EventFlow.ReadStores; +using Hangfire; + +namespace Books.Api.EventFlow.Infrastructure; + +public class ReadStoresResilienceStrategy( + ILogger logger, + IScheduler scheduler) : IDispatchToReadStoresResilienceStrategy +{ + public Task BeforeUpdateAsync( + IReadStoreManager readStoreManager, + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task HandleUpdateFailedAsync( + IReadStoreManager readStoreManager, + IReadOnlyCollection domainEvents, + Exception exception, + CancellationToken cancellationToken) + { + var firstEvent = domainEvents.FirstOrDefault(); + var aggregateId = firstEvent?.GetIdentity()?.Value ?? "unknown"; + var readModelType = readStoreManager.ReadModelType.Name; + + logger.LogError(exception, + "[RESILIENCE] Failed to update read model {ReadModelType} for aggregate {AggregateId}. " + + "Events: {EventCount}. This may cause data inconsistency.", + readModelType, + aggregateId, + domainEvents.Count); + + // Schedule a retry job to repopulate the read model + if (firstEvent != null) + { + var aggregateType = firstEvent.AggregateType.Name; + + scheduler.EnqueueJob( + job => job.RepopulateReadModelAsync( + aggregateType, + aggregateId, + readModelType), + TimeSpan.FromSeconds(30)); + + logger.LogWarning( + "[RESILIENCE] Scheduled read model repopulation job for {ReadModelType} / {AggregateId}", + readModelType, + aggregateId); + } + + // Return false to indicate we handled the failure (don't rethrow) + return Task.FromResult(false); + } + + public Task UpdateSucceededAsync( + IReadStoreManager readStoreManager, + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} + +public class ReadModelRepopulationJob(ILogger logger) +{ + [AutomaticRetry(Attempts = 3, DelaysInSeconds = [30, 60, 120])] + public Task RepopulateReadModelAsync( + string aggregateType, + string aggregateId, + string readModelType) + { + logger.LogInformation( + "[REPOPULATION] Starting read model repopulation for {ReadModelType} / {AggregateId}", + readModelType, + aggregateId); + + // For now, we log the repopulation attempt + // A full implementation would: + // 1. Load all events for the aggregate from the event store + // 2. Clear the existing read model entry + // 3. Replay all events to rebuild the read model + + logger.LogWarning( + "[REPOPULATION] Read model repopulation for {ReadModelType} / {AggregateId} " + + "requires manual intervention. Check data consistency.", + readModelType, + aggregateId); + + return Task.CompletedTask; + } +} diff --git a/backend/Books.Api/EventFlow/ReadModels/CompanyReadModel.cs b/backend/Books.Api/EventFlow/ReadModels/CompanyReadModel.cs new file mode 100644 index 0000000..ab238b2 --- /dev/null +++ b/backend/Books.Api/EventFlow/ReadModels/CompanyReadModel.cs @@ -0,0 +1,80 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Books.Api.Domain.Companies; +using Books.Api.Domain.Companies.Events; +using EventFlow.Aggregates; +using EventFlow.PostgreSql.ReadStores.Attributes; +using EventFlow.ReadStores; + +namespace Books.Api.EventFlow.ReadModels; + +[Table("company_read_models")] +public class CompanyReadModel : IReadModel, + IAmReadModelFor, + IAmReadModelFor +{ + // EventFlow standard columns + [PostgreSqlReadModelIdentityColumn] + public string AggregateId { get; set; } = string.Empty; + + public DateTimeOffset CreateTime { get; set; } + public DateTimeOffset UpdatedTime { get; set; } + + [PostgreSqlReadModelVersionColumn] + public int LastAggregateSequenceNumber { get; set; } + + // Business columns + public string Name { get; set; } = string.Empty; + public string? Cvr { get; set; } + public string? Address { get; set; } + public string? PostalCode { get; set; } + public string? City { get; set; } + public string Country { get; set; } = "DK"; + public int FiscalYearStartMonth { get; set; } = 1; + public string Currency { get; set; } = "DKK"; + public bool VatRegistered { get; set; } + public string? VatPeriodFrequency { get; set; } + + public Task ApplyAsync( + IReadModelContext context, + IDomainEvent domainEvent, + CancellationToken cancellationToken) + { + var e = domainEvent.AggregateEvent; + AggregateId = domainEvent.AggregateIdentity.Value; + CreateTime = domainEvent.Timestamp; + UpdatedTime = domainEvent.Timestamp; + LastAggregateSequenceNumber = (int)domainEvent.AggregateSequenceNumber; + Name = e.Name; + Cvr = e.Cvr; + Address = e.Address; + PostalCode = e.PostalCode; + City = e.City; + Country = e.Country; + FiscalYearStartMonth = e.FiscalYearStartMonth; + Currency = e.Currency; + VatRegistered = e.VatRegistered; + VatPeriodFrequency = e.VatPeriodFrequency; + return Task.CompletedTask; + } + + public Task ApplyAsync( + IReadModelContext context, + IDomainEvent domainEvent, + CancellationToken cancellationToken) + { + var e = domainEvent.AggregateEvent; + UpdatedTime = domainEvent.Timestamp; + LastAggregateSequenceNumber = (int)domainEvent.AggregateSequenceNumber; + Name = e.Name; + Cvr = e.Cvr; + Address = e.Address; + PostalCode = e.PostalCode; + City = e.City; + Country = e.Country; + FiscalYearStartMonth = e.FiscalYearStartMonth; + Currency = e.Currency; + VatRegistered = e.VatRegistered; + VatPeriodFrequency = e.VatPeriodFrequency; + return Task.CompletedTask; + } +} diff --git a/backend/Books.Api/EventFlow/ReadModels/CompanyReadModelDto.cs b/backend/Books.Api/EventFlow/ReadModels/CompanyReadModelDto.cs new file mode 100644 index 0000000..7344090 --- /dev/null +++ b/backend/Books.Api/EventFlow/ReadModels/CompanyReadModelDto.cs @@ -0,0 +1,24 @@ +namespace Books.Api.EventFlow.ReadModels; + +/// +/// DTO for reading company data from the database. +/// Uses a class with properties instead of a positional record because +/// PostgreSQL returns column names in lowercase, and Dapper matches +/// properties case-insensitively but requires exact constructor parameter names. +/// +public class CompanyReadModelDto +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? Cvr { get; set; } + public string? Address { get; set; } + public string? PostalCode { get; set; } + public string? City { get; set; } + public string Country { get; set; } = string.Empty; + public int FiscalYearStartMonth { get; set; } + public string Currency { get; set; } = string.Empty; + public bool VatRegistered { get; set; } + public string? VatPeriodFrequency { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/backend/Books.Api/EventFlow/ReadModels/CompanyReadModelLocator.cs b/backend/Books.Api/EventFlow/ReadModels/CompanyReadModelLocator.cs new file mode 100644 index 0000000..fe85753 --- /dev/null +++ b/backend/Books.Api/EventFlow/ReadModels/CompanyReadModelLocator.cs @@ -0,0 +1,16 @@ +using Books.Api.Domain.Companies; +using EventFlow.Aggregates; +using EventFlow.ReadStores; + +namespace Books.Api.EventFlow.ReadModels; + +public class CompanyReadModelLocator : IReadModelLocator +{ + public IEnumerable GetReadModelIds(IDomainEvent domainEvent) + { + if (domainEvent is IDomainEvent typedEvent) + { + yield return typedEvent.AggregateIdentity.Value; + } + } +} diff --git a/backend/Books.Api/EventFlow/Repositories/CompanyRepository.cs b/backend/Books.Api/EventFlow/Repositories/CompanyRepository.cs new file mode 100644 index 0000000..ca1d6c3 --- /dev/null +++ b/backend/Books.Api/EventFlow/Repositories/CompanyRepository.cs @@ -0,0 +1,89 @@ +using Books.Api.Domain.Companies; +using Books.Api.EventFlow.ReadModels; +using Dapper; +using Npgsql; + +namespace Books.Api.EventFlow.Repositories; + +public class CompanyRepository(NpgsqlDataSource dataSource) : ICompanyRepository +{ + public async Task GetByIdAsync(string id, CancellationToken cancellationToken = default) + { + await using var connection = await dataSource.OpenConnectionAsync(cancellationToken); + + const string sql = """ + SELECT + aggregate_id AS Id, + name AS Name, + cvr AS Cvr, + address AS Address, + postal_code AS PostalCode, + city AS City, + country AS Country, + fiscal_year_start_month AS FiscalYearStartMonth, + currency AS Currency, + vat_registered AS VatRegistered, + vat_period_frequency AS VatPeriodFrequency, + create_time AS CreatedAt, + updated_time AS UpdatedAt + FROM company_read_models + WHERE aggregate_id = @Id + """; + + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + + public async Task> GetByIds(List ids, + CancellationToken cancellationToken = default) + { + await using var connection = await dataSource.OpenConnectionAsync(cancellationToken); + + const string sql = """ + SELECT + aggregate_id AS Id, + name AS Name, + cvr AS Cvr, + address AS Address, + postal_code AS PostalCode, + city AS City, + country AS Country, + fiscal_year_start_month AS FiscalYearStartMonth, + currency AS Currency, + vat_registered AS VatRegistered, + vat_period_frequency AS VatPeriodFrequency, + create_time AS CreatedAt, + updated_time AS UpdatedAt + FROM company_read_models + WHERE aggregate_id = ANY(@Ids) + """; + + return await connection.QueryAsync(sql, new { Ids = ids.Select(i => i.Value).ToArray() }); + } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + await using var connection = await dataSource.OpenConnectionAsync(cancellationToken); + + const string sql = """ + SELECT + aggregate_id AS Id, + name AS Name, + cvr AS Cvr, + address AS Address, + postal_code AS PostalCode, + city AS City, + country AS Country, + fiscal_year_start_month AS FiscalYearStartMonth, + currency AS Currency, + vat_registered AS VatRegistered, + vat_period_frequency AS VatPeriodFrequency, + create_time AS CreatedAt, + updated_time AS UpdatedAt + FROM company_read_models + ORDER BY name + """; + + var result = await connection.QueryAsync(sql); + return result.ToList(); + } +} diff --git a/backend/Books.Api/EventFlow/Repositories/ICompanyRepository.cs b/backend/Books.Api/EventFlow/Repositories/ICompanyRepository.cs new file mode 100644 index 0000000..47206d9 --- /dev/null +++ b/backend/Books.Api/EventFlow/Repositories/ICompanyRepository.cs @@ -0,0 +1,11 @@ +using Books.Api.Domain.Companies; +using Books.Api.EventFlow.ReadModels; + +namespace Books.Api.EventFlow.Repositories; + +public interface ICompanyRepository +{ + Task GetByIdAsync(string id, CancellationToken cancellationToken = default); + Task> GetByIds(List ids, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); +} diff --git a/backend/Books.Api/GraphQL/BooksSchema.cs b/backend/Books.Api/GraphQL/BooksSchema.cs new file mode 100644 index 0000000..8cf08a4 --- /dev/null +++ b/backend/Books.Api/GraphQL/BooksSchema.cs @@ -0,0 +1,14 @@ +using Books.Api.GraphQL.Mutations; +using Books.Api.GraphQL.Queries; +using GraphQL.Types; + +namespace Books.Api.GraphQL; + +public class BooksSchema : Schema +{ + public BooksSchema(IServiceProvider serviceProvider) : base(serviceProvider) + { + Query = serviceProvider.GetRequiredService(); + Mutation = serviceProvider.GetRequiredService(); + } +} diff --git a/backend/Books.Api/GraphQL/InputTypes/CreateCompanyInputType.cs b/backend/Books.Api/GraphQL/InputTypes/CreateCompanyInputType.cs new file mode 100644 index 0000000..98f2a0c --- /dev/null +++ b/backend/Books.Api/GraphQL/InputTypes/CreateCompanyInputType.cs @@ -0,0 +1,37 @@ +using GraphQL.Types; + +namespace Books.Api.GraphQL.InputTypes; + +public class CreateCompanyInputType : InputObjectGraphType +{ + public CreateCompanyInputType() + { + Name = "CreateCompanyInput"; + Description = "Input for creating a new company"; + + Field(x => x.Name).Description("Company name (required)"); + Field(x => x.Cvr, nullable: true).Description("Danish CVR number"); + Field(x => x.Address, nullable: true).Description("Street address"); + Field(x => x.PostalCode, nullable: true).Description("Postal code"); + Field(x => x.City, nullable: true).Description("City"); + Field(x => x.Country, nullable: true).Description("Country code (default: DK)"); + Field(x => x.FiscalYearStartMonth, nullable: true).Description("Month when fiscal year starts (default: 1)"); + Field(x => x.Currency, nullable: true).Description("Default currency (default: DKK)"); + Field(x => x.VatRegistered, nullable: true).Description("Whether VAT registered (default: false)"); + Field(x => x.VatPeriodFrequency, nullable: true).Description("VAT reporting frequency"); + } +} + +public class CreateCompanyInput +{ + public string Name { get; set; } = string.Empty; + public string? Cvr { get; set; } + public string? Address { get; set; } + public string? PostalCode { get; set; } + public string? City { get; set; } + public string? Country { get; set; } + public int? FiscalYearStartMonth { get; set; } + public string? Currency { get; set; } + public bool? VatRegistered { get; set; } + public string? VatPeriodFrequency { get; set; } +} diff --git a/backend/Books.Api/GraphQL/InputTypes/UpdateCompanyInputType.cs b/backend/Books.Api/GraphQL/InputTypes/UpdateCompanyInputType.cs new file mode 100644 index 0000000..be438e6 --- /dev/null +++ b/backend/Books.Api/GraphQL/InputTypes/UpdateCompanyInputType.cs @@ -0,0 +1,37 @@ +using GraphQL.Types; + +namespace Books.Api.GraphQL.InputTypes; + +public class UpdateCompanyInputType : InputObjectGraphType +{ + public UpdateCompanyInputType() + { + Name = "UpdateCompanyInput"; + Description = "Input for updating an existing company"; + + Field(x => x.Name).Description("Company name (required)"); + Field(x => x.Cvr, nullable: true).Description("Danish CVR number"); + Field(x => x.Address, nullable: true).Description("Street address"); + Field(x => x.PostalCode, nullable: true).Description("Postal code"); + Field(x => x.City, nullable: true).Description("City"); + Field(x => x.Country, nullable: true).Description("Country code"); + Field(x => x.FiscalYearStartMonth, nullable: true).Description("Month when fiscal year starts"); + Field(x => x.Currency, nullable: true).Description("Default currency"); + Field(x => x.VatRegistered, nullable: true).Description("Whether VAT registered"); + Field(x => x.VatPeriodFrequency, nullable: true).Description("VAT reporting frequency"); + } +} + +public class UpdateCompanyInput +{ + public string Name { get; set; } = string.Empty; + public string? Cvr { get; set; } + public string? Address { get; set; } + public string? PostalCode { get; set; } + public string? City { get; set; } + public string? Country { get; set; } + public int? FiscalYearStartMonth { get; set; } + public string? Currency { get; set; } + public bool? VatRegistered { get; set; } + public string? VatPeriodFrequency { get; set; } +} diff --git a/backend/Books.Api/GraphQL/Mutations/BooksMutation.cs b/backend/Books.Api/GraphQL/Mutations/BooksMutation.cs new file mode 100644 index 0000000..b1ec859 --- /dev/null +++ b/backend/Books.Api/GraphQL/Mutations/BooksMutation.cs @@ -0,0 +1,82 @@ +using Books.Api.Commands.Companies; +using Books.Api.Domain.Companies; +using Books.Api.EventFlow.Repositories; +using Books.Api.GraphQL.InputTypes; +using Books.Api.GraphQL.Types; +using EventFlow; +using GraphQL; +using GraphQL.Types; + +namespace Books.Api.GraphQL.Mutations; + +public class BooksMutation : ObjectGraphType +{ + public BooksMutation() + { + Name = "Mutation"; + Description = "Root mutation for the Books API"; + + // createCompany(input: CreateCompanyInput!): CompanyType + Field("createCompany") + .Description("Create a new company") + .Argument>("input", "The company data") + .ResolveAsync(async ctx => + { + var input = ctx.GetArgument("input"); + var commandBus = ctx.RequestServices!.GetRequiredService(); + var repository = ctx.RequestServices!.GetRequiredService(); + + var companyId = CompanyId.New; + + var command = new CreateCompanyCommand( + companyId, + input.Name, + input.Cvr, + input.Address, + input.PostalCode, + input.City, + input.Country ?? "DK", + input.FiscalYearStartMonth ?? 1, + input.Currency ?? "DKK", + input.VatRegistered ?? false, + input.VatPeriodFrequency); + + await commandBus.PublishAsync(command, ctx.CancellationToken); + + // Return the created company (eventually consistent) + return await repository.GetByIdAsync(companyId.Value, ctx.CancellationToken); + }); + + // updateCompany(id: ID!, input: UpdateCompanyInput!): CompanyType + Field("updateCompany") + .Description("Update an existing company") + .Argument>("id", "The company ID") + .Argument>("input", "The updated company data") + .ResolveAsync(async ctx => + { + var id = ctx.GetArgument("id"); + var input = ctx.GetArgument("input"); + var commandBus = ctx.RequestServices!.GetRequiredService(); + var repository = ctx.RequestServices!.GetRequiredService(); + + var companyId = CompanyId.With(id); + + var command = new UpdateCompanyCommand( + companyId, + input.Name, + input.Cvr, + input.Address, + input.PostalCode, + input.City, + input.Country ?? "DK", + input.FiscalYearStartMonth ?? 1, + input.Currency ?? "DKK", + input.VatRegistered ?? false, + input.VatPeriodFrequency); + + await commandBus.PublishAsync(command, ctx.CancellationToken); + + return await repository.GetByIdAsync(companyId.Value, ctx.CancellationToken); + }); + } +} diff --git a/backend/Books.Api/GraphQL/Queries/BooksQuery.cs b/backend/Books.Api/GraphQL/Queries/BooksQuery.cs new file mode 100644 index 0000000..2736fe1 --- /dev/null +++ b/backend/Books.Api/GraphQL/Queries/BooksQuery.cs @@ -0,0 +1,37 @@ +using Books.Api.Domain.Companies; +using Books.Api.EventFlow.Repositories; +using Books.Api.GraphQL.Types; +using GraphQL; +using GraphQL.Types; + +namespace Books.Api.GraphQL.Queries; + +public class BooksQuery : ObjectGraphType +{ + public BooksQuery() + { + Name = "Query"; + Description = "Root query for the Books API"; + + // companies: [CompanyType] + Field>("companies") + .Description("Get all companies") + .ResolveAsync(async ctx => + { + var repository = ctx.RequestServices!.GetRequiredService(); + return await repository.GetAllAsync(ctx.CancellationToken); + }); + + // company(id: ID!): CompanyType + Field("company") + .Description("Get a company by ID") + .Argument>("id", "The company ID") + .ResolveAsync(async ctx => + { + var id = ctx.GetArgument("id"); + var repository = ctx.RequestServices!.GetRequiredService(); + var companies = await repository.GetByIds([CompanyId.With(id)], ctx.CancellationToken); + return companies.FirstOrDefault(); + }); + } +} diff --git a/backend/Books.Api/GraphQL/Types/CompanyType.cs b/backend/Books.Api/GraphQL/Types/CompanyType.cs new file mode 100644 index 0000000..7f5a075 --- /dev/null +++ b/backend/Books.Api/GraphQL/Types/CompanyType.cs @@ -0,0 +1,31 @@ +using Books.Api.EventFlow.ReadModels; +using GraphQL.Types; + +namespace Books.Api.GraphQL.Types; + +public class CompanyType : ObjectGraphType +{ + public CompanyType() + { + Name = "Company"; + Description = "A company/organization for bookkeeping"; + + Field(x => x.Id).Description("Unique identifier"); + Field(x => x.Name).Description("Company name"); + Field(x => x.Cvr, nullable: true).Description("Danish CVR number"); + Field(x => x.Address, nullable: true).Description("Street address"); + Field(x => x.PostalCode, nullable: true).Description("Postal code"); + Field(x => x.City, nullable: true).Description("City"); + Field(x => x.Country).Description("Country code (e.g., DK)"); + Field(x => x.FiscalYearStartMonth).Description("Month when fiscal year starts (1-12)"); + Field(x => x.Currency).Description("Default currency (e.g., DKK)"); + Field(x => x.VatRegistered).Description("Whether the company is VAT registered"); + Field(x => x.VatPeriodFrequency, nullable: true).Description("VAT reporting frequency"); + Field(x => x.CreatedAt).Description("When the company was created"); + Field(x => x.UpdatedAt).Description("When the company was last updated"); + + // Nested fields will be added later: + // - fiscalYears: [FiscalYearType] via DataLoader + // - accounts: [AccountType] via DataLoader + } +} diff --git a/backend/Books.Api/Infrastructure/HangfireScheduler.cs b/backend/Books.Api/Infrastructure/HangfireScheduler.cs new file mode 100644 index 0000000..5524a5c --- /dev/null +++ b/backend/Books.Api/Infrastructure/HangfireScheduler.cs @@ -0,0 +1,27 @@ +using System.Linq.Expressions; +using Hangfire; + +namespace Books.Api.Infrastructure; + +public class HangfireScheduler(IBackgroundJobClient jobClient, IRecurringJobManager recurringJobManager) : IScheduler +{ + public void EnqueueJob(Expression> methodCall) + { + jobClient.Enqueue(methodCall); + } + + public void EnqueueJob(Expression> methodCall, TimeSpan delay) + { + jobClient.Schedule(methodCall, delay); + } + + public void EnqueueJob(Expression> methodCall) + { + jobClient.Enqueue(methodCall); + } + + public void AddOrUpdateScheduledJob(string title, Expression> methodCall, string cron) + { + recurringJobManager.AddOrUpdate(title, methodCall, cron); + } +} diff --git a/backend/Books.Api/Infrastructure/IScheduler.cs b/backend/Books.Api/Infrastructure/IScheduler.cs new file mode 100644 index 0000000..d88ffb1 --- /dev/null +++ b/backend/Books.Api/Infrastructure/IScheduler.cs @@ -0,0 +1,11 @@ +using System.Linq.Expressions; + +namespace Books.Api.Infrastructure; + +public interface IScheduler +{ + void EnqueueJob(Expression> methodCall); + void EnqueueJob(Expression> methodCall, TimeSpan delay); + void EnqueueJob(Expression> methodCall); + void AddOrUpdateScheduledJob(string title, Expression> methodCall, string cron); +} diff --git a/backend/Books.Api/Logging/ServiceCollectionExtensions.cs b/backend/Books.Api/Logging/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..0f13ae8 --- /dev/null +++ b/backend/Books.Api/Logging/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +namespace Books.Api.Logging; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection DecorateAsyncEventHandlersWithLogging(this IServiceCollection services) + { + // Decoration will be set up once we have event handlers registered + // The SubscribeAsynchronousToDecorator wraps handlers with logging + // This is a placeholder for now - implement when we have actual event handlers + + return services; + } +} diff --git a/backend/Books.Api/Logging/SubscribeAsynchronousToDecorator.cs b/backend/Books.Api/Logging/SubscribeAsynchronousToDecorator.cs new file mode 100644 index 0000000..c329e3a --- /dev/null +++ b/backend/Books.Api/Logging/SubscribeAsynchronousToDecorator.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using EventFlow.Aggregates; +using EventFlow.Core; +using EventFlow.Subscribers; + +namespace Books.Api.Logging; + +public class SubscribeAsynchronousToDecorator( + ISubscribeAsynchronousTo inner, + ILogger> logger) + : ISubscribeAsynchronousTo + where TAggregate : IAggregateRoot + where TIdentity : IIdentity + where TEvent : IAggregateEvent +{ + public async Task HandleAsync( + IDomainEvent domainEvent, + CancellationToken cancellationToken) + { + var eventName = typeof(TEvent).Name; + var handlerName = inner.GetType().Name; + var aggregateId = domainEvent.AggregateIdentity.Value; + + logger.LogDebug( + "Handling {EventName} for {AggregateId} with {HandlerName}", + eventName, aggregateId, handlerName); + + var stopwatch = Stopwatch.StartNew(); + + try + { + await inner.HandleAsync(domainEvent, cancellationToken); + + stopwatch.Stop(); + logger.LogInformation( + "Handled {EventName} for {AggregateId} with {HandlerName} in {ElapsedMs}ms", + eventName, aggregateId, handlerName, stopwatch.ElapsedMilliseconds); + } + catch (Exception ex) + { + stopwatch.Stop(); + logger.LogError(ex, + "Failed to handle {EventName} for {AggregateId} with {HandlerName} after {ElapsedMs}ms", + eventName, aggregateId, handlerName, stopwatch.ElapsedMilliseconds); + throw; + } + } +} diff --git a/backend/Books.Api/Program.cs b/backend/Books.Api/Program.cs new file mode 100644 index 0000000..45b0a2b --- /dev/null +++ b/backend/Books.Api/Program.cs @@ -0,0 +1,35 @@ +using Books.Api; +using Books.Api.GraphQL; +using GraphQL; +using GraphQL.Server.Ui.Altair; +using Hangfire; + +// Enable legacy timestamp behavior for Npgsql 6.0+ +// This allows DateTimeOffset with non-UTC offsets to be written to timestamptz columns +AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + +var builder = WebApplication.CreateBuilder(args); + +// Configure EventFlow, Hangfire, GraphQL and all services +Startup.ConfigureServices(builder.Services, builder.Configuration, builder.Environment); + +var app = builder.Build(); + +// Configure the HTTP request pipeline +if (app.Environment.IsDevelopment()) +{ + app.UseHangfireDashboard(); +} + +app.UseHttpsRedirection(); + +// GraphQL endpoint +app.UseGraphQL("/graphql"); + +// GraphQL UI (development only) - use external tools like Altair, Insomnia, or GraphiQL +if (app.Environment.IsDevelopment()) +{ + app.UseGraphQLAltair("/graphql/ui"); +} + +app.Run(); diff --git a/backend/Books.Api/Properties/launchSettings.json b/backend/Books.Api/Properties/launchSettings.json new file mode 100644 index 0000000..39a6f26 --- /dev/null +++ b/backend/Books.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5142", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7141;http://localhost:5142", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/backend/Books.Api/Startup.cs b/backend/Books.Api/Startup.cs new file mode 100644 index 0000000..040562e --- /dev/null +++ b/backend/Books.Api/Startup.cs @@ -0,0 +1,77 @@ +using Books.Api.EventFlow.Extensions; +using Books.Api.EventFlow.Infrastructure; +using Books.Api.GraphQL; +using Books.Api.Infrastructure; +using Books.Api.Logging; +using EventFlow; +using EventFlow.Configuration; +using EventFlow.Extensions; +using EventFlow.Hangfire.Extensions; +using EventFlow.PostgreSql.Connections; +using EventFlow.PostgreSql.Extensions; +using EventFlow.ReadStores; +using EventFlow.Subscribers; +using GraphQL; +using Hangfire; +using Hangfire.PostgreSql; +using Npgsql; + +namespace Books.Api; + +public static class Startup +{ + public static void ConfigureServices(IServiceCollection services, IConfiguration config, IHostEnvironment? environment = null) + { + var connectionString = config.GetConnectionString("Default") + ?? throw new InvalidOperationException("Connection string 'Default' not found"); + + // Run database migrations (skipped in Test environment where migrations run separately with test connection string) + var isTestEnvironment = environment?.EnvironmentName == "Test"; + if (!isTestEnvironment) + { + DatabaseMigrator.Migrate(connectionString); + } + + // PostgreSQL data source + var dataSource = new NpgsqlDataSourceBuilder(connectionString).Build(); + services.AddSingleton(dataSource); + + // Hangfire + services.AddHangfire(c => c + .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UsePostgreSqlStorage(o => o.UseNpgsqlConnection(connectionString))); + services.AddHangfireServer(); + + // Scheduler abstraction over Hangfire + services.AddSingleton(); + + // EventFlow + services.AddEventFlow(o => o + .UsePostgreSqlEventStore() + .ConfigurePostgreSql(PostgreSqlConfiguration.New.SetConnectionString(connectionString)) + .AddDefaults(typeof(Startup).Assembly) + .AddReadModels() + .Configure(c => c.IsAsynchronousSubscribersEnabled = true) + .UseHangfireJobScheduler()); + + // Resilience strategies + services.AddSingleton(); + services.AddSingleton(); + + // Read model repositories + services.AddRepositories(); + + // Logging decorators + services.DecorateAsyncEventHandlersWithLogging(); + + // GraphQL + services.AddGraphQL(builder => builder + .AddSchema() + .AddSystemTextJson() + .AddDataLoader() + .AddGraphTypes(typeof(BooksSchema).Assembly) + .AddErrorInfoProvider(opt => opt.ExposeExceptionDetails = true)); + } +} diff --git a/backend/Books.Api/WeatherForecast.cs b/backend/Books.Api/WeatherForecast.cs new file mode 100644 index 0000000..badd639 --- /dev/null +++ b/backend/Books.Api/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Books.Api; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/backend/Books.Api/appsettings.json b/backend/Books.Api/appsettings.json new file mode 100644 index 0000000..ac4abfb --- /dev/null +++ b/backend/Books.Api/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "EventFlow": "Information", + "Hangfire": "Information" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "Default": "Host=localhost;Database=books;Username=postgres;Password=postgres;Include Error Detail=true" + } +} diff --git a/backend/Books.slnx b/backend/Books.slnx new file mode 100644 index 0000000..0585fb9 --- /dev/null +++ b/backend/Books.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..54a713c --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Bogfoering - Regnskabssystem + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..08e575f --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6459 @@ +{ + "name": "books", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "books", + "version": "1.0.0", + "dependencies": { + "@ant-design/charts": "^2.2.1", + "@ant-design/icons": "^5.5.1", + "@tanstack/react-query": "^5.62.7", + "antd": "^5.22.3", + "dayjs": "^1.11.13", + "graphql": "^16.9.0", + "graphql-request": "^7.1.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.1", + "xlsx": "^0.18.5", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/node": "^22.10.2", + "@types/react": "^18.3.17", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.17.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.14.0", + "typescript": "~5.6.2", + "typescript-eslint": "^8.18.2", + "vite": "^6.0.5" + } + }, + "node_modules/@ant-design/charts": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/@ant-design/charts/-/charts-2.6.7.tgz", + "integrity": "sha512-XfmsnspUpfrMlRFGTwmHJ2TPKcosq5a5nSxAfIOpEXAvmJBT2N16oejGTZhUFTzba8W3XtBOziwRAXmDmLUqvA==", + "license": "MIT", + "dependencies": { + "@ant-design/graphs": "^2.1.1", + "@ant-design/plots": "^2.6.7", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, + "node_modules/@ant-design/charts-util": { + "version": "0.0.1-alpha.7", + "resolved": "https://registry.npmjs.org/@ant-design/charts-util/-/charts-util-0.0.1-alpha.7.tgz", + "integrity": "sha512-Yh0o6EdO6SvdSnStFZMbnUzjyymkVzV+TQ9ymVW9hlVgO/fUkUII3JYSdV+UVcFnYwUF0YiDKuSTLCZNAzg2bQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, + "node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", + "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/graphs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@ant-design/graphs/-/graphs-2.1.1.tgz", + "integrity": "sha512-qT3Oo8BWeoAmZEy9gfR6uIk+rczbNJ3sWXKonoOD5koATWv7dY0kgvS1JnhdM1QW4FkfPPJTeQVSlRRUtvWDwA==", + "license": "MIT", + "dependencies": { + "@ant-design/charts-util": "0.0.1-alpha.7", + "@antv/g6": "^5.0.44", + "@antv/g6-extension-react": "^0.2.0", + "@antv/graphin": "^3.0.4", + "lodash": "^4.17.21", + "styled-components": "^6.1.15" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/plots": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/@ant-design/plots/-/plots-2.6.8.tgz", + "integrity": "sha512-QsunUs2d5rbq/1BwVhga/siA5H50OaG23YopMYwPD4sPsza6NQzPQ8FM3elNIsD/BIk298tihqX1cJ/MmvVJbQ==", + "license": "MIT", + "dependencies": { + "@ant-design/charts-util": "0.0.3", + "@antv/event-emitter": "^0.1.3", + "@antv/g": "^6.1.7", + "@antv/g2": "^5.2.7", + "@antv/g2-extension-plot": "^0.2.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, + "node_modules/@ant-design/plots/node_modules/@ant-design/charts-util": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@ant-design/charts-util/-/charts-util-0.0.3.tgz", + "integrity": "sha512-x1H7UT6t4dXAyGRoHqlOnEsEqBSTANFGTZEAMI0CWYhYUpp13n0o9grl9oPtoL6FEQMjUBTY+zGJKlHkz8smMw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">=16.8.4", + "react-dom": ">=16.8.4" + } + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@antv/algorithm": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@antv/algorithm/-/algorithm-0.1.26.tgz", + "integrity": "sha512-DVhcFSQ8YQnMNW34Mk8BSsfc61iC1sAnmcfYoXTAshYHuU50p/6b7x3QYaGctDNKWGvi1ub7mPcSY0bK+aN0qg==", + "license": "MIT", + "dependencies": { + "@antv/util": "^2.0.13", + "tslib": "^2.0.0" + } + }, + "node_modules/@antv/algorithm/node_modules/@antv/util": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-2.0.17.tgz", + "integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==", + "license": "ISC", + "dependencies": { + "csstype": "^3.0.8", + "tslib": "^2.0.3" + } + }, + "node_modules/@antv/component": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@antv/component/-/component-2.1.11.tgz", + "integrity": "sha512-dTdz8VAd3rpjOaGEZTluz82mtzrP4XCtNlNQyrxY7VNRNcjtvpTLDn57bUL2lRu1T+iklKvgbE2llMriWkq9vQ==", + "license": "MIT", + "dependencies": { + "@antv/g": "^6.1.11", + "@antv/scale": "^0.4.16", + "@antv/util": "^3.3.10", + "svg-path-parser": "^1.1.0" + } + }, + "node_modules/@antv/component/node_modules/@antv/scale": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.4.16.tgz", + "integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.7", + "color-string": "^1.5.5", + "fecha": "^4.2.1" + } + }, + "node_modules/@antv/coord": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@antv/coord/-/coord-0.4.7.tgz", + "integrity": "sha512-UTbrMLhwJUkKzqJx5KFnSRpU3BqrdLORJbwUbHK2zHSCT3q3bjcFA//ZYLVfIlwqFDXp/hzfMyRtp0c77A9ZVA==", + "license": "MIT", + "dependencies": { + "@antv/scale": "^0.4.12", + "@antv/util": "^2.0.13", + "gl-matrix": "^3.4.3" + } + }, + "node_modules/@antv/coord/node_modules/@antv/scale": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.4.16.tgz", + "integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.7", + "color-string": "^1.5.5", + "fecha": "^4.2.1" + } + }, + "node_modules/@antv/coord/node_modules/@antv/scale/node_modules/@antv/util": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz", + "integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "gl-matrix": "^3.3.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@antv/coord/node_modules/@antv/util": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-2.0.17.tgz", + "integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==", + "license": "ISC", + "dependencies": { + "csstype": "^3.0.8", + "tslib": "^2.0.3" + } + }, + "node_modules/@antv/event-emitter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@antv/event-emitter/-/event-emitter-0.1.3.tgz", + "integrity": "sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==", + "license": "MIT" + }, + "node_modules/@antv/expr": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@antv/expr/-/expr-1.0.2.tgz", + "integrity": "sha512-vrfdmPHkTuiS5voVutKl2l06w1ihBh9A8SFdQPEE+2KMVpkymzGOF1eWpfkbGZ7tiFE15GodVdhhHomD/hdIwg==", + "license": "MIT" + }, + "node_modules/@antv/g": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@antv/g/-/g-6.3.1.tgz", + "integrity": "sha512-WYEKqy86LHB2PzTmrZXrIsIe+3Epeds2f68zceQ+BJtRoGki7Sy4IhlC8LrUMztgfT1t3d/0L745NWZwITroKA==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "html2canvas": "^1.4.1" + } + }, + "node_modules/@antv/g-canvas": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@antv/g-canvas/-/g-canvas-2.2.0.tgz", + "integrity": "sha512-h7zVBBo2aO64DuGKvq9sG+yTU3sCUb9DALCVm7nz8qGPs8hhLuFOkKPEzUDNfNYZGJUGzY8UDtJ3QRGRFcvEQg==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/g-math": "3.1.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-lite": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@antv/g-lite/-/g-lite-2.7.0.tgz", + "integrity": "sha512-uSzgHYa5bwR5L2Au7/5tsOhFmXKZKLPBH90+Q9bP9teVs5VT4kOAi0isPSpDI8uhdDC2/VrfTWu5K9HhWI6FWw==", + "license": "MIT", + "dependencies": { + "@antv/g-math": "3.1.0", + "@antv/util": "^3.3.5", + "@antv/vendor": "^1.0.3", + "@babel/runtime": "^7.25.6", + "eventemitter3": "^5.0.1", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@antv/g-math/-/g-math-3.1.0.tgz", + "integrity": "sha512-DtN1Gj/yI0UiK18nSBsZX8RK0LszGwqfb+cBYWgE+ddyTm8dZnW4tPUhV7QXePsS6/A5hHC+JFpAAK7OEGo5ZQ==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-plugin-dragndrop": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@antv/g-plugin-dragndrop/-/g-plugin-dragndrop-2.1.1.tgz", + "integrity": "sha512-+aesDUJVQDs6UJ2bOBbDlaGAPCfHmU0MbrMTlQlfpwNplWueqtgVAZ3L57oZ2ZGHRWUHiRwZGPjXMBM3O2LELw==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g-svg": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@antv/g-svg/-/g-svg-2.1.1.tgz", + "integrity": "sha512-gVzBkjqA8FzDTbkuIxj6L0Omz/X/hFbYLzK6alWr0sHTfywqP6czcjDUJU8DF2MRIY1Twy55uZYW4dqqLXOXXg==", + "license": "MIT", + "dependencies": { + "@antv/g-lite": "2.7.0", + "@antv/util": "^3.3.5", + "@babel/runtime": "^7.25.6", + "gl-matrix": "^3.4.3", + "tslib": "^2.5.3" + } + }, + "node_modules/@antv/g2": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@antv/g2/-/g2-5.4.8.tgz", + "integrity": "sha512-IvgIpwmT4M5/QAd3Mn2WiHIDeBqFJ4WA2gcZhRRSZuZ2KmgCqZWZwwIT0hc+kIGxwYeDoCQqf//t6FMVu3ryBg==", + "license": "MIT", + "dependencies": { + "@antv/component": "^2.1.9", + "@antv/coord": "^0.4.7", + "@antv/event-emitter": "^0.1.3", + "@antv/expr": "^1.0.2", + "@antv/g": "^6.1.24", + "@antv/g-canvas": "^2.0.43", + "@antv/g-plugin-dragndrop": "^2.0.35", + "@antv/scale": "^0.5.1", + "@antv/util": "^3.3.10", + "@antv/vendor": "^1.0.11", + "flru": "^1.0.2", + "pdfast": "^0.2.0" + } + }, + "node_modules/@antv/g2-extension-plot": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@antv/g2-extension-plot/-/g2-extension-plot-0.2.2.tgz", + "integrity": "sha512-KJXCXO7as+h0hDqirGXf1omrNuYzQmY3VmBmp7lIvkepbQ7sz3pPwy895r1FWETGF3vTk5UeFcAF5yzzBHWgbw==", + "dependencies": { + "@antv/g2": "^5.1.8", + "@antv/util": "^3.3.5", + "@antv/vendor": "^1.0.10" + } + }, + "node_modules/@antv/g6": { + "version": "5.0.51", + "resolved": "https://registry.npmjs.org/@antv/g6/-/g6-5.0.51.tgz", + "integrity": "sha512-/88LJDZ7FHKtpyJibXOnJWZ8gFRp32mLb8KzEFrMuiIC/dsZgTf/oYVw6L4tLKooPXfXqUtrJb2tWFMGR04EMg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@antv/algorithm": "^0.1.26", + "@antv/component": "^2.1.7", + "@antv/event-emitter": "^0.1.3", + "@antv/g": "^6.1.28", + "@antv/g-canvas": "^2.0.48", + "@antv/g-plugin-dragndrop": "^2.0.38", + "@antv/graphlib": "^2.0.4", + "@antv/hierarchy": "^0.7.1", + "@antv/layout": "1.2.14-beta.9", + "@antv/util": "^3.3.11", + "bubblesets-js": "^2.3.4" + } + }, + "node_modules/@antv/g6-extension-react": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@antv/g6-extension-react/-/g6-extension-react-0.2.6.tgz", + "integrity": "sha512-JWOiWMz/r4jG+Nn2Y28LfohpxfUaf9M/0brLdKBshSVa4DraQFfQvA9OTIbzahLLoxIXsKKG2KteQ9QcXL26Kw==", + "license": "MIT", + "dependencies": { + "@antv/g": "^6.1.24", + "@antv/g-svg": "^2.0.38" + }, + "peerDependencies": { + "@antv/g6": "^5.0.50", + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@antv/graphin": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@antv/graphin/-/graphin-3.0.5.tgz", + "integrity": "sha512-V/j8R8Ty44wUqxVIYLdpPuIO8WWCTIVq1eBJg5YRunL5t5o5qAFpC/qkQxslbBMWyKdIH0oWBnvHA74riGi7cw==", + "license": "MIT", + "dependencies": { + "@antv/g6": "^5.0.28" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.1.0", + "react-dom": "^18.0.0 || ^19.1.0" + } + }, + "node_modules/@antv/graphlib": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@antv/graphlib/-/graphlib-2.0.4.tgz", + "integrity": "sha512-zc/5oQlsdk42Z0ib1mGklwzhJ5vczLFiPa1v7DgJkTbgJ2YxRh9xdarf86zI49sKVJmgbweRpJs7Nu5bIiwv4w==", + "license": "MIT", + "dependencies": { + "@antv/event-emitter": "^0.1.3" + } + }, + "node_modules/@antv/hierarchy": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@antv/hierarchy/-/hierarchy-0.7.1.tgz", + "integrity": "sha512-7r22r+HxfcRZp79ZjGmsn97zgC1Iajrv0Mm9DIgx3lPfk+Kme2MG/+EKdZj1iEBsN0rJRzjWVPGL5YrBdVHchw==", + "license": "MIT" + }, + "node_modules/@antv/layout": { + "version": "1.2.14-beta.9", + "resolved": "https://registry.npmjs.org/@antv/layout/-/layout-1.2.14-beta.9.tgz", + "integrity": "sha512-wPlwBFMtq2lWZFc89/7Lzb8fjHnyKVZZ9zBb2h+zZIP0YWmVmHRE8+dqCiPKOyOGUXEdDtn813f1g107dCHZlg==", + "license": "MIT", + "dependencies": { + "@antv/event-emitter": "^0.1.3", + "@antv/graphlib": "^2.0.0", + "@antv/util": "^3.3.2", + "@naoak/workerize-transferable": "^0.1.0", + "comlink": "^4.4.1", + "d3-force": "^3.0.0", + "d3-force-3d": "^3.0.5", + "d3-octree": "^1.0.2", + "d3-quadtree": "^3.0.1", + "dagre": "^0.8.5", + "ml-matrix": "^6.10.4", + "tslib": "^2.5.0" + } + }, + "node_modules/@antv/scale": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.5.2.tgz", + "integrity": "sha512-rTHRAwvpHWC5PGZF/mJ2ZuTDqwwvVBDRph0Uu5PV9BXwzV7K8+9lsqGJ+XHVLxe8c6bKog5nlzvV/dcYb0d5Ow==", + "license": "MIT", + "dependencies": { + "@antv/util": "^3.3.7", + "color-string": "^1.5.5", + "fecha": "^4.2.1" + } + }, + "node_modules/@antv/util": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz", + "integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "gl-matrix": "^3.3.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@antv/vendor": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@antv/vendor/-/vendor-1.0.11.tgz", + "integrity": "sha512-LmhPEQ+aapk3barntaiIxJ5VHno/Tyab2JnfdcPzp5xONh/8VSfed4bo/9xKo5HcUAEydko38vYLfj6lJliLiw==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.2.1", + "@types/d3-color": "^3.1.3", + "@types/d3-dispatch": "^3.0.6", + "@types/d3-dsv": "^3.0.7", + "@types/d3-ease": "^3.0.2", + "@types/d3-fetch": "^3.0.7", + "@types/d3-force": "^3.0.10", + "@types/d3-format": "^3.0.4", + "@types/d3-geo": "^3.1.0", + "@types/d3-hierarchy": "^3.1.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-path": "^3.1.0", + "@types/d3-quadtree": "^3.0.6", + "@types/d3-random": "^3.0.3", + "@types/d3-scale": "^4.0.9", + "@types/d3-scale-chromatic": "^3.1.0", + "@types/d3-shape": "^3.1.7", + "@types/d3-time": "^3.0.4", + "@types/d3-timer": "^3.0.2", + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-dispatch": "^3.0.1", + "d3-dsv": "^3.0.1", + "d3-ease": "^3.0.1", + "d3-fetch": "^3.0.1", + "d3-force": "^3.0.0", + "d3-force-3d": "^3.0.5", + "d3-format": "^3.1.0", + "d3-geo": "^3.1.1", + "d3-geo-projection": "^4.0.0", + "d3-hierarchy": "^3.1.2", + "d3-interpolate": "^3.0.1", + "d3-path": "^3.1.0", + "d3-quadtree": "^3.0.1", + "d3-random": "^3.0.1", + "d3-regression": "^1.3.10", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.1.0", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@naoak/workerize-transferable": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@naoak/workerize-transferable/-/workerize-transferable-0.1.0.tgz", + "integrity": "sha512-fDLfuP71IPNP5+zSfxFb52OHgtjZvauRJWbVnpzQ7G7BjcbLjTny0OW1d3ZO806XKpLWNKmeeW3MhE0sy8iwYQ==", + "license": "MIT", + "peerDependencies": { + "workerize-loader": "*" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz", + "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.18", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.18.tgz", + "integrity": "sha512-rbGx6bHgPNVzutP7BEr+53UPKohpckqlMAad+To9UxTbeaQ+kC/1SDRj+QzkwbQ7qhLT/1IKp34yS6thda6fzA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.18", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.18.tgz", + "integrity": "sha512-KqNZX0C5IFz4639zR1ilnQ288tQdJrMNLtzmlzyJ14xauBkhtLEy3mPU/V4KiHsr41eL1ILZbDP36TB12lYfCQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/stylis": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz", + "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", + "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/type-utils": "8.53.0", + "@typescript-eslint/utils": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.53.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz", + "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", + "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.0", + "@typescript-eslint/types": "^8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", + "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", + "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz", + "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/utils": "8.53.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", + "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", + "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.0", + "@typescript-eslint/tsconfig-utils": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz", + "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", + "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd": { + "version": "5.29.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz", + "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.2.1", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.1.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.3.0", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.3.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.1", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.8", + "rc-slider": "~11.1.9", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.54.0", + "rc-tabs": "~15.7.0", + "rc-textarea": "~1.10.2", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.11.0", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", + "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bubblesets-js": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/bubblesets-js/-/bubblesets-js-2.3.4.tgz", + "integrity": "sha512-DyMjHmpkS2+xcFNtyN00apJYL3ESdp9fTrkDr5+9Qg/GPqFmcWgGsK1akZnttE1XFxJ/VMy4DNNGMGYtmFp1Sg==", + "license": "MIT" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/comlink": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", + "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", + "license": "Apache-2.0" + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-3d": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", + "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", + "license": "MIT", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", + "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", + "license": "ISC", + "dependencies": { + "commander": "7", + "d3-array": "1 - 3", + "d3-geo": "1.12.0 - 3" + }, + "bin": { + "geo2svg": "bin/geo2svg.js", + "geograticule": "bin/geograticule.js", + "geoproject": "bin/geoproject.js", + "geoquantize": "bin/geoquantize.js", + "geostitch": "bin/geostitch.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-octree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", + "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", + "license": "MIT" + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-regression": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/d3-regression/-/d3-regression-1.3.10.tgz", + "integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT", + "peer": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "license": "ISC" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/flru": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", + "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/graphql": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", + "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-request": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.4.0.tgz", + "integrity": "sha512-xfr+zFb/QYbs4l4ty0dltqiXIp07U6sl+tOKAb0t50/EnQek6CVVBLjETXi+FghElytvgaAWtIOt3EV7zLzIAQ==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-any-array": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-2.0.1.tgz", + "integrity": "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==", + "license": "MIT" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ml-array-max": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-1.2.4.tgz", + "integrity": "sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.0" + } + }, + "node_modules/ml-array-min": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-1.2.3.tgz", + "integrity": "sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.0" + } + }, + "node_modules/ml-array-rescale": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-1.3.7.tgz", + "integrity": "sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.0", + "ml-array-max": "^1.2.4", + "ml-array-min": "^1.2.3" + } + }, + "node_modules/ml-matrix": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.12.1.tgz", + "integrity": "sha512-TJ+8eOFdp+INvzR4zAuwBQJznDUfktMtOB6g/hUcGh3rcyjxbz4Te57Pgri8Q9bhSQ7Zys4IYOGhFdnlgeB6Lw==", + "license": "MIT", + "dependencies": { + "is-any-array": "^2.0.1", + "ml-array-rescale": "^1.3.7" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfast": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/pdfast/-/pdfast-0.2.0.tgz", + "integrity": "sha512-cq6TTu6qKSFUHwEahi68k/kqN2mfepjkGrG9Un70cgdRRKLKY6Rf8P8uvP2NvZktaQZNF3YE7agEkLj0vGK9bA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.1.tgz", + "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz", + "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.1.tgz", + "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz", + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.54.0", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.54.0.tgz", + "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz", + "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz", + "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.11.0.tgz", + "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-virtual-list": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz", + "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "license": "MIT", + "dependencies": { + "react-router": "7.12.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-components": { + "version": "6.3.8", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.8.tgz", + "integrity": "sha512-Kq/W41AKQloOqKM39zfaMdJ4BcYDw/N5CIq4/GTI0YjU6pKcZ1KKhk6b4du0a+6RA9pIfOP/eu94Ge7cu+PDCA==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.4.0", + "@emotion/unitless": "0.10.0", + "@types/stylis": "4.2.7", + "css-to-react-native": "3.2.0", + "csstype": "3.2.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.6", + "tslib": "2.8.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/styled-components/node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svg-path-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/svg-path-parser/-/svg-path-parser-1.1.0.tgz", + "integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz", + "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.53.0", + "@typescript-eslint/parser": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/utils": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", + "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.4", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerize-loader": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/workerize-loader/-/workerize-loader-2.0.2.tgz", + "integrity": "sha512-HoZ6XY4sHWxA2w0WpzgBwUiR3dv1oo7bS+oCwIpb6n54MclQ/7KXdXsVIChTCygyuHtVuGBO1+i3HzTt699UJQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loader-utils": "^2.0.0" + }, + "peerDependencies": { + "webpack": "*" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz", + "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..6503d58 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,40 @@ +{ + "name": "books", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/charts": "^2.2.1", + "@ant-design/icons": "^5.5.1", + "@tanstack/react-query": "^5.62.7", + "antd": "^5.22.3", + "dayjs": "^1.11.13", + "graphql": "^16.9.0", + "graphql-request": "^7.1.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.1", + "xlsx": "^0.18.5", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/node": "^22.10.2", + "@types/react": "^18.3.17", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.17.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.14.0", + "typescript": "~5.6.2", + "typescript-eslint": "^8.18.2", + "vite": "^6.0.5" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..f5d1dbc --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,18 @@ +import { BrowserRouter } from 'react-router-dom'; +import { App as AntApp } from 'antd'; +import AppRoutes from './routes'; +import AppLayout from './components/layout/AppLayout'; + +function App() { + return ( + + + + + + + + ); +} + +export default App; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..401d79e --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,73 @@ +import { GraphQLClient } from 'graphql-request'; +import { QueryClient } from '@tanstack/react-query'; + +// GraphQL endpoint - configure based on environment +const GRAPHQL_ENDPOINT = import.meta.env.VITE_GRAPHQL_ENDPOINT || 'http://localhost:5000/graphql'; + +// Create GraphQL client +export const graphqlClient = new GraphQLClient(GRAPHQL_ENDPOINT, { + headers: { + // Add auth headers here when authentication is implemented + // 'Authorization': `Bearer ${token}`, + }, +}); + +// Configure headers dynamically (for auth tokens, etc.) +export const setAuthHeader = (token: string) => { + graphqlClient.setHeader('Authorization', `Bearer ${token}`); +}; + +export const removeAuthHeader = () => { + graphqlClient.setHeader('Authorization', ''); +}; + +// Create TanStack Query client with default options +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Cache data for 5 minutes + staleTime: 5 * 60 * 1000, + // Keep unused data in cache for 30 minutes + gcTime: 30 * 60 * 1000, + // Retry failed requests 3 times + retry: 3, + // Retry delay with exponential backoff + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + // Refetch on window focus for fresh data + refetchOnWindowFocus: true, + }, + mutations: { + // Retry mutations once + retry: 1, + }, + }, +}); + +// Helper function to make GraphQL requests with error handling +export async function fetchGraphQL>( + query: string, + variables?: TVariables +): Promise { + try { + const data = await graphqlClient.request(query, variables); + return data; + } catch (error) { + // Log error for debugging + console.error('GraphQL Error:', error); + + // Re-throw with more context + if (error instanceof Error) { + throw new Error(`GraphQL request failed: ${error.message}`); + } + throw error; + } +} + +// Type-safe query helper +export function createQueryKey(base: string, params?: Record): string[] { + const key = [base]; + if (params) { + key.push(JSON.stringify(params)); + } + return key; +} diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..9972cc2 --- /dev/null +++ b/frontend/src/components/layout/AppLayout.tsx @@ -0,0 +1,40 @@ +import { Layout } from 'antd'; +import { ReactNode } from 'react'; +import Sidebar from './Sidebar'; +import Header from './Header'; +import { useUIStore } from '@/stores/uiStore'; + +const { Content } = Layout; + +interface AppLayoutProps { + children: ReactNode; +} + +export default function AppLayout({ children }: AppLayoutProps) { + const sidebarCollapsed = useUIStore((state) => state.sidebarCollapsed); + + return ( + + + +
+ + {children} + + + + ); +} diff --git a/frontend/src/components/layout/CompanySwitcher.tsx b/frontend/src/components/layout/CompanySwitcher.tsx new file mode 100644 index 0000000..faa6db7 --- /dev/null +++ b/frontend/src/components/layout/CompanySwitcher.tsx @@ -0,0 +1,94 @@ +import { Select, Space, Typography, Tag } from 'antd'; +import { ShopOutlined } from '@ant-design/icons'; +import { useCompanyStore } from '@/stores/companyStore'; +import { formatCVR } from '@/lib/formatters'; +import type { Company } from '@/types/accounting'; + +const { Text } = Typography; + +// Mock data - will be replaced with API call +const mockCompanies: Company[] = [ + { + id: '1', + name: 'Demo Virksomhed ApS', + cvr: '12345678', + address: 'Hovedgaden 1', + city: 'Koebenhavn', + postalCode: '1000', + country: 'DK', + fiscalYearStart: 1, + currency: 'DKK', + createdAt: '2024-01-01', + updatedAt: '2024-01-01', + }, + { + id: '2', + name: 'Anden Virksomhed A/S', + cvr: '87654321', + address: 'Sidegaden 2', + city: 'Aarhus', + postalCode: '8000', + country: 'DK', + fiscalYearStart: 7, + currency: 'DKK', + createdAt: '2024-01-01', + updatedAt: '2024-01-01', + }, +]; + +export default function CompanySwitcher() { + const { activeCompany, setActiveCompany, setCompanies } = useCompanyStore(); + + // Initialize with mock data if needed + if (useCompanyStore.getState().companies.length === 0) { + setCompanies(mockCompanies); + if (!activeCompany) { + setActiveCompany(mockCompanies[0]); + } + } + + const companies = useCompanyStore((state) => state.companies); + + const handleCompanyChange = (companyId: string) => { + const company = companies.find((c) => c.id === companyId); + if (company) { + setActiveCompany(company); + } + }; + + return ( + + + ( + <> + {menu} + + + + + + + )} + options={sortedYears.map((year) => ({ + value: year.id, + label: `Regnskabsår ${year.name}`, + year, + }))} + optionRender={(option) => { + const year = option.data.year; + // Type guard - ensure year exists and has required properties + if (!year || typeof year !== 'object' || !('status' in year)) { + return null; + } + const fiscalYear = year as FiscalYear; + const statusConfig = STATUS_CONFIG[fiscalYear.status]; + + return ( + + + {fiscalYear.name} + + {statusConfig.label} + + + + {formatDateShort(fiscalYear.startDate)} - {formatDateShort(fiscalYear.endDate)} + + + ); + }} + /> + {currentFiscalYear && ( + + {STATUS_CONFIG[currentFiscalYear.status].label} + + )} + + {/* Create Fiscal Year Modal */} + + + ); +} diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx new file mode 100644 index 0000000..49654d0 --- /dev/null +++ b/frontend/src/components/layout/Header.tsx @@ -0,0 +1,113 @@ +import { Layout, Space, Button, Dropdown, Avatar, Typography, Divider } from 'antd'; +import { + UserOutlined, + LogoutOutlined, + SettingOutlined, + BellOutlined, + QuestionCircleOutlined, +} from '@ant-design/icons'; +import type { MenuProps } from 'antd'; +import CompanySwitcher from './CompanySwitcher'; +import FiscalYearSelector from './FiscalYearSelector'; + +const { Header: AntHeader } = Layout; +const { Text } = Typography; + +export default function Header() { + const userMenuItems: MenuProps['items'] = [ + { + key: 'profile', + icon: , + label: 'Min profil', + }, + { + key: 'settings', + icon: , + label: 'Indstillinger', + }, + { + type: 'divider', + }, + { + key: 'logout', + icon: , + label: 'Log ud', + danger: true, + }, + ]; + + const handleUserMenuClick: MenuProps['onClick'] = ({ key }) => { + switch (key) { + case 'logout': + // Handle logout + console.log('Logout clicked'); + break; + case 'settings': + // Navigate to settings + break; + case 'profile': + // Navigate to profile + break; + } + }; + + return ( + + {/* Left side - Company Switcher and Fiscal Year Selector */} + }> + + + + + {/* Right side - User actions */} + + {/* Help */} + + + + {/* Summary Cards */} + + + + formatCurrency(value as number)} + /> + + + + + formatCurrency(value as number)} + /> + + + + + formatCurrency(value as number)} + /> + + + + + formatCurrency(value as number)} + /> + + + + + {/* Main Content */} + + {/* Account Tree */} + + } + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + style={{ width: 200 }} + allowClear + /> + } + > + + + + + {/* Account Details */} + + {selectedAccount ? ( + + {selectedAccount.accountNumber} + {selectedAccount.name} + + } + size="small" + extra={ + + } + > + +
+ formatCurrency(value as number)} + valueStyle={{ + color: + selectedAccount.balance >= 0 + ? accountingColors.credit + : accountingColors.debit, + }} + /> +
+ String(index)} + size="small" + pagination={{ pageSize: 10 }} + /> + + ), + }, + { + key: 'info', + label: 'Kontooplysninger', + children: ( +
+ +
+ Kontonummer +
+ {selectedAccount.accountNumber} +
+ + + Kontotype +
+ {getAccountTypeName(selectedAccount.type)} +
+ + + Status +
+ {selectedAccount.isActive ? ( + Aktiv + ) : ( + Inaktiv + )} +
+ + + Momskode +
+ {selectedAccount.vatCode || 'Ingen'} +
+ + + + ), + }, + ]} + /> + + ) : ( + +
+ +
Vælg en konto for at se detaljer
+
+
+ )} + + + + {/* Create/Edit Account Modal */} + setIsModalOpen(false)} + onOk={handleSubmit} + okText="Gem" + cancelText="Annuller" + > +
+ + + + + + + + + + + + + + + + + + ({ + value: emp.id, + label: emp.name, + }))} + /> + + + + {/* Summary Cards */} + +
+ + } + /> + + + + + formatCurrency(value as number)} + /> + + + + + formatCurrency(value as number)} + valueStyle={{ color: accountingColors.debit }} + /> + + + + + formatCurrency(value as number)} + valueStyle={{ color: accountingColors.debit }} + /> + + + + + formatCurrency(value as number)} + valueStyle={{ color: accountingColors.debit }} + /> + + + + + formatCurrency(value as number)} + valueStyle={{ color: accountingColors.credit }} + /> + + + + + {/* Main Content */} + + + +
( + + + + I alt + + + + {formatCurrency(totalGross)} + + + + + -{formatCurrency(totalAMBidrag)} + + + + + -{formatCurrency(totalASkat)} + + + + + -{formatCurrency(totalATP)} + + + + + -{formatCurrency(totalPension)} + + + + + {formatCurrency(totalNet)} + + + + + + )} + /> + + + {/* Løn Forklaring */} + + + + AM-bidraget er på 8% af bruttolønnen og trækkes + før A-skat beregnes. + +
+ + Eksempel: Bruttoløn 45.000 kr. × 8% = 3.600 kr. + + + ), + }, + { + key: '2', + label: 'A-skat', + children: ( +
+ + A-skat beregnes af (bruttoløn - AM-bidrag) efter medarbejderens + skattekort. Skatteprocenten afhænger af personfradrag og + kommuneskat. + +
+ ), + }, + { + key: '3', + label: 'ATP (Arbejdsmarkedets Tillægspension)', + children: ( +
+ + ATP-bidraget er pt. 99,65 kr. pr. måned for + fuldtidsansatte. Arbejdsgiver betaler 2/3, lønmodtager betaler 1/3. + +
+ ), + }, + { + key: '4', + label: 'Pension', + children: ( +
+ + Pensionsbidraget afhænger af overenskomst eller individuel aftale. + Typisk mellem 8-17% af bruttolønnen, hvoraf arbejdsgiver ofte + betaler 2/3. + +
+ ), + }, + ]} + /> +
+ + + + {/* Trend Chart */} + + + + + {/* Cost Breakdown */} + +
+ Ud over bruttolønnen betaler arbejdsgiver: +
+ + + {formatCurrency(totalGross * 0.125)} + + + {formatCurrency(totalATP * 2)} + + + {formatCurrency(totalPension * 2)} + + + {formatCurrency(mockEmployees.length * 450)} + + +
+ Total lønomkostning (estimat): + + {formatCurrency( + totalGross * 1.125 + totalATP * 2 + totalPension * 2 + mockEmployees.length * 450 + )} + +
+
+ + + + ); +} diff --git a/frontend/src/pages/Momsindberetning.tsx b/frontend/src/pages/Momsindberetning.tsx new file mode 100644 index 0000000..6029d11 --- /dev/null +++ b/frontend/src/pages/Momsindberetning.tsx @@ -0,0 +1,574 @@ +import { useState } from 'react'; +import { + Typography, + Card, + Row, + Col, + Select, + DatePicker, + Button, + Table, + Statistic, + Tag, + Space, + Divider, + Alert, + Modal, + Descriptions, + message, +} from 'antd'; +import { + DownloadOutlined, + SendOutlined, + CheckCircleOutlined, + ClockCircleOutlined, + ExclamationCircleOutlined, +} from '@ant-design/icons'; +import { Pie } from '@ant-design/charts'; +import dayjs from 'dayjs'; +import { useCompany } from '@/hooks/useCompany'; +import { formatCurrency, formatDate, formatPeriod } from '@/lib/formatters'; +import { accountingColors } from '@/styles/theme'; + +const { Title, Text } = Typography; + +// Danish VAT boxes (Rubrikker) +interface VATBox { + boxNumber: number; + nameDanish: string; + nameEnglish: string; + description: string; + amount: number; + basis?: number; +} + +const mockVATReport: VATBox[] = [ + { + boxNumber: 1, + nameDanish: 'Salgsmoms', + nameEnglish: 'Output VAT', + description: 'Moms af varer og ydelser solgt i Danmark (25%)', + amount: 62500, + basis: 250000, + }, + { + boxNumber: 2, + nameDanish: 'Moms af varekøb i udlandet (EU)', + nameEnglish: 'VAT on goods from EU', + description: 'Erhvervelsesmoms ved køb af varer fra andre EU-lande', + amount: 5000, + basis: 20000, + }, + { + boxNumber: 3, + nameDanish: 'Moms af ydelseskøb i udlandet', + nameEnglish: 'VAT on services from abroad', + description: 'Moms ved køb af ydelser fra udlandet med omvendt betalingspligt', + amount: 2500, + basis: 10000, + }, + { + boxNumber: 4, + nameDanish: 'Købsmoms', + nameEnglish: 'Input VAT', + description: 'Fradragsberettiget moms af køb', + amount: 35000, + basis: 140000, + }, + { + boxNumber: 5, + nameDanish: 'Olie- og flaskegasafgift', + nameEnglish: 'Oil and gas duty', + description: 'Godtgørelse af olie- og flaskegasafgift', + amount: 0, + }, + { + boxNumber: 6, + nameDanish: 'Elafgift', + nameEnglish: 'Electricity duty', + description: 'Godtgørelse af elafgift', + amount: 1200, + }, + { + boxNumber: 7, + nameDanish: 'Naturgas- og bygasafgift', + nameEnglish: 'Natural gas duty', + description: 'Godtgørelse af naturgas- og bygasafgift', + amount: 0, + }, + { + boxNumber: 8, + nameDanish: 'Kulafgift', + nameEnglish: 'Coal duty', + description: 'Godtgørelse af kulafgift', + amount: 0, + }, + { + boxNumber: 9, + nameDanish: 'CO2-afgift', + nameEnglish: 'CO2 duty', + description: 'Godtgørelse af CO2-afgift', + amount: 300, + }, +]; + +// Historical submissions +const mockSubmissions = [ + { + id: '1', + period: '2024-10', + submittedAt: '2024-11-28', + status: 'accepted', + netVAT: 28500, + referenceNumber: 'SKAT-2024-123456', + }, + { + id: '2', + period: '2024-07', + submittedAt: '2024-08-30', + status: 'accepted', + netVAT: 32100, + referenceNumber: 'SKAT-2024-789012', + }, + { + id: '3', + period: '2024-04', + submittedAt: '2024-05-29', + status: 'accepted', + netVAT: -5600, + referenceNumber: 'SKAT-2024-345678', + }, +]; + +export default function Momsindberetning() { + const { company } = useCompany(); + const [selectedPeriod, setSelectedPeriod] = useState( + dayjs().subtract(1, 'month').startOf('month') + ); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); + const [periodType, setPeriodType] = useState<'monthly' | 'quarterly'>('quarterly'); + + // Calculate totals + const outputVAT = mockVATReport + .filter((box) => [1, 2, 3].includes(box.boxNumber)) + .reduce((sum, box) => sum + box.amount, 0); + + const inputVAT = mockVATReport + .filter((box) => box.boxNumber === 4) + .reduce((sum, box) => sum + box.amount, 0); + + const energyDuties = mockVATReport + .filter((box) => [5, 6, 7, 8, 9].includes(box.boxNumber)) + .reduce((sum, box) => sum + box.amount, 0); + + const netVAT = outputVAT - inputVAT - energyDuties; + + // Pie chart config + const pieData = [ + { type: 'Salgsmoms', value: mockVATReport[0].amount }, + { type: 'EU-moms', value: mockVATReport[1].amount + mockVATReport[2].amount }, + { type: 'Købsmoms (fradrag)', value: inputVAT }, + { type: 'Energiafgifter (fradrag)', value: energyDuties }, + ]; + + const pieConfig = { + data: pieData, + angleField: 'value', + colorField: 'type', + radius: 0.8, + innerRadius: 0.6, + label: { + type: 'outer', + formatter: (datum: { type: string; value: number }) => + `${datum.type}: ${formatCurrency(datum.value)}`, + }, + legend: { + position: 'bottom' as const, + }, + height: 250, + }; + + const columns = [ + { + dataIndex: 'boxNumber', + title: 'Rubrik', + width: 80, + render: (value: number) => {value}, + }, + { + dataIndex: 'nameDanish', + title: 'Felt', + render: (value: string, record: VATBox) => ( +
+ {value} +
+ + {record.description} + +
+ ), + }, + { + dataIndex: 'basis', + title: 'Grundlag', + align: 'right' as const, + width: 140, + render: (value: number | undefined) => + value !== undefined ? ( + {formatCurrency(value)} + ) : ( + '-' + ), + }, + { + dataIndex: 'amount', + title: 'Moms/Afgift', + align: 'right' as const, + width: 140, + render: (value: number, record: VATBox) => { + const isDeductible = record.boxNumber >= 4; + return ( + + {isDeductible ? '-' : ''} + {formatCurrency(value)} + + ); + }, + }, + ]; + + const handleSubmit = () => { + Modal.confirm({ + title: 'Indsend momsangivelse', + icon: , + content: ( +
+

Du er ved at indsende momsangivelse for:

+

+ Periode: {formatPeriod(selectedPeriod.toDate())} +

+

+ Moms til betaling:{' '} + = 0 ? accountingColors.debit : accountingColors.credit, + }} + > + {formatCurrency(Math.abs(netVAT))} + {netVAT < 0 ? ' (tilgode)' : ''} + +

+ +
+ ), + okText: 'Indsend til SKAT', + cancelText: 'Annuller', + onOk: () => { + message.success('Momsangivelse indsendt til SKAT'); + }, + }); + }; + + const getStatusTag = (status: string) => { + switch (status) { + case 'accepted': + return ( + }> + Godkendt + + ); + case 'pending': + return ( + }> + Afventer + + ); + case 'rejected': + return ( + }> + Afvist + + ); + default: + return {status}; + } + }; + + return ( +
+ {/* Header */} +
+
+ + Momsindberetning + + {company?.name} +
+ + + + +
+ + {/* Period Selection */} + + + Periode: +
( + + + + Moms til betaling / tilgode + + + = 0 ? accountingColors.debit : accountingColors.credit, + }} + > + {netVAT >= 0 ? '' : '-'} + {formatCurrency(Math.abs(netVAT))} + + + + + )} + /> + + + + + + + + + + {mockSubmissions.map((sub) => ( +
+
+
+ + {dayjs(sub.period, 'YYYY-MM').format('MMMM YYYY')} + +
+ + Indsendt {formatDate(sub.submittedAt)} + +
+
+ {getStatusTag(sub.status)} +
+ = 0 + ? accountingColors.debit + : accountingColors.credit, + }} + > + {formatCurrency(sub.netVAT)} + +
+
+
+ ))} +
+ + + + {/* Preview Modal */} + setIsPreviewOpen(false)} + width={700} + footer={[ + , + , + , + ]} + > + + {company?.name} + {company?.cvr} + + {formatPeriod(selectedPeriod.toDate())} + + + {selectedPeriod.endOf('month').format('D. MMMM YYYY')} + + + + + +
formatCurrency(v), + }, + ]} + rowKey="boxNumber" + pagination={false} + size="small" + /> + + + +
+ + {netVAT >= 0 ? 'Moms til betaling: ' : 'Moms til gode: '} + = 0 ? accountingColors.debit : accountingColors.credit, + }} + > + {formatCurrency(Math.abs(netVAT))} + + +
+ + + ); +} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx new file mode 100644 index 0000000..8109fd8 --- /dev/null +++ b/frontend/src/pages/Settings.tsx @@ -0,0 +1,422 @@ +import { + Typography, + Card, + Row, + Col, + Form, + Input, + Select, + Button, + Tabs, + Switch, + Divider, + message, + Space, + Tag, +} from 'antd'; +import { + SaveOutlined, + BuildOutlined, + UserOutlined, + BankOutlined, + SettingOutlined, +} from '@ant-design/icons'; +import { useCompany } from '@/hooks/useCompany'; + +const { Title, Text } = Typography; + +export default function Settings() { + const { company } = useCompany(); + const [companyForm] = Form.useForm(); + const [preferencesForm] = Form.useForm(); + + const handleSaveCompany = async () => { + try { + const values = await companyForm.validateFields(); + console.log('Saving company:', values); + message.success('Virksomhedsoplysninger gemt'); + } catch (error) { + console.error('Validation failed:', error); + } + }; + + const handleSavePreferences = async () => { + try { + const values = await preferencesForm.validateFields(); + console.log('Saving preferences:', values); + message.success('Præferencer gemt'); + } catch (error) { + console.error('Validation failed:', error); + } + }; + + const tabItems = [ + { + key: 'company', + label: ( + + Virksomhed + + ), + children: ( + + + +
+ + + + + + + + + + + + Adresse + + + + + + + + + + + + + + + + + + + + + + Regnskab + + + + + + + + + +
+ +
+ + + ), + }, + { + key: 'preferences', + label: ( + + Præferencer + + ), + children: ( + +
+ Moms + +
+ + + + + + +
+ +
+ + + ), + }, + { + key: 'bankAccounts', + label: ( + + Bankkonti + + ), + children: ( + + +
+ + Tilknyttede bankkonti + + +
+ + + + {/* Mock bank accounts */} + {[ + { + id: '1', + bankName: 'Danske Bank', + accountName: 'Erhvervskonto', + accountNumber: '1234-5678901234', + ledgerAccount: '1000 - Bank', + isActive: true, + }, + { + id: '2', + bankName: 'Nordea', + accountName: 'Opsparingskonto', + accountNumber: '9876-5432109876', + ledgerAccount: '1010 - Bank opsparing', + isActive: true, + }, + ].map((account) => ( + + +
+ + + {account.bankName} + {account.accountName} + {account.isActive && Aktiv} + + {account.accountNumber} + + Bogføringskonto: {account.ledgerAccount} + + + + + + + + + + + + ))} + + + ), + }, + { + key: 'users', + label: ( + + Brugere + + ), + children: ( + + +
+ + Brugere med adgang + + +
+ + + + {/* Mock users */} + {[ + { + id: '1', + name: 'Admin Bruger', + email: 'admin@example.com', + role: 'Administrator', + lastLogin: '2025-01-17', + }, + { + id: '2', + name: 'Bogholder', + email: 'bogholder@example.com', + role: 'Bogholder', + lastLogin: '2025-01-16', + }, + ].map((user) => ( + + +
+ + {user.name} + {user.email} + + + + + + {user.role} + + + Sidste login: {user.lastLogin} + + + + + + + ))} + + + ), + }, + ]; + + return ( +
+ {/* Header */} +
+ + Indstillinger + + {company?.name} +
+ + +
+ ); +} diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx new file mode 100644 index 0000000..c209bb2 --- /dev/null +++ b/frontend/src/routes.tsx @@ -0,0 +1,59 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import { Suspense, lazy } from 'react'; +import { Spin } from 'antd'; + +// Lazy load pages for code splitting +const Dashboard = lazy(() => import('./pages/Dashboard')); +const Kassekladde = lazy(() => import('./pages/Kassekladde')); +const HurtigBogforing = lazy(() => import('./pages/HurtigBogforing')); +const Kontooversigt = lazy(() => import('./pages/Kontooversigt')); +const Bankafstemning = lazy(() => import('./pages/Bankafstemning')); +const Momsindberetning = lazy(() => import('./pages/Momsindberetning')); +const Loenforstaelse = lazy(() => import('./pages/Loenforstaelse')); +const Settings = lazy(() => import('./pages/Settings')); + +// Loading fallback component +function PageLoader() { + return ( +
+ +
+ ); +} + +export default function AppRoutes() { + return ( + }> + + {/* Dashboard */} + } /> + + {/* Accounting */} + } /> + } /> + } /> + + {/* Bank */} + } /> + + {/* Reporting */} + } /> + } /> + + {/* Settings */} + } /> + + {/* Fallback redirect */} + } /> + + + ); +} diff --git a/frontend/src/stores/companyStore.ts b/frontend/src/stores/companyStore.ts new file mode 100644 index 0000000..1f3c03d --- /dev/null +++ b/frontend/src/stores/companyStore.ts @@ -0,0 +1,53 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { Company } from '@/types/accounting'; + +interface CompanyState { + // Current active company + activeCompany: Company | null; + // List of available companies + companies: Company[]; + // Loading state + isLoading: boolean; + + // Actions + setActiveCompany: (company: Company) => void; + setCompanies: (companies: Company[]) => void; + setLoading: (loading: boolean) => void; + clearActiveCompany: () => void; +} + +export const useCompanyStore = create()( + persist( + (set) => ({ + activeCompany: null, + companies: [], + isLoading: false, + + setActiveCompany: (company) => + set({ activeCompany: company }), + + setCompanies: (companies) => + set({ companies }), + + setLoading: (isLoading) => + set({ isLoading }), + + clearActiveCompany: () => + set({ activeCompany: null }), + }), + { + name: 'books-company-storage', + partialize: (state) => ({ + activeCompany: state.activeCompany, + }), + } + ) +); + +// Selector hooks for convenience +export const useActiveCompany = () => + useCompanyStore((state) => state.activeCompany); + +export const useCompanies = () => + useCompanyStore((state) => state.companies); diff --git a/frontend/src/stores/periodStore.ts b/frontend/src/stores/periodStore.ts new file mode 100644 index 0000000..8f7ba52 --- /dev/null +++ b/frontend/src/stores/periodStore.ts @@ -0,0 +1,407 @@ +// Period Store - Zustand store for fiscal period management + +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { + FiscalYear, + AccountingPeriod, + VATPeriod, + PeriodContext, + PeriodSettings, +} from '@/types/periods'; + +interface PeriodState { + // Fiscal years + fiscalYears: FiscalYear[]; + currentFiscalYear: FiscalYear | null; + selectedFiscalYear: FiscalYear | null; // User-selected fiscal year (persisted) + + // Accounting periods + periods: AccountingPeriod[]; + currentPeriod: AccountingPeriod | null; + selectedPeriod: AccountingPeriod | null; + + // VAT periods + vatPeriods: VATPeriod[]; + selectedVATPeriod: VATPeriod | null; + + // Comparison + comparisonPeriod: AccountingPeriod | null; + comparisonType: 'previous-period' | 'previous-year' | 'custom' | null; + + // Settings + periodSettings: PeriodSettings | null; + + // Loading states + isLoading: boolean; + + // Actions - Fiscal Years + setFiscalYears: (years: FiscalYear[]) => void; + setCurrentFiscalYear: (year: FiscalYear | null) => void; + setSelectedFiscalYear: (year: FiscalYear | null) => void; + selectFiscalYear: (yearId: string) => void; + addFiscalYear: (year: FiscalYear) => void; + updateFiscalYear: (id: string, updates: Partial) => void; + closeFiscalYear: (id: string, closedBy: string) => void; + reopenFiscalYear: (id: string, reopenedBy: string) => void; + lockFiscalYear: (id: string, lockedBy: string) => void; + + // Actions - Accounting Periods + setPeriods: (periods: AccountingPeriod[]) => void; + setCurrentPeriod: (period: AccountingPeriod | null) => void; + setSelectedPeriod: (period: AccountingPeriod | null) => void; + updatePeriod: (id: string, updates: Partial) => void; + closePeriod: (id: string, closedBy: string) => void; + reopenPeriod: (id: string, reopenedBy: string) => void; + lockPeriod: (id: string, lockedBy: string) => void; + + // Actions - VAT Periods + setVATPeriods: (periods: VATPeriod[]) => void; + setSelectedVATPeriod: (period: VATPeriod | null) => void; + updateVATPeriod: (id: string, updates: Partial) => void; + + // Actions - Comparison + setComparisonPeriod: (period: AccountingPeriod | null, type: PeriodState['comparisonType']) => void; + clearComparison: () => void; + + // Actions - Settings + setPeriodSettings: (settings: PeriodSettings) => void; + + // Actions - Loading + setLoading: (loading: boolean) => void; + + // Computed/derived + getContext: () => PeriodContext; + getPeriodById: (id: string) => AccountingPeriod | undefined; + getVATPeriodById: (id: string) => VATPeriod | undefined; + getFiscalYearById: (id: string) => FiscalYear | undefined; + getFiscalYearForDate: (date: string) => FiscalYear | undefined; + getPeriodsForYear: (fiscalYearId: string) => AccountingPeriod[]; + getOpenPeriods: () => AccountingPeriod[]; + getOpenFiscalYears: () => FiscalYear[]; + getEffectiveFiscalYear: () => FiscalYear | null; + canPostToDate: (date: string) => { allowed: boolean; reason?: string; reasonDanish?: string }; + + // Reset + resetPeriodState: () => void; +} + +const initialState = { + fiscalYears: [], + currentFiscalYear: null, + selectedFiscalYear: null, + periods: [], + currentPeriod: null, + selectedPeriod: null, + vatPeriods: [], + selectedVATPeriod: null, + comparisonPeriod: null, + comparisonType: null as PeriodState['comparisonType'], + periodSettings: null, + isLoading: false, +}; + +export const usePeriodStore = create()( + persist( + (set, get) => ({ + ...initialState, + + // Fiscal Years Actions + setFiscalYears: (years) => set({ fiscalYears: years }), + + setCurrentFiscalYear: (year) => set({ currentFiscalYear: year }), + + addFiscalYear: (year) => + set((state) => ({ + fiscalYears: [...state.fiscalYears, year], + })), + + setSelectedFiscalYear: (year) => set({ selectedFiscalYear: year }), + + selectFiscalYear: (yearId) => { + const year = get().fiscalYears.find((y) => y.id === yearId); + if (year) { + set({ selectedFiscalYear: year, currentFiscalYear: year }); + } + }, + + updateFiscalYear: (id, updates) => + set((state) => ({ + fiscalYears: state.fiscalYears.map((y) => + y.id === id ? { ...y, ...updates } : y + ), + currentFiscalYear: + state.currentFiscalYear?.id === id + ? { ...state.currentFiscalYear, ...updates } + : state.currentFiscalYear, + selectedFiscalYear: + state.selectedFiscalYear?.id === id + ? { ...state.selectedFiscalYear, ...updates } + : state.selectedFiscalYear, + })), + + closeFiscalYear: (id, closedBy) => { + const now = new Date().toISOString(); + get().updateFiscalYear(id, { + status: 'closed', + closingDate: now, + closedBy, + }); + }, + + reopenFiscalYear: (id, _reopenedBy) => { + get().updateFiscalYear(id, { + status: 'open', + closingDate: undefined, + closedBy: undefined, + }); + }, + + lockFiscalYear: (id, lockedBy) => { + const now = new Date().toISOString(); + get().updateFiscalYear(id, { + status: 'locked', + closingDate: now, + closedBy: lockedBy, + }); + }, + + // Accounting Periods Actions + setPeriods: (periods) => set({ periods }), + + setCurrentPeriod: (period) => set({ currentPeriod: period }), + + setSelectedPeriod: (period) => set({ selectedPeriod: period }), + + updatePeriod: (id, updates) => + set((state) => ({ + periods: state.periods.map((p) => + p.id === id ? { ...p, ...updates } : p + ), + currentPeriod: + state.currentPeriod?.id === id + ? { ...state.currentPeriod, ...updates } + : state.currentPeriod, + selectedPeriod: + state.selectedPeriod?.id === id + ? { ...state.selectedPeriod, ...updates } + : state.selectedPeriod, + })), + + closePeriod: (id, closedBy) => { + const now = new Date().toISOString(); + get().updatePeriod(id, { + status: 'closed', + closedAt: now, + closedBy, + }); + }, + + reopenPeriod: (id, reopenedBy) => { + const now = new Date().toISOString(); + get().updatePeriod(id, { + status: 'open', + reopenedAt: now, + reopenedBy, + closedAt: undefined, + closedBy: undefined, + }); + }, + + lockPeriod: (id, lockedBy) => { + const now = new Date().toISOString(); + get().updatePeriod(id, { + status: 'locked', + lockedAt: now, + lockedBy, + }); + }, + + // VAT Periods Actions + setVATPeriods: (periods) => set({ vatPeriods: periods }), + + setSelectedVATPeriod: (period) => set({ selectedVATPeriod: period }), + + updateVATPeriod: (id, updates) => + set((state) => ({ + vatPeriods: state.vatPeriods.map((p) => + p.id === id ? { ...p, ...updates } : p + ), + selectedVATPeriod: + state.selectedVATPeriod?.id === id + ? { ...state.selectedVATPeriod, ...updates } + : state.selectedVATPeriod, + })), + + // Comparison Actions + setComparisonPeriod: (period, type) => + set({ + comparisonPeriod: period, + comparisonType: type, + }), + + clearComparison: () => + set({ + comparisonPeriod: null, + comparisonType: null, + }), + + // Settings Actions + setPeriodSettings: (settings) => set({ periodSettings: settings }), + + // Loading Actions + setLoading: (isLoading) => set({ isLoading }), + + // Computed/Derived + getContext: () => { + const state = get(); + return { + currentFiscalYear: state.currentFiscalYear, + currentPeriod: state.currentPeriod, + selectedPeriod: state.selectedPeriod, + selectedVATPeriod: state.selectedVATPeriod, + comparisonPeriod: state.comparisonPeriod, + comparisonType: state.comparisonType, + }; + }, + + getPeriodById: (id) => get().periods.find((p) => p.id === id), + + getVATPeriodById: (id) => get().vatPeriods.find((p) => p.id === id), + + getFiscalYearById: (id) => get().fiscalYears.find((y) => y.id === id), + + getFiscalYearForDate: (date) => { + const targetDate = new Date(date); + return get().fiscalYears.find((y) => { + const start = new Date(y.startDate); + const end = new Date(y.endDate); + return targetDate >= start && targetDate <= end; + }); + }, + + getPeriodsForYear: (fiscalYearId) => + get().periods.filter((p) => p.fiscalYearId === fiscalYearId), + + getOpenPeriods: () => + get().periods.filter((p) => p.status === 'open'), + + getOpenFiscalYears: () => + get().fiscalYears.filter((y) => y.status === 'open'), + + getEffectiveFiscalYear: () => { + const state = get(); + return state.selectedFiscalYear || state.currentFiscalYear; + }, + + canPostToDate: (date) => { + const state = get(); + const dateParts = date.split('-'); + const targetDate = new Date( + parseInt(dateParts[0]), + parseInt(dateParts[1]) - 1, + parseInt(dateParts[2]) + ); + + // Find the period for this date + const period = state.periods.find((p) => { + const start = new Date(p.startDate); + const end = new Date(p.endDate); + return targetDate >= start && targetDate <= end; + }); + + if (!period) { + return { + allowed: false, + reason: 'No period found for this date', + reasonDanish: 'Ingen periode fundet for denne dato', + }; + } + + if (period.status === 'locked') { + return { + allowed: false, + reason: 'Period is locked', + reasonDanish: 'Perioden er laast', + }; + } + + if ( + period.status === 'closed' && + state.periodSettings?.preventPostingToClosedPeriods + ) { + return { + allowed: false, + reason: 'Period is closed', + reasonDanish: 'Perioden er lukket', + }; + } + + if ( + period.status === 'future' && + state.periodSettings?.preventPostingToFuturePeriods + ) { + return { + allowed: false, + reason: 'Cannot post to future periods', + reasonDanish: 'Kan ikke bogfoere i fremtidige perioder', + }; + } + + return { allowed: true }; + }, + + // Reset + resetPeriodState: () => set(initialState), + }), + { + name: 'books-period-storage', + partialize: (state) => ({ + selectedFiscalYear: state.selectedFiscalYear, + selectedPeriod: state.selectedPeriod, + selectedVATPeriod: state.selectedVATPeriod, + comparisonType: state.comparisonType, + }), + } + ) +); + +// ===================================================== +// SELECTOR HOOKS +// ===================================================== + +export const useCurrentFiscalYear = () => + usePeriodStore((state) => state.currentFiscalYear); + +export const useSelectedFiscalYear = () => + usePeriodStore((state) => state.selectedFiscalYear); + +export const useEffectiveFiscalYear = () => + usePeriodStore((state) => state.getEffectiveFiscalYear()); + +export const useFiscalYears = () => + usePeriodStore((state) => state.fiscalYears); + +export const useOpenFiscalYears = () => + usePeriodStore((state) => state.getOpenFiscalYears()); + +export const useCurrentPeriod = () => + usePeriodStore((state) => state.currentPeriod); + +export const useSelectedPeriod = () => + usePeriodStore((state) => state.selectedPeriod); + +export const useSelectedVATPeriod = () => + usePeriodStore((state) => state.selectedVATPeriod); + +export const usePeriodContext = () => + usePeriodStore((state) => state.getContext()); + +export const useOpenPeriods = () => + usePeriodStore((state) => state.getOpenPeriods()); + +export const usePeriodSettings = () => + usePeriodStore((state) => state.periodSettings); + +export const useCanPostToDate = (date: string) => + usePeriodStore((state) => state.canPostToDate(date)); diff --git a/frontend/src/stores/reconciliationStore.ts b/frontend/src/stores/reconciliationStore.ts new file mode 100644 index 0000000..f1d6aec --- /dev/null +++ b/frontend/src/stores/reconciliationStore.ts @@ -0,0 +1,172 @@ +import { create } from 'zustand'; +import type { BankTransaction, Transaction } from '@/types/accounting'; +import type { MatchSuggestion } from '@/types/ui'; + +interface ReconciliationState { + // Selection state + selectedBankTransactions: string[]; + selectedLedgerTransactions: string[]; + + // Match suggestions from backend + matchSuggestions: MatchSuggestion[]; + + // Current bank account being reconciled + activeBankAccountId: string | null; + + // Period being reconciled + reconciliationPeriod: { start: string; end: string } | null; + + // View state + viewMode: 'list' | 'side-by-side'; + + // Pending matches (before save) + pendingMatches: PendingMatch[]; + + // Actions + selectBankTransaction: (id: string) => void; + deselectBankTransaction: (id: string) => void; + toggleBankTransaction: (id: string) => void; + clearBankSelection: () => void; + + selectLedgerTransaction: (id: string) => void; + deselectLedgerTransaction: (id: string) => void; + toggleLedgerTransaction: (id: string) => void; + clearLedgerSelection: () => void; + + clearAllSelections: () => void; + + setMatchSuggestions: (suggestions: MatchSuggestion[]) => void; + setActiveBankAccount: (id: string | null) => void; + setReconciliationPeriod: (period: { start: string; end: string } | null) => void; + setViewMode: (mode: 'list' | 'side-by-side') => void; + + addPendingMatch: (match: PendingMatch) => void; + removePendingMatch: (bankTransactionId: string) => void; + clearPendingMatches: () => void; + + // Reset entire state + resetReconciliation: () => void; +} + +interface PendingMatch { + bankTransactionId: string; + bankTransaction?: BankTransaction; + ledgerTransactionId?: string; + ledgerTransaction?: Transaction; + matchType: 'existing' | 'new'; + newTransaction?: { + description: string; + accountId: string; + }; +} + +const initialState = { + selectedBankTransactions: [], + selectedLedgerTransactions: [], + matchSuggestions: [], + activeBankAccountId: null, + reconciliationPeriod: null, + viewMode: 'side-by-side' as const, + pendingMatches: [], +}; + +export const useReconciliationStore = create()((set) => ({ + ...initialState, + + // Bank transaction selection + selectBankTransaction: (id) => + set((state) => ({ + selectedBankTransactions: [...state.selectedBankTransactions, id], + })), + + deselectBankTransaction: (id) => + set((state) => ({ + selectedBankTransactions: state.selectedBankTransactions.filter((i) => i !== id), + })), + + toggleBankTransaction: (id) => + set((state) => ({ + selectedBankTransactions: state.selectedBankTransactions.includes(id) + ? state.selectedBankTransactions.filter((i) => i !== id) + : [...state.selectedBankTransactions, id], + })), + + clearBankSelection: () => + set({ selectedBankTransactions: [] }), + + // Ledger transaction selection + selectLedgerTransaction: (id) => + set((state) => ({ + selectedLedgerTransactions: [...state.selectedLedgerTransactions, id], + })), + + deselectLedgerTransaction: (id) => + set((state) => ({ + selectedLedgerTransactions: state.selectedLedgerTransactions.filter((i) => i !== id), + })), + + toggleLedgerTransaction: (id) => + set((state) => ({ + selectedLedgerTransactions: state.selectedLedgerTransactions.includes(id) + ? state.selectedLedgerTransactions.filter((i) => i !== id) + : [...state.selectedLedgerTransactions, id], + })), + + clearLedgerSelection: () => + set({ selectedLedgerTransactions: [] }), + + clearAllSelections: () => + set({ selectedBankTransactions: [], selectedLedgerTransactions: [] }), + + // Other state management + setMatchSuggestions: (matchSuggestions) => + set({ matchSuggestions }), + + setActiveBankAccount: (activeBankAccountId) => + set({ activeBankAccountId }), + + setReconciliationPeriod: (reconciliationPeriod) => + set({ reconciliationPeriod }), + + setViewMode: (viewMode) => + set({ viewMode }), + + addPendingMatch: (match) => + set((state) => ({ + pendingMatches: [...state.pendingMatches, match], + selectedBankTransactions: state.selectedBankTransactions.filter( + (id) => id !== match.bankTransactionId + ), + selectedLedgerTransactions: match.ledgerTransactionId + ? state.selectedLedgerTransactions.filter( + (id) => id !== match.ledgerTransactionId + ) + : state.selectedLedgerTransactions, + })), + + removePendingMatch: (bankTransactionId) => + set((state) => ({ + pendingMatches: state.pendingMatches.filter( + (m) => m.bankTransactionId !== bankTransactionId + ), + })), + + clearPendingMatches: () => + set({ pendingMatches: [] }), + + resetReconciliation: () => + set(initialState), +})); + +// Selector hooks +export const useSelectedBankTransactions = () => + useReconciliationStore((state) => state.selectedBankTransactions); + +export const useSelectedLedgerTransactions = () => + useReconciliationStore((state) => state.selectedLedgerTransactions); + +export const useMatchSuggestions = () => + useReconciliationStore((state) => state.matchSuggestions); + +export const usePendingMatches = () => + useReconciliationStore((state) => state.pendingMatches); diff --git a/frontend/src/stores/simpleBookingStore.ts b/frontend/src/stores/simpleBookingStore.ts new file mode 100644 index 0000000..b0a5bc7 --- /dev/null +++ b/frontend/src/stores/simpleBookingStore.ts @@ -0,0 +1,364 @@ +// Simple Booking Store - Zustand store for quick/simple booking workflow + +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { VATCode } from '@/types/vat'; +import type { + BankTransactionInput, + GeneratedTransaction, + SplitBookingLine, +} from '@/lib/accounting'; + +// ===================================================== +// TYPES +// ===================================================== + +/** + * Bank transaction with booking status + */ +export interface PendingBankTransaction extends BankTransactionInput { + isBooked: boolean; + bookedAt?: string; + transactionId?: string; // Reference to created accounting transaction +} + +/** + * Favorite/quick account for fast selection + */ +export interface FavoriteAccount { + id: string; + accountId: string; + accountNumber: string; + accountName: string; + defaultVATCode: VATCode; + usageCount: number; + lastUsed?: string; +} + +/** + * Booking modal state + */ +export interface BookingModalState { + isOpen: boolean; + type: 'simple' | 'split' | null; + bankTransaction: PendingBankTransaction | null; +} + +/** + * Split booking working state + */ +export interface SplitBookingState { + lines: SplitBookingLine[]; + remainingAmount: number; +} + +// ===================================================== +// STORE STATE +// ===================================================== + +interface SimpleBookingState { + // Bank transactions + pendingTransactions: PendingBankTransaction[]; + selectedBankAccountId: string | null; + + // Favorites + favoriteAccounts: FavoriteAccount[]; + + // Modal state + modal: BookingModalState; + + // Split booking state + splitState: SplitBookingState; + + // Preview + preview: GeneratedTransaction | null; + + // Loading states + isLoading: boolean; + isSaving: boolean; + + // Actions - Bank Transactions + setPendingTransactions: (transactions: PendingBankTransaction[]) => void; + markAsBooked: (transactionId: string, accountingTransactionId: string) => void; + setSelectedBankAccount: (accountId: string | null) => void; + + // Actions - Favorites + addFavoriteAccount: (account: Omit) => void; + removeFavoriteAccount: (accountId: string) => void; + incrementFavoriteUsage: (accountId: string) => void; + updateFavoriteVATCode: (accountId: string, vatCode: VATCode) => void; + + // Actions - Modal + openSimpleBooking: (transaction: PendingBankTransaction) => void; + openSplitBooking: (transaction: PendingBankTransaction) => void; + closeModal: () => void; + + // Actions - Split Booking + addSplitLine: (line: SplitBookingLine) => void; + updateSplitLine: (index: number, line: Partial) => void; + removeSplitLine: (index: number) => void; + clearSplitLines: () => void; + + // Actions - Preview + setPreview: (preview: GeneratedTransaction | null) => void; + + // Actions - Loading + setLoading: (loading: boolean) => void; + setSaving: (saving: boolean) => void; + + // Computed + getUnbookedTransactions: () => PendingBankTransaction[]; + getTopFavorites: (limit?: number) => FavoriteAccount[]; + getSplitRemainingAmount: () => number; + + // Reset + resetStore: () => void; +} + +// ===================================================== +// INITIAL STATE +// ===================================================== + +const initialModalState: BookingModalState = { + isOpen: false, + type: null, + bankTransaction: null, +}; + +const initialSplitState: SplitBookingState = { + lines: [], + remainingAmount: 0, +}; + +const initialState = { + pendingTransactions: [], + selectedBankAccountId: null, + favoriteAccounts: [], + modal: initialModalState, + splitState: initialSplitState, + preview: null, + isLoading: false, + isSaving: false, +}; + +// ===================================================== +// STORE IMPLEMENTATION +// ===================================================== + +export const useSimpleBookingStore = create()( + persist( + (set, get) => ({ + ...initialState, + + // Bank Transactions + setPendingTransactions: (transactions) => + set({ pendingTransactions: transactions }), + + markAsBooked: (transactionId, accountingTransactionId) => + set((state) => ({ + pendingTransactions: state.pendingTransactions.map((tx) => + tx.id === transactionId + ? { + ...tx, + isBooked: true, + bookedAt: new Date().toISOString(), + transactionId: accountingTransactionId, + } + : tx + ), + })), + + setSelectedBankAccount: (accountId) => + set({ selectedBankAccountId: accountId }), + + // Favorites + addFavoriteAccount: (account) => + set((state) => ({ + favoriteAccounts: [ + ...state.favoriteAccounts, + { + ...account, + id: `fav-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + usageCount: 0, + }, + ], + })), + + removeFavoriteAccount: (accountId) => + set((state) => ({ + favoriteAccounts: state.favoriteAccounts.filter((f) => f.accountId !== accountId), + })), + + incrementFavoriteUsage: (accountId) => + set((state) => ({ + favoriteAccounts: state.favoriteAccounts.map((f) => + f.accountId === accountId + ? { + ...f, + usageCount: f.usageCount + 1, + lastUsed: new Date().toISOString(), + } + : f + ), + })), + + updateFavoriteVATCode: (accountId, vatCode) => + set((state) => ({ + favoriteAccounts: state.favoriteAccounts.map((f) => + f.accountId === accountId ? { ...f, defaultVATCode: vatCode } : f + ), + })), + + // Modal + openSimpleBooking: (transaction) => + set({ + modal: { + isOpen: true, + type: 'simple', + bankTransaction: transaction, + }, + preview: null, + }), + + openSplitBooking: (transaction) => + set({ + modal: { + isOpen: true, + type: 'split', + bankTransaction: transaction, + }, + splitState: { + lines: [], + remainingAmount: Math.abs(transaction.amount), + }, + preview: null, + }), + + closeModal: () => + set({ + modal: initialModalState, + splitState: initialSplitState, + preview: null, + }), + + // Split Booking + addSplitLine: (line) => + set((state) => { + const newLines = [...state.splitState.lines, line]; + const totalAllocated = newLines.reduce((sum, l) => sum + Math.abs(l.amount), 0); + const totalAmount = state.modal.bankTransaction + ? Math.abs(state.modal.bankTransaction.amount) + : 0; + + return { + splitState: { + lines: newLines, + remainingAmount: Math.max(0, totalAmount - totalAllocated), + }, + }; + }), + + updateSplitLine: (index, updates) => + set((state) => { + const newLines = [...state.splitState.lines]; + if (index >= 0 && index < newLines.length) { + newLines[index] = { ...newLines[index], ...updates }; + } + + const totalAllocated = newLines.reduce((sum, l) => sum + Math.abs(l.amount), 0); + const totalAmount = state.modal.bankTransaction + ? Math.abs(state.modal.bankTransaction.amount) + : 0; + + return { + splitState: { + lines: newLines, + remainingAmount: Math.max(0, totalAmount - totalAllocated), + }, + }; + }), + + removeSplitLine: (index) => + set((state) => { + const newLines = state.splitState.lines.filter((_, i) => i !== index); + const totalAllocated = newLines.reduce((sum, l) => sum + Math.abs(l.amount), 0); + const totalAmount = state.modal.bankTransaction + ? Math.abs(state.modal.bankTransaction.amount) + : 0; + + return { + splitState: { + lines: newLines, + remainingAmount: Math.max(0, totalAmount - totalAllocated), + }, + }; + }), + + clearSplitLines: () => + set((state) => ({ + splitState: { + lines: [], + remainingAmount: state.modal.bankTransaction + ? Math.abs(state.modal.bankTransaction.amount) + : 0, + }, + })), + + // Preview + setPreview: (preview) => set({ preview }), + + // Loading + setLoading: (isLoading) => set({ isLoading }), + setSaving: (isSaving) => set({ isSaving }), + + // Computed + getUnbookedTransactions: () => + get().pendingTransactions.filter((tx) => !tx.isBooked), + + getTopFavorites: (limit = 5) => + [...get().favoriteAccounts] + .sort((a, b) => b.usageCount - a.usageCount) + .slice(0, limit), + + getSplitRemainingAmount: () => get().splitState.remainingAmount, + + // Reset + resetStore: () => set(initialState), + }), + { + name: 'simple-booking-favorites', + // Only persist favoriteAccounts - transient state like modal/preview should not be persisted + partialize: (state) => ({ + favoriteAccounts: state.favoriteAccounts, + }), + } + ) +); + +// ===================================================== +// SELECTOR HOOKS +// ===================================================== + +export const usePendingTransactions = () => + useSimpleBookingStore((state) => state.pendingTransactions); + +export const useUnbookedTransactions = () => + useSimpleBookingStore((state) => state.getUnbookedTransactions()); + +export const useFavoriteAccounts = () => + useSimpleBookingStore((state) => state.favoriteAccounts); + +export const useTopFavorites = (limit?: number) => + useSimpleBookingStore((state) => state.getTopFavorites(limit)); + +export const useBookingModal = () => + useSimpleBookingStore((state) => state.modal); + +export const useSplitState = () => + useSimpleBookingStore((state) => state.splitState); + +export const useBookingPreview = () => + useSimpleBookingStore((state) => state.preview); + +export const useIsBookingSaving = () => + useSimpleBookingStore((state) => state.isSaving); diff --git a/frontend/src/stores/uiStore.ts b/frontend/src/stores/uiStore.ts new file mode 100644 index 0000000..6317cf8 --- /dev/null +++ b/frontend/src/stores/uiStore.ts @@ -0,0 +1,82 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface UIState { + // Sidebar + sidebarCollapsed: boolean; + + // Theme + darkMode: boolean; + + // Notifications + notifications: Notification[]; + + // Actions + toggleSidebar: () => void; + setSidebarCollapsed: (collapsed: boolean) => void; + toggleDarkMode: () => void; + addNotification: (notification: Omit) => void; + removeNotification: (id: string) => void; + clearNotifications: () => void; +} + +interface Notification { + id: string; + type: 'success' | 'error' | 'info' | 'warning'; + message: string; + description?: string; + timestamp: number; +} + +export const useUIStore = create()( + persist( + (set) => ({ + sidebarCollapsed: false, + darkMode: false, + notifications: [], + + toggleSidebar: () => + set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })), + + setSidebarCollapsed: (collapsed) => + set({ sidebarCollapsed: collapsed }), + + toggleDarkMode: () => + set((state) => ({ darkMode: !state.darkMode })), + + addNotification: (notification) => + set((state) => ({ + notifications: [ + ...state.notifications, + { + ...notification, + id: `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: Date.now(), + }, + ], + })), + + removeNotification: (id) => + set((state) => ({ + notifications: state.notifications.filter((n) => n.id !== id), + })), + + clearNotifications: () => + set({ notifications: [] }), + }), + { + name: 'books-ui-storage', + partialize: (state) => ({ + sidebarCollapsed: state.sidebarCollapsed, + darkMode: state.darkMode, + }), + } + ) +); + +// Selector hooks +export const useSidebarCollapsed = () => + useUIStore((state) => state.sidebarCollapsed); + +export const useDarkMode = () => + useUIStore((state) => state.darkMode); diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css new file mode 100644 index 0000000..3d93630 --- /dev/null +++ b/frontend/src/styles/global.css @@ -0,0 +1,111 @@ +/* Global styles for bookkeeping system */ + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Tabular figures for number alignment */ +.tabular-nums { + font-variant-numeric: tabular-nums; +} + +/* Amount colors */ +.amount-positive { + color: #52c41a; +} + +.amount-negative { + color: #ff4d4f; +} + +.amount-zero { + color: #8c8c8c; +} + +/* Right-aligned numbers in tables */ +.text-right { + text-align: right; +} + +/* Currency formatting */ +.currency { + font-variant-numeric: tabular-nums; + text-align: right; +} + +/* Sticky table headers */ +.ant-table-sticky-holder { + z-index: 3; +} + +/* High-density table rows */ +.ant-table-tbody > tr > td { + padding: 8px 12px !important; +} + +/* Sidebar navigation improvements */ +.ant-layout-sider { + overflow: auto; + height: 100vh; + position: fixed !important; + left: 0; + top: 0; + bottom: 0; +} + +/* Main content area */ +.ant-layout-content { + min-height: calc(100vh - 64px); + padding: 16px; + overflow: auto; +} + +/* Card shadows for depth */ +.ant-card { + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), + 0 2px 4px rgba(0, 0, 0, 0.02); +} + +/* Form labels */ +.ant-form-item-label > label { + font-weight: 500; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a1a1a1; +} + +/* Print styles */ +@media print { + .ant-layout-sider, + .ant-layout-header { + display: none !important; + } + + .ant-layout-content { + margin: 0 !important; + padding: 0 !important; + } +} diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts new file mode 100644 index 0000000..f10002f --- /dev/null +++ b/frontend/src/styles/theme.ts @@ -0,0 +1,78 @@ +import type { ThemeConfig } from 'antd'; + +export const theme: ThemeConfig = { + token: { + // Primary colors + colorPrimary: '#1677ff', + colorSuccess: '#52c41a', + colorError: '#ff4d4f', + colorWarning: '#faad14', + colorInfo: '#1677ff', + + // Typography - compact for high density + fontSize: 13, + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif", + + // Spacing - tighter for density + borderRadius: 4, + padding: 12, + paddingSM: 8, + paddingXS: 4, + + // Layout + controlHeight: 32, + controlHeightSM: 24, + controlHeightLG: 40, + }, + components: { + Table: { + cellPaddingBlock: 8, + cellPaddingInline: 12, + headerBg: '#fafafa', + rowHoverBg: '#f5f5f5', + borderColor: '#f0f0f0', + }, + Layout: { + siderBg: '#001529', + headerBg: '#fff', + bodyBg: '#f5f5f5', + }, + Menu: { + darkItemBg: '#001529', + darkSubMenuItemBg: '#000c17', + }, + Form: { + labelFontSize: 13, + verticalLabelPadding: '0 0 4px', + }, + Card: { + paddingLG: 16, + }, + Statistic: { + titleFontSize: 12, + contentFontSize: 20, + }, + }, +}; + +// Danish locale configuration +export const daLocale = { + locale: 'da', + // Number formatting for Danish + numberFormat: { + decimalSeparator: ',', + thousandSeparator: '.', + currencySymbol: 'kr.', + currencyPosition: 'after' as const, + }, +}; + +// Color utilities for accounting +export const accountingColors = { + debit: '#ff4d4f', // Red + credit: '#52c41a', // Green + neutral: '#8c8c8c', // Gray + balance: '#1677ff', // Blue + warning: '#faad14', // Yellow/Orange +}; diff --git a/frontend/src/types/accounting.ts b/frontend/src/types/accounting.ts new file mode 100644 index 0000000..3fd4ed9 --- /dev/null +++ b/frontend/src/types/accounting.ts @@ -0,0 +1,193 @@ +// Core accounting types for the bookkeeping system + +export interface Company { + id: string; + name: string; + cvr: string; // Danish CVR number + address?: string; + city?: string; + postalCode?: string; + country: string; + vatNumber?: string; + fiscalYearStart: number; // Month (1-12) + currency: string; + createdAt: string; + updatedAt: string; +} + +export interface Account { + id: string; + companyId: string; + accountNumber: string; // e.g., "1000", "2100" + name: string; + type: AccountType; + parentId?: string; + isActive: boolean; + description?: string; + vatCode?: string; + balance: number; + createdAt: string; + updatedAt: string; +} + +export type AccountType = + | 'asset' // Aktiver (1000-1999) + | 'liability' // Passiver (2000-2999) + | 'equity' // Egenkapital (3000-3999) + | 'revenue' // Indtægter (4000-4999) + | 'cogs' // Vareforbrug (5000-5999) + | 'expense' // Driftsomkostninger (6000-6999) + | 'personnel' // Personaleomkostninger (7000-7999) + | 'financial' // Finansielle poster (8000-8999) + | 'extraordinary'; // Ekstraordinære poster (9000-9999) + +export interface Transaction { + id: string; + companyId: string; + transactionNumber: string; + date: string; + description: string; + lines: TransactionLine[]; + isReconciled: boolean; + isVoided: boolean; + attachments: Attachment[]; + createdAt: string; + updatedAt: string; + createdBy: string; +} + +export interface TransactionLine { + id: string; + transactionId: string; + accountId: string; + account?: Account; + description?: string; + debit: number; + credit: number; + vatCode?: string; + vatAmount?: number; +} + +export interface Attachment { + id: string; + filename: string; + mimeType: string; + url: string; + size: number; + uploadedAt: string; +} + +export interface BankAccount { + id: string; + companyId?: string; + name: string; + bankName: string; + accountNumber: string; + iban?: string; + currency: string; + ledgerAccountId?: string; // Linked to an Account + isActive: boolean; + balance?: number; + lastSyncedAt?: string; +} + +export interface BankTransaction { + id: string; + bankAccountId: string; + date: string; + valueDate: string; + description: string; + amount: number; + balance: number; + reference?: string; + counterparty?: string; + isReconciled: boolean; + matchedTransactionId?: string; + importedAt: string; +} + +export interface ReconciliationMatch { + bankTransactionId: string; + ledgerTransactionId: string; + matchType: 'auto' | 'manual'; + confidence?: number; // For auto-matches (0-1) + matchedAt: string; + matchedBy: string; +} + +export interface VATReport { + companyId: string; + period: DateRange; + boxes: VATBox[]; + totalVATDue: number; + totalVATDeductible: number; + netVAT: number; + status: 'draft' | 'submitted' | 'accepted' | 'rejected'; + submittedAt?: string; +} + +export interface VATBox { + boxNumber: number; + name: string; + nameDanish: string; + amount: number; + basis?: number; +} + +export interface DateRange { + start: string; + end: string; +} + +export interface Employee { + id: string; + companyId: string; + name: string; + cpr?: string; // Danish CPR (masked) + employeeNumber: string; + department?: string; + position?: string; + startDate: string; + endDate?: string; + isActive: boolean; +} + +export interface PayrollEntry { + id: string; + employeeId: string; + employee?: Employee; + period: string; // YYYY-MM + grossSalary: number; + amBidrag: number; // AM-bidrag (8%) + aSkat: number; // A-skat + atp: number; // ATP contribution + pension: number; + netSalary: number; + otherDeductions: number; + status: 'draft' | 'approved' | 'paid'; +} + +// Utility types +export interface PaginatedResponse { + items: T[]; + total: number; + pageInfo: PageInfo; +} + +export interface PageInfo { + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor?: string; + endCursor?: string; +} + +export interface SortConfig { + field: string; + direction: 'asc' | 'desc'; +} + +export interface FilterConfig { + field: string; + operator: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'in'; + value: unknown; +} diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts new file mode 100644 index 0000000..530d467 --- /dev/null +++ b/frontend/src/types/api.ts @@ -0,0 +1,150 @@ +// GraphQL API types + +import type { + Company, + Account, + Transaction, + BankAccount, + BankTransaction, + VATReport, + Employee, + PayrollEntry, + PaginatedResponse, + DateRange, +} from './accounting'; + +// Query response types +export interface CompaniesQueryResponse { + companies: Company[]; +} + +export interface TransactionsQueryResponse { + transactions: PaginatedResponse; +} + +export interface AccountsQueryResponse { + accounts: Account[]; +} + +export interface BankAccountsQueryResponse { + bankAccounts: BankAccount[]; +} + +export interface BankTransactionsQueryResponse { + bankTransactions: BankTransaction[]; +} + +export interface VATReportQueryResponse { + vatReport: VATReport; +} + +export interface EmployeesQueryResponse { + employees: Employee[]; +} + +export interface PayrollQueryResponse { + payrollEntries: PayrollEntry[]; +} + +// Query variables +export interface TransactionsQueryVariables { + companyId: string; + first?: number; + after?: string; + filter?: TransactionFilter; + sort?: TransactionSort; +} + +export interface TransactionFilter { + dateFrom?: string; + dateTo?: string; + accountId?: string; + minAmount?: number; + maxAmount?: number; + description?: string; + isReconciled?: boolean; +} + +export interface TransactionSort { + field: 'date' | 'transactionNumber' | 'description' | 'amount'; + direction: 'ASC' | 'DESC'; +} + +export interface BankTransactionsQueryVariables { + companyId: string; + bankAccountId: string; + period: DateRange; + isReconciled?: boolean; +} + +export interface VATReportQueryVariables { + companyId: string; + period: DateRange; +} + +// Mutation inputs +export interface CreateTransactionInput { + companyId: string; + date: string; + description: string; + lines: CreateTransactionLineInput[]; + attachmentIds?: string[]; +} + +export interface CreateTransactionLineInput { + accountId: string; + description?: string; + debit: number; + credit: number; + vatCode?: string; +} + +export interface MatchBankTransactionInput { + bankTransactionId: string; + ledgerTransactionId?: string; + createTransaction?: CreateTransactionInput; +} + +export interface VoidTransactionInput { + transactionId: string; + reason: string; +} + +// Mutation response types +export interface CreateTransactionResponse { + createTransaction: Transaction; +} + +export interface MatchBankTransactionResponse { + matchBankTransaction: { + success: boolean; + bankTransaction: BankTransaction; + matchedTransaction?: Transaction; + }; +} + +// Dashboard/Analytics types +export interface DashboardMetrics { + cashPosition: number; + accountsReceivable: number; + accountsPayable: number; + monthlyRevenue: number; + monthlyExpenses: number; + vatLiability: number; + unreconciledTransactions: number; +} + +export interface CashFlowData { + date: string; + inflow: number; + outflow: number; + balance: number; +} + +export interface AccountBalanceData { + accountId: string; + accountName: string; + accountNumber: string; + balance: number; + type: string; +} diff --git a/frontend/src/types/periods.ts b/frontend/src/types/periods.ts new file mode 100644 index 0000000..44d89cc --- /dev/null +++ b/frontend/src/types/periods.ts @@ -0,0 +1,264 @@ +// Fiscal Period Types for Danish Accounting (Regnskabsperioder) + +/** + * Period frequency - how often accounting periods are defined + */ +export type PeriodFrequency = + | 'monthly' // Maanedlig + | 'quarterly' // Kvartalsvis + | 'half-yearly' // Halvaarlig + | 'yearly'; // Aarlig + +/** + * Period status according to Danish accounting requirements + */ +export type PeriodStatus = + | 'future' // Fremtidig - not yet started + | 'open' // Aaben - current working period + | 'closed' // Lukket - closed but can be reopened + | 'locked'; // Laast - permanently locked (after arsafslutning) + +/** + * VAT Period frequency (can differ from accounting periods) + * Based on SKAT requirements + */ +export type VATPeriodicitet = + | 'monthly' // Maanedlig (omsaetning > 50M DKK) + | 'quarterly' // Kvartalsvis (default for most) + | 'half-yearly' // Halvaarlig (omsaetning < 1M DKK, optional) + | 'yearly'; // Aarlig (omsaetning < 300K DKK, optional) + +/** + * Fiscal Year (Regnskabsaar) + */ +export interface FiscalYear { + id: string; + companyId: string; + + // Year identification + name: string; // e.g., "2024/2025" or "2025" + startDate: string; // ISO date + endDate: string; // ISO date + + // Status + status: 'open' | 'closed' | 'locked'; + + // Year-end closing + closingDate?: string; // When year-end was performed + closedBy?: string; // User who closed the year + openingBalancePosted: boolean; + + // Metadata + createdAt: string; + updatedAt: string; +} + +/** + * Accounting Period (Regnskabsperiode) + */ +export interface AccountingPeriod { + id: string; + companyId: string; + fiscalYearId: string; + + // Period identification + periodNumber: number; // 1-12 for monthly, 1-4 for quarterly, etc. + name: string; // e.g., "Januar 2025" or "Q1 2025" + shortName: string; // e.g., "Jan 2025" or "Q1" + + // Date range + startDate: string; // ISO date + endDate: string; // ISO date + + // Status + status: PeriodStatus; + + // Status change tracking + closedAt?: string; + closedBy?: string; + lockedAt?: string; + lockedBy?: string; + reopenedAt?: string; + reopenedBy?: string; + + // Metadata + createdAt: string; + updatedAt: string; +} + +/** + * VAT Period (Momsperiode) for SKAT reporting + */ +export interface VATPeriod { + id: string; + companyId: string; + + // Period identification + periodicitet: VATPeriodicitet; + year: number; + periodNumber: number; // 1-12 for monthly, 1-4 for quarterly, etc. + name: string; // e.g., "Q4 2024" + + // Date range + startDate: string; + endDate: string; + + // SKAT deadlines + deadline: string; // Frist for indberetning + paymentDeadline?: string; // Frist for betaling + + // Status + status: 'future' | 'open' | 'closed' | 'draft' | 'submitted' | 'accepted' | 'rejected'; + submittedAt?: string; + submissionReference?: string; // SKAT reference number + + // Calculated amounts (from transactions) + netVAT?: number; // Moms til betaling/tilgode + + // Metadata + createdAt: string; + updatedAt: string; +} + +/** + * Period Settings per Company + */ +export interface PeriodSettings { + companyId: string; + + // Accounting periods + accountingFrequency: PeriodFrequency; + fiscalYearStartMonth: number; // 1-12 + + // VAT periods + vatFrequency: VATPeriodicitet; + + // Auto-close settings + autoClosePeriods: boolean; + autoCloseDelayDays: number; // Days after period end to auto-close + + // Validation settings + preventPostingToClosedPeriods: boolean; + preventPostingToFuturePeriods: boolean; + requirePeriodCloseApproval: boolean; +} + +/** + * Period Context - for components that need period awareness + */ +export interface PeriodContext { + currentFiscalYear: FiscalYear | null; + currentPeriod: AccountingPeriod | null; + selectedPeriod: AccountingPeriod | null; + selectedVATPeriod: VATPeriod | null; + + // Comparison period for reports + comparisonPeriod?: AccountingPeriod | null; + comparisonType: 'previous-period' | 'previous-year' | 'custom' | null; +} + +/** + * Year-end closing entry types + */ +export type ClosingEntryType = + | 'revenue-close' // Close revenue accounts to result + | 'expense-close' // Close expense accounts to result + | 'result-transfer' // Transfer result to equity + | 'opening-balance'; // Opening balances for new year + +/** + * Year-end closing entry + */ +export interface ClosingEntry { + id: string; + fiscalYearId: string; + type: ClosingEntryType; + transactionId: string; // Reference to created transaction + amount: number; + description: string; + createdAt: string; +} + +/** + * Period validation result + */ +export interface PeriodValidationResult { + isValid: boolean; + canPost: boolean; + errors: PeriodValidationError[]; + warnings: PeriodValidationWarning[]; +} + +export interface PeriodValidationError { + code: string; + message: string; + messageDanish: string; + field?: string; +} + +export interface PeriodValidationWarning { + code: string; + message: string; + messageDanish: string; +} + +/** + * Danish month names for display + */ +export const DANISH_MONTHS = [ + 'Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'December' +] as const; + +/** + * Danish short month names + */ +export const DANISH_MONTHS_SHORT = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', + 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec' +] as const; + +/** + * Period frequency display names + */ +export const PERIOD_FREQUENCY_NAMES: Record = { + 'monthly': { danish: 'Maanedlig', english: 'Monthly' }, + 'quarterly': { danish: 'Kvartalsvis', english: 'Quarterly' }, + 'half-yearly': { danish: 'Halvaarslig', english: 'Half-yearly' }, + 'yearly': { danish: 'Aarlig', english: 'Yearly' }, +}; + +/** + * Period status display names and colors + */ +export const PERIOD_STATUS_CONFIG: Record = { + 'future': { + danish: 'Fremtidig', + english: 'Future', + color: 'default', + icon: 'clock-circle' + }, + 'open': { + danish: 'Aaben', + english: 'Open', + color: 'green', + icon: 'check-circle' + }, + 'closed': { + danish: 'Lukket', + english: 'Closed', + color: 'orange', + icon: 'minus-circle' + }, + 'locked': { + danish: 'Laast', + english: 'Locked', + color: 'red', + icon: 'lock' + }, +}; diff --git a/frontend/src/types/ui.ts b/frontend/src/types/ui.ts new file mode 100644 index 0000000..2f97927 --- /dev/null +++ b/frontend/src/types/ui.ts @@ -0,0 +1,126 @@ +// UI component types + +import type { TableColumnType } from 'antd'; +import type { DocumentNode } from 'graphql'; + +// DataTable configuration +export interface DataTableColumn extends Omit, 'render' | 'dataIndex'> { + dataIndex: string | string[]; + title: string; + sortable?: boolean; + filterable?: boolean; + render?: (value: unknown, record: T, index: number) => React.ReactNode; + // Custom column types + columnType?: 'text' | 'number' | 'currency' | 'date' | 'boolean' | 'actions'; + // Currency/number formatting + decimalPlaces?: number; + showSign?: boolean; +} + +export interface DataTableProps { + // Data source configuration + queryKey: string[]; + query: DocumentNode; + variables?: Record; + dataPath: string; // Path to items array in response + totalPath: string; // Path to total count in response + + // Columns + columns: DataTableColumn[]; + + // Pagination + pageSize?: number; + pageSizeOptions?: number[]; + + // Features + rowSelection?: 'single' | 'multiple' | false; + onRowClick?: (record: T) => void; + onSelectionChange?: (selectedRowKeys: string[], selectedRows: T[]) => void; + exportable?: boolean; + exportFilename?: string; + + // Row styling + rowClassName?: (record: T, index: number) => string; + + // Actions + toolbarActions?: React.ReactNode; + + // Empty state + emptyText?: string; + + // Loading + loading?: boolean; +} + +export interface FilterFormConfig { + fields: FilterFieldConfig[]; + onFilter: (values: Record) => void; + onReset: () => void; +} + +export interface FilterFieldConfig { + name: string; + label: string; + type: 'text' | 'number' | 'date' | 'dateRange' | 'select' | 'checkbox'; + placeholder?: string; + options?: { label: string; value: string | number }[]; + defaultValue?: unknown; +} + +// Navigation types +export interface MenuItem { + key: string; + label: string; + icon?: React.ReactNode; + path?: string; + children?: MenuItem[]; +} + +// Form types +export interface FormFieldError { + field: string; + message: string; +} + +// Modal types +export interface ModalState { + visible: boolean; + mode: 'create' | 'edit' | 'view'; + data?: unknown; +} + +// Notification types +export interface NotificationConfig { + type: 'success' | 'error' | 'info' | 'warning'; + message: string; + description?: string; + duration?: number; +} + +// Reconciliation UI types +export interface ReconciliationViewState { + selectedBankTransactions: string[]; + selectedLedgerTransactions: string[]; + matchSuggestions: MatchSuggestion[]; + viewMode: 'list' | 'side-by-side'; +} + +export interface MatchSuggestion { + bankTransactionId: string; + ledgerTransactionId: string; + confidence: number; + reason: string; +} + +// Chart data types +export interface ChartDataPoint { + label: string; + value: number; + color?: string; +} + +export interface TimeSeriesDataPoint { + date: string; + value: number; + category?: string; +} diff --git a/frontend/src/types/vat.ts b/frontend/src/types/vat.ts new file mode 100644 index 0000000..3308aaf --- /dev/null +++ b/frontend/src/types/vat.ts @@ -0,0 +1,360 @@ +// VAT (Moms) Types for Danish SKAT Compliance + +import type { VATPeriodicitet } from './periods'; + +// ===================================================== +// SKAT VAT CODES (Momskoder) +// ===================================================== + +/** + * VAT codes used in Danish bookkeeping + */ +export type VATCode = + | 'S25' // Salgsmoms 25% (udgaaende moms) + | 'K25' // Koebsmoms 25% (indgaaende moms) + | 'EU_VARE' // EU-varekoeb (reverse charge) + | 'EU_YDELSE' // EU-ydelseskoeb (reverse charge) + | 'MOMSFRI' // Momsfritaget (healthcare, education, etc.) + | 'EKSPORT' // Eksport (0%) + | 'NONE'; // Ingen moms + +/** + * VAT code type classification + */ +export type VATCodeType = + | 'output' // Udgaaende moms (salg) + | 'input' // Indgaaende moms (koeb) + | 'reverse_charge' // Omvendt betalingspligt + | 'exempt' // Momsfritaget + | 'none'; // Ingen moms + +/** + * VAT code configuration with calculation rules + */ +export interface VATCodeConfig { + code: VATCode; + nameDanish: string; + nameEnglish: string; + rate: number; // VAT rate (0.25 for 25%) + type: VATCodeType; + affectsBoxes: { + vatBox?: VATBoxId; // Which VAT box this affects + basisBox?: BasisBoxId; // Which basis/turnover box this affects + }; + reverseCharge: boolean; // Is this reverse charge VAT? + deductible: boolean; // Is this VAT deductible? + description: string; // Danish description for UI +} + +// ===================================================== +// SKAT VAT BOXES (Rubrikker) +// ===================================================== + +/** + * SKAT VAT Box identifiers + * Rubrik A-D: VAT amounts (momsbeloeb) + * Rubrik 1-4: Turnover/basis amounts (omsaetning/grundlag) + */ +export type VATBoxId = 'A' | 'B' | 'C' | 'D'; +export type BasisBoxId = '1' | '2' | '3' | '4'; + +/** + * SKAT VAT Box (Rubrik) definition + */ +export interface SKATVATBox { + id: VATBoxId | BasisBoxId; + type: 'vat' | 'basis'; + nameDanish: string; + nameEnglish: string; + description: string; + skippable: boolean; // Can be omitted if zero + isDeductible: boolean; // Is this a deductible amount? +} + +/** + * Calculated VAT box with amount + */ +export interface CalculatedVATBox extends SKATVATBox { + amount: number; + basis?: number; // For VAT boxes, the underlying basis amount + transactionCount: number; // Number of transactions contributing + transactionIds: string[]; // IDs of contributing transactions +} + +// ===================================================== +// VAT REPORT +// ===================================================== + +/** + * VAT report status + */ +export type VATReportStatus = + | 'future' // Period hasn't started yet + | 'open' // Period is open for transactions + | 'closed' // Period closed, not yet reported + | 'draft' // Report drafted but not submitted + | 'submitted' // Submitted to SKAT + | 'accepted' // Accepted by SKAT + | 'rejected' // Rejected by SKAT + | 'corrected'; // Correction submitted + +/** + * VAT Report Period (matches VATPeriod in periods.ts but specific to reports) + */ +export interface VATReportPeriod { + id: string; + companyId: string; + periodicitet: VATPeriodicitet; + year: number; + periodNumber: number; + startDate: string; + endDate: string; + deadline: string; + status: VATReportStatus; +} + +/** + * Complete VAT report for a period + */ +export interface VATReport { + id: string; + companyId: string; + period: VATReportPeriod; + + // SKAT boxes with calculated amounts + boxes: { + // VAT amounts (Momsbeloeb) + A: CalculatedVATBox; // Salgsmoms (udgaaende moms) + B: CalculatedVATBox; // Koebsmoms (indgaaende moms - fradrag) + C: CalculatedVATBox; // EU-varekoeb moms + D: CalculatedVATBox; // Ydelseskoeb fra udland moms + + // Basis/turnover amounts (Omsaetning) + '1': CalculatedVATBox; // Salg med moms (momsgrundlag) + '2': CalculatedVATBox; // Salg uden moms (momsfrit/eksport) + '3': CalculatedVATBox; // EU-varekoeb + '4': CalculatedVATBox; // Ydelseskoeb fra udland + }; + + // Summary calculations + totalOutputVAT: number; // A + C + D + totalInputVAT: number; // B (fradragsberettiget) + netVAT: number; // totalOutputVAT - totalInputVAT + + // Energy duty refunds (Afgiftsgodtgoerelse) - optional + energyDuties?: { + oilGas: number; // Olie- og flaskegasafgift + electricity: number; // Elafgift + naturalGas: number; // Naturgas- og bygasafgift + coal: number; // Kulafgift + co2: number; // CO2-afgift + }; + + // Metadata + status: VATReportStatus; + createdAt: string; + updatedAt: string; + submittedAt?: string; + submittedBy?: string; + skatReferenceNumber?: string; // Reference from SKAT + + // Audit trail + adjustments: VATAdjustment[]; + history: VATReportHistory[]; +} + +/** + * VAT adjustment entry (for corrections) + */ +export interface VATAdjustment { + id: string; + reportId: string; + boxId: VATBoxId | BasisBoxId; + previousAmount: number; + newAmount: number; + reason: string; + adjustedAt: string; + adjustedBy: string; +} + +/** + * VAT report history entry + */ +export interface VATReportHistory { + id: string; + action: 'created' | 'calculated' | 'adjusted' | 'submitted' | 'accepted' | 'rejected' | 'corrected'; + timestamp: string; + userId: string; + details?: string; +} + +// ===================================================== +// VAT CALCULATION +// ===================================================== + +/** + * Transaction line with VAT information for calculation + */ +export interface VATTransactionLine { + transactionId: string; + transactionLineId: string; + transactionNumber: string; + transactionDate: string; + accountId: string; + accountNumber: string; + accountName: string; + description: string; + debit: number; + credit: number; + netAmount: number; // Amount excluding VAT + vatCode: VATCode; + vatAmount: number; // Calculated VAT amount + vatRate: number; // Applied VAT rate + isReverseCharge: boolean; +} + +/** + * VAT calculation result + */ +export interface VATCalculationResult { + period: VATReportPeriod; + transactions: VATTransactionLine[]; + boxes: Record; + summary: { + totalOutputVAT: number; + totalInputVAT: number; + netVAT: number; + }; + warnings: VATCalculationWarning[]; + errors: VATCalculationError[]; +} + +export interface VATCalculationWarning { + type: 'missing_vat_code' | 'unusual_amount' | 'period_mismatch' | 'account_mismatch'; + message: string; + messageDanish: string; + transactionId?: string; + severity: 'low' | 'medium' | 'high'; +} + +export interface VATCalculationError { + type: 'invalid_vat_code' | 'calculation_error' | 'data_integrity'; + message: string; + messageDanish: string; + transactionId?: string; +} + +// ===================================================== +// SKAT EXPORT FORMAT +// ===================================================== + +/** + * SKAT CSV export format + */ +export interface SKATExportCSV { + cvr: string; + periode: string; // Format: YYYYMM or YYYYQQ + rubrikA: number; + rubrikB: number; + rubrikC: number; + rubrikD: number; + felt1: number; + felt2: number; + felt3: number; + felt4: number; +} + +/** + * SKAT XML export format (for API submission) + */ +export interface SKATExportXML { + version: string; + cvr: string; + periodeStart: string; + periodeSlut: string; + angivelse: { + salgsmoms: number; // Rubrik A + koebsmoms: number; // Rubrik B + euVarekoebMoms: number; // Rubrik C + ydelseskoebMoms: number; // Rubrik D + salgMedMoms: number; // Felt 1 + salgUdenMoms: number; // Felt 2 + euVarekoeb: number; // Felt 3 + ydelseskoeb: number; // Felt 4 + }; + afgifter?: { + olie: number; + el: number; + naturgas: number; + kul: number; + co2: number; + }; +} + +// ===================================================== +// SUBMISSION TRACKING +// ===================================================== + +/** + * VAT submission record + */ +export interface VATSubmission { + id: string; + reportId: string; + companyId: string; + period: VATReportPeriod; + + // Submission details + submittedAt: string; + submittedBy: string; + method: 'manual' | 'api'; // Manual upload or API + + // SKAT response + skatStatus: 'pending' | 'accepted' | 'rejected'; + skatReferenceNumber?: string; + skatResponseAt?: string; + skatErrorMessage?: string; + + // Amounts submitted + submittedAmounts: { + boxA: number; + boxB: number; + boxC: number; + boxD: number; + basis1: number; + basis2: number; + basis3: number; + basis4: number; + netVAT: number; + }; + + // Files + exportFile?: { + type: 'csv' | 'xml'; + filename: string; + url: string; + }; +} + +// ===================================================== +// PERIODICITET CONFIG +// ===================================================== + +/** + * VAT period configuration per type + */ +export interface VATPeriodicitetConfig { + type: VATPeriodicitet; + nameDanish: string; + nameEnglish: string; + deadlineDaysAfterPeriod: number; // Days after period end for deadline + periodsPerYear: number; + threshold?: { + min?: number; // Minimum annual revenue + max?: number; // Maximum annual revenue + }; +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..12c8d50 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,14 @@ +/// + +interface ImportMetaEnv { + readonly VITE_GRAPHQL_ENDPOINT: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + +declare module '*.css' { + const content: string; + export default content; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..5930f68 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..f315807 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo new file mode 100644 index 0000000..6b57a9d --- /dev/null +++ b/frontend/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/routes.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/components/layout/applayout.tsx","./src/components/layout/companyswitcher.tsx","./src/components/layout/fiscalyearselector.tsx","./src/components/layout/header.tsx","./src/components/layout/sidebar.tsx","./src/components/modals/closefiscalyearwizard.tsx","./src/components/modals/createfiscalyearmodal.tsx","./src/components/simple-booking/accountquickpicker.tsx","./src/components/simple-booking/banktransactioncard.tsx","./src/components/simple-booking/quickbookmodal.tsx","./src/components/simple-booking/splitbookmodal.tsx","./src/components/simple-booking/index.ts","./src/components/tables/datatable.tsx","./src/hooks/usecompany.ts","./src/hooks/usedatatable.ts","./src/hooks/useperiod.ts","./src/lib/accounting.ts","./src/lib/fiscalyear.ts","./src/lib/formatters.ts","./src/lib/periods.ts","./src/lib/vatcalculation.ts","./src/lib/vatcodes.ts","./src/pages/bankafstemning.tsx","./src/pages/dashboard.tsx","./src/pages/hurtigbogforing.tsx","./src/pages/kassekladde.tsx","./src/pages/kontooversigt.tsx","./src/pages/loenforstaelse.tsx","./src/pages/momsindberetning.tsx","./src/pages/settings.tsx","./src/stores/companystore.ts","./src/stores/periodstore.ts","./src/stores/reconciliationstore.ts","./src/stores/simplebookingstore.ts","./src/stores/uistore.ts","./src/styles/theme.ts","./src/types/accounting.ts","./src/types/api.ts","./src/types/periods.ts","./src/types/ui.ts","./src/types/vat.ts"],"errors":true,"version":"5.6.3"} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..eda4fc8 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + port: 3000, + open: true, + }, +});