diff --git a/README.md b/README.md index de4b4d828..c82a715ef 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,70 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/` for authorization + +#### List Budgets + +`GET http://localhost:3333/api/v1/budgets?month=2026-06` + +#### Create Budget + +`POST http://localhost:3333/api/v1/budgets` + +#### Body + +``` +{ + "accountId": "clx...", + "amount": 500, + "categoryId": "clx...", + "currency": "USD", + "month": "2026-06", + "name": "Groceries", + "type": "EXPENSE" +} +``` + +| Field | Type | Description | +| ------------ | ------------------- | ------------------------------------------------------------------ | +| `accountId` | `string` (optional) | Id of the account | +| `amount` | `number` | Budget amount | +| `categoryId` | `string` | Id of the expense category | +| `currency` | `string` | `CHF` \| `EUR` \| `USD` etc. | +| `month` | `string` | Month in the format `YYYY-MM` | +| `name` | `string` | Name of the budget line | +| `type` | `string` | `EXPENSE` \| `CASH_SAVINGS` \| `INVESTMENT_SAVINGS` | + +#### Additional Budget Endpoints + +| Method | Endpoint | Description | +| -------- | ------------------------------------------------------- | ------------------------ | +| `GET` | `/api/v1/budgets/:id` | Get one budget | +| `PUT` | `/api/v1/budgets/:id` | Update one budget | +| `DELETE` | `/api/v1/budgets/:id` | Delete one budget | +| `GET` | `/api/v1/budgets/categories` | List expense categories | +| `POST` | `/api/v1/budgets/categories` | Create expense category | +| `PUT` | `/api/v1/budgets/categories/:id` | Update expense category | +| `DELETE` | `/api/v1/budgets/categories/:id` | Delete expense category | + +#### Create Expense Category Body + +``` +{ + "color": "#ff0000", + "name": "Food" +} +``` + +| Field | Type | Description | +| ------- | ------------------- | ---------------------------- | +| `color` | `string` (optional) | Hex color code | +| `name` | `string` | Name of the expense category | + ### Portfolio (experimental) #### Prerequisites diff --git a/apps/api/src/app/budgets/budgets.service.spec.ts b/apps/api/src/app/budgets/budgets.service.spec.ts index c894c1faf..08f1fccbd 100644 --- a/apps/api/src/app/budgets/budgets.service.spec.ts +++ b/apps/api/src/app/budgets/budgets.service.spec.ts @@ -6,6 +6,7 @@ import { BudgetsService } from './budgets.service'; describe('BudgetsService', () => { const userId = 'user-1'; + const accountId = 'account-1'; const categoryId = 'category-1'; const budgetId = 'budget-1'; const createdAt = new Date('2026-06-23T00:00:00.000Z'); @@ -13,6 +14,9 @@ describe('BudgetsService', () => { let budgetsService: BudgetsService; let prismaService: { + account: { + findFirst: jest.Mock; + }; budget: { create: jest.Mock; delete: jest.Mock; @@ -34,6 +38,9 @@ describe('BudgetsService', () => { beforeEach(() => { prismaService = { + account: { + findFirst: jest.fn() + }, budget: { create: jest.fn(), delete: jest.fn(), @@ -61,6 +68,16 @@ describe('BudgetsService', () => { it('lists budgets for a month and includes spent and remaining amounts', async () => { prismaService.budget.findMany.mockResolvedValue([ { + account: { + balance: 0, + createdAt, + id: accountId, + isExcluded: false, + name: 'Checking', + updatedAt, + userId + }, + accountId, amount: 500, category: { color: '#0055aa', @@ -74,6 +91,29 @@ describe('BudgetsService', () => { currency: 'USD', id: budgetId, month: new Date('2026-06-01T00:00:00.000Z'), + name: 'Food shop', + type: 'EXPENSE', + updatedAt, + userId + }, + { + account: null, + accountId: null, + amount: 200, + category: { + color: '#0055aa', + createdAt, + id: categoryId, + name: 'Groceries', + updatedAt + }, + categoryId, + createdAt, + currency: 'USD', + id: 'budget-2', + month: new Date('2026-06-01T00:00:00.000Z'), + name: 'Emergency fund', + type: 'CASH_SAVINGS', updatedAt, userId } @@ -91,8 +131,8 @@ describe('BudgetsService', () => { }); expect(prismaService.budget.findMany).toHaveBeenCalledWith({ - include: { category: true }, - orderBy: { category: { name: 'asc' } }, + include: { account: true, category: true }, + orderBy: { name: 'asc' }, where: { month: new Date('2026-06-01T00:00:00.000Z'), userId @@ -113,6 +153,16 @@ describe('BudgetsService', () => { expect(response).toEqual({ budgets: [ { + account: { + balance: 0, + createdAt, + id: accountId, + isExcluded: false, + name: 'Checking', + updatedAt, + userId + }, + accountId, amount: 500, category: { color: '#0055aa', @@ -126,13 +176,37 @@ describe('BudgetsService', () => { currency: 'USD', id: budgetId, month: '2026-06', + name: 'Food shop', remaining: 375, spent: 125, + type: 'EXPENSE', + updatedAt + }, + { + amount: 200, + category: { + color: '#0055aa', + createdAt, + id: categoryId, + name: 'Groceries', + updatedAt + }, + categoryId, + createdAt, + currency: 'USD', + id: 'budget-2', + month: '2026-06', + name: 'Emergency fund', + remaining: 200, + spent: 0, + type: 'CASH_SAVINGS', updatedAt } ], - totalBudgeted: 500, - totalRemaining: 375, + totalBudgeted: 700, + totalMonthlySavings: 200, + totalPlannedSpend: 500, + totalRemaining: 575, totalSpent: 125 }); }); @@ -299,8 +373,21 @@ describe('BudgetsService', () => { id: categoryId, userId }); - prismaService.budget.findFirst.mockResolvedValue(null); + prismaService.account.findFirst.mockResolvedValue({ + id: accountId, + userId + }); prismaService.budget.create.mockResolvedValue({ + account: { + balance: 0, + createdAt, + id: accountId, + isExcluded: false, + name: 'Checking', + updatedAt, + userId + }, + accountId, amount: 250, category: { createdAt, @@ -313,54 +400,73 @@ describe('BudgetsService', () => { currency: 'USD', id: budgetId, month: new Date('2026-06-01T00:00:00.000Z'), + name: 'Train pass', + type: 'EXPENSE', updatedAt, userId }); const response = await budgetsService.createBudget({ data: { + accountId, amount: 250, categoryId, currency: 'USD', - month: '2026-06' + month: '2026-06', + name: 'Train pass', + type: 'EXPENSE' }, userId }); + expect(prismaService.account.findFirst).toHaveBeenCalledWith({ + where: { + id: accountId, + userId + } + }); expect(prismaService.budget.create).toHaveBeenCalledWith({ data: { + account: { + connect: { + id_userId: { id: accountId, userId } + } + }, amount: 250, category: { connect: { id: categoryId } }, currency: 'USD', month: new Date('2026-06-01T00:00:00.000Z'), + name: 'Train pass', + type: 'EXPENSE', user: { connect: { id: userId } } }, - include: { category: true } + include: { account: true, category: true } }); expect(response.remaining).toBe(250); expect(response.spent).toBe(0); }); - it('rejects duplicate budgets for the same category and month', async () => { + it('rejects a budget account not owned by the current user', async () => { prismaService.expenseCategory.findFirst.mockResolvedValue({ id: categoryId, userId }); - prismaService.budget.findFirst.mockResolvedValue({ - id: budgetId - }); + prismaService.account.findFirst.mockResolvedValue(null); await expect( budgetsService.createBudget({ data: { + accountId, amount: 250, categoryId, currency: 'USD', - month: '2026-06' + month: '2026-06', + name: 'Train pass', + type: 'EXPENSE' }, userId }) - ).rejects.toBeInstanceOf(ConflictException); + ).rejects.toBeInstanceOf(ForbiddenException); }); it('updates only a budget owned by the current user', async () => { @@ -373,6 +479,8 @@ describe('BudgetsService', () => { userId }); prismaService.budget.update.mockResolvedValue({ + account: null, + accountId: null, amount: 300, category: { createdAt, @@ -385,6 +493,8 @@ describe('BudgetsService', () => { currency: 'USD', id: budgetId, month: new Date('2026-06-01T00:00:00.000Z'), + name: 'Train pass', + type: 'EXPENSE', updatedAt, userId }); @@ -395,7 +505,9 @@ describe('BudgetsService', () => { categoryId, currency: 'USD', id: budgetId, - month: '2026-06' + month: '2026-06', + name: 'Train pass', + type: 'EXPENSE' }, id: budgetId, userId @@ -403,12 +515,15 @@ describe('BudgetsService', () => { expect(prismaService.budget.update).toHaveBeenCalledWith({ data: { + account: { disconnect: true }, amount: 300, category: { connect: { id: categoryId } }, currency: 'USD', - month: new Date('2026-06-01T00:00:00.000Z') + month: new Date('2026-06-01T00:00:00.000Z'), + name: 'Train pass', + type: 'EXPENSE' }, - include: { category: true }, + include: { account: true, category: true }, where: { id: budgetId } }); expect(response.amount).toBe(300); @@ -424,7 +539,9 @@ describe('BudgetsService', () => { categoryId, currency: 'USD', id: budgetId, - month: '2026-06' + month: '2026-06', + name: 'Train pass', + type: 'EXPENSE' }, id: budgetId, userId diff --git a/apps/api/src/app/budgets/budgets.service.ts b/apps/api/src/app/budgets/budgets.service.ts index 1e8f8e623..10d06f0e4 100644 --- a/apps/api/src/app/budgets/budgets.service.ts +++ b/apps/api/src/app/budgets/budgets.service.ts @@ -7,6 +7,7 @@ import { } from '@ghostfolio/common/dtos'; import { BudgetResponse, + type BudgetType, BudgetsResponse, ExpenseCategoryResponse } from '@ghostfolio/common/interfaces'; @@ -16,9 +17,15 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; +import type { Account } from '@prisma/client'; @Injectable() export class BudgetsService { + private static readonly SAVINGS_BUDGET_TYPES: BudgetType[] = [ + 'CASH_SAVINGS', + 'INVESTMENT_SAVINGS' + ]; + public constructor(private readonly prismaService: PrismaService) {} public async createCategory({ @@ -56,29 +63,35 @@ export class BudgetsService { userId }); - const month = this.parseBudgetMonth(data.month); - - const existingBudget = await this.prismaService.budget.findFirst({ - where: { - categoryId: data.categoryId, - month, + if (data.accountId) { + await this.validateAccountOwnership({ + accountId: data.accountId, userId - } - }); - - if (existingBudget) { - throw new ConflictException(); + }); } + const month = this.parseBudgetMonth(data.month); + const budget = await this.prismaService.budget.create({ data: { + ...(data.accountId + ? { + account: { + connect: { + id_userId: { id: data.accountId, userId } + } + } + } + : {}), amount: data.amount, category: { connect: { id: data.categoryId } }, currency: data.currency, month, + name: data.name, + type: data.type, user: { connect: { id: userId } } }, - include: { category: true } + include: { account: true, category: true } }); return this.toBudgetResponse({ budget, spent: 0 }); @@ -108,7 +121,7 @@ export class BudgetsService { userId: string; }): Promise { const budget = await this.prismaService.budget.findFirst({ - include: { category: true }, + include: { account: true, category: true }, where: { id, userId } }); @@ -151,15 +164,17 @@ export class BudgetsService { const nextMonth = this.getNextMonth(budgetMonth); const budgets = await this.prismaService.budget.findMany({ - include: { category: true }, - orderBy: { category: { name: 'asc' } }, + include: { account: true, category: true }, + orderBy: { name: 'asc' }, where: { month: budgetMonth, userId } }); - const categoryIds = budgets.map(({ categoryId }) => categoryId); + const categoryIds = Array.from( + new Set(budgets.map(({ categoryId }) => categoryId)) + ); const expenseSums = categoryIds.length ? await this.prismaService.expense.groupBy({ by: ['categoryId'], @@ -182,9 +197,13 @@ export class BudgetsService { ); const budgetResponses = budgets.map((budget) => { + const isExpenseBudget = this.isPlannedSpendBudgetType(budget.type); + return this.toBudgetResponse({ budget, - spent: spentByCategoryId.get(budget.categoryId) ?? 0 + spent: isExpenseBudget + ? (spentByCategoryId.get(budget.categoryId) ?? 0) + : 0 }); }); @@ -193,6 +212,14 @@ export class BudgetsService { totalBudgeted: budgetResponses.reduce((sum, { amount }) => { return sum + amount; }, 0), + totalMonthlySavings: budgetResponses.reduce((sum, { amount, type }) => { + return BudgetsService.SAVINGS_BUDGET_TYPES.includes(type) + ? sum + amount + : sum; + }, 0), + totalPlannedSpend: budgetResponses.reduce((sum, { amount, type }) => { + return this.isPlannedSpendBudgetType(type) ? sum + amount : sum; + }, 0), totalRemaining: budgetResponses.reduce((sum, { remaining }) => { return sum + remaining; }, 0), @@ -217,16 +244,32 @@ export class BudgetsService { userId }); + if (data.accountId) { + await this.validateAccountOwnership({ + accountId: data.accountId, + userId + }); + } + const month = this.parseBudgetMonth(data.month); const budget = await this.prismaService.budget.update({ data: { + account: data.accountId + ? { + connect: { + id_userId: { id: data.accountId, userId } + } + } + : { disconnect: true }, amount: data.amount, category: { connect: { id: data.categoryId } }, currency: data.currency, - month + month, + name: data.name, + type: data.type }, - include: { category: true }, + include: { account: true, category: true }, where: { id } }); @@ -270,6 +313,14 @@ export class BudgetsService { ); } + private isPlannedSpendBudgetType(type: BudgetType) { + return [ + 'EXPENSE', + 'LIABILITY_AUTOMATIC', + 'YEARLY_EXPENSE_AUTOMATIC' + ].includes(type); + } + private async getSpentForBudget({ categoryId, month, @@ -305,6 +356,8 @@ export class BudgetsService { }: { budget: { amount: number; + account?: Account | null; + accountId?: string | null; category: { color?: string; createdAt: Date; @@ -317,11 +370,15 @@ export class BudgetsService { currency: string; id: string; month: Date; + name: string; + type: BudgetType; updatedAt: Date; }; spent: number; }): BudgetResponse { return { + account: budget.account ?? undefined, + accountId: budget.accountId ?? undefined, amount: budget.amount, category: budget.category, categoryId: budget.categoryId, @@ -329,8 +386,10 @@ export class BudgetsService { currency: budget.currency, id: budget.id, month: this.formatBudgetMonth(budget.month), + name: budget.name || budget.category.name, remaining: budget.amount - spent, spent, + type: budget.type, updatedAt: budget.updatedAt }; } @@ -367,6 +426,25 @@ export class BudgetsService { } } + private async validateAccountOwnership({ + accountId, + userId + }: { + accountId: string; + userId: string; + }) { + const account = await this.prismaService.account.findFirst({ + where: { + id: accountId, + userId + } + }); + + if (!account) { + throw new ForbiddenException(); + } + } + private async validateCategoryOwnership({ categoryId, userId diff --git a/apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts b/apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts index 550ead68c..77c6abaab 100644 --- a/apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts +++ b/apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts @@ -40,6 +40,33 @@ jest.mock('@ghostfolio/ui/value', () => { return { GfValueComponent }; }); +jest.mock('@ionic/angular/standalone', () => { + const { Component, Input } = require('@angular/core'); + + @Component({ + selector: 'ion-icon', + template: '' + }) + class IonIcon { + @Input() public name: string; + } + + return { IonIcon }; +}); + +jest.mock('ionicons', () => { + return { + addIcons: jest.fn() + }; +}); + +jest.mock('ionicons/icons', () => { + return { + createOutline: {}, + trashOutline: {} + }; +}); + const { UserService } = require('@ghostfolio/client/services/user/user.service'); @@ -59,6 +86,16 @@ describe('GfBudgetPageComponent', () => { of({ budgets: [ { + account: { + balance: 0, + createdAt: new Date('2026-06-01'), + id: 'checking', + isExcluded: false, + name: 'Checking', + updatedAt: new Date('2026-06-01'), + userId: 'user-1' + }, + accountId: 'checking', amount: 500, category: { id: 'food', @@ -69,12 +106,16 @@ describe('GfBudgetPageComponent', () => { currency: 'USD', id: 'budget-1', month: '2026-06', + name: 'Food shop', remaining: 125, spent: 375, + type: 'EXPENSE', updatedAt: new Date('2026-06-01') } ], totalBudgeted: 500, + totalMonthlySavings: 0, + totalPlannedSpend: 500, totalRemaining: 125, totalSpent: 375 }) @@ -126,7 +167,18 @@ describe('GfBudgetPageComponent', () => { month: expect.stringMatching(/^\d{4}-\d{2}$/) }); expect(fixture.nativeElement.textContent).toContain('Budget'); + expect(fixture.nativeElement.textContent).toContain('Food shop'); expect(fixture.nativeElement.textContent).toContain('Food'); + expect(fixture.nativeElement.textContent).toContain('Checking'); + expect( + fixture.nativeElement.querySelector('[aria-label="Edit budget"] ion-icon') + ).not.toBeNull(); + expect( + fixture.nativeElement.querySelector( + '[aria-label="Delete budget"] ion-icon' + ) + ).not.toBeNull(); + expect(fixture.nativeElement.textContent).not.toContain('editdelete'); }); it('reloads budgets after creating a budget', async () => { @@ -163,7 +215,8 @@ describe('GfBudgetPageComponent', () => { await fixture.whenStable(); expect(dialog.open).toHaveBeenCalledWith(expect.any(Function), { - width: '36rem' + maxWidth: 'calc(100vw - 2rem)', + width: '42rem' }); }); }); diff --git a/apps/client/src/app/pages/portfolio/budget/budget-page.component.ts b/apps/client/src/app/pages/portfolio/budget/budget-page.component.ts index bfb7aa894..198d1da9a 100644 --- a/apps/client/src/app/pages/portfolio/budget/budget-page.component.ts +++ b/apps/client/src/app/pages/portfolio/budget/budget-page.component.ts @@ -1,5 +1,5 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { BudgetResponse, User } from '@ghostfolio/common/interfaces'; +import type { BudgetResponse, User } from '@ghostfolio/common/interfaces'; import { DataService } from '@ghostfolio/ui/services'; import { GfValueComponent } from '@ghostfolio/ui/value'; @@ -14,10 +14,12 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; -import { MatIconModule } from '@angular/material/icon'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { IonIcon } from '@ionic/angular/standalone'; import { format } from 'date-fns'; +import { addIcons } from 'ionicons'; +import { createOutline, trashOutline } from 'ionicons/icons'; import { GfCreateOrUpdateBudgetDialogComponent } from './create-or-update-budget-dialog/create-or-update-budget-dialog.component'; import { GfManageBudgetCategoriesDialogComponent } from './manage-budget-categories-dialog/manage-budget-categories-dialog.component'; @@ -27,8 +29,8 @@ import { GfManageBudgetCategoriesDialogComponent } from './manage-budget-categor imports: [ CommonModule, GfValueComponent, + IonIcon, MatButtonModule, - MatIconModule, MatProgressBarModule, MatTableModule, ReactiveFormsModule @@ -40,7 +42,10 @@ import { GfManageBudgetCategoriesDialogComponent } from './manage-budget-categor export class GfBudgetPageComponent implements OnInit { public dataSource = new MatTableDataSource([]); public displayedColumns = [ + 'name', 'category', + 'account', + 'type', 'amount', 'spent', 'remaining', @@ -52,6 +57,8 @@ export class GfBudgetPageComponent implements OnInit { nonNullable: true }); public totalBudgeted = 0; + public totalMonthlySavings = 0; + public totalPlannedSpend = 0; public totalRemaining = 0; public totalSpent = 0; public user: User; @@ -62,7 +69,9 @@ export class GfBudgetPageComponent implements OnInit { private destroyRef: DestroyRef, private dialog: MatDialog, private userService: UserService - ) {} + ) { + addIcons({ createOutline, trashOutline }); + } public ngOnInit() { this.userService.stateChanged @@ -89,14 +98,25 @@ export class GfBudgetPageComponent implements OnInit { this.dataService .fetchBudgets({ month: this.monthControl.value }) .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(({ budgets, totalBudgeted, totalRemaining, totalSpent }) => { - this.dataSource = new MatTableDataSource(budgets); - this.totalBudgeted = totalBudgeted; - this.totalRemaining = totalRemaining; - this.totalSpent = totalSpent; - this.isLoading = false; - this.changeDetectorRef.markForCheck(); - }); + .subscribe( + ({ + budgets, + totalBudgeted, + totalMonthlySavings, + totalPlannedSpend, + totalRemaining, + totalSpent + }) => { + this.dataSource = new MatTableDataSource(budgets); + this.totalBudgeted = totalBudgeted; + this.totalMonthlySavings = totalMonthlySavings; + this.totalPlannedSpend = totalPlannedSpend; + this.totalRemaining = totalRemaining; + this.totalSpent = totalSpent; + this.isLoading = false; + this.changeDetectorRef.markForCheck(); + } + ); } public getProgress({ amount, spent }: BudgetResponse) { @@ -107,6 +127,21 @@ export class GfBudgetPageComponent implements OnInit { return Math.min((spent / amount) * 100, 100); } + public getBudgetTypeLabel(type: BudgetResponse['type']) { + switch (type) { + case 'CASH_SAVINGS': + return $localize`Cash savings`; + case 'INVESTMENT_SAVINGS': + return $localize`Investment savings`; + case 'LIABILITY_AUTOMATIC': + return $localize`Liability`; + case 'YEARLY_EXPENSE_AUTOMATIC': + return $localize`Yearly expense`; + default: + return $localize`Expense`; + } + } + public onCreateBudget() { this.openBudgetDialog({ currency: this.getCurrency(), @@ -126,7 +161,8 @@ export class GfBudgetPageComponent implements OnInit { public onManageCategories() { this.dialog .open(GfManageBudgetCategoriesDialogComponent, { - width: '36rem' + maxWidth: 'calc(100vw - 2rem)', + width: '42rem' }) .afterClosed() .pipe(takeUntilDestroyed(this.destroyRef)) diff --git a/apps/client/src/app/pages/portfolio/budget/budget-page.html b/apps/client/src/app/pages/portfolio/budget/budget-page.html index 84f514b4b..07cf383ee 100644 --- a/apps/client/src/app/pages/portfolio/budget/budget-page.html +++ b/apps/client/src/app/pages/portfolio/budget/budget-page.html @@ -44,91 +44,120 @@ @if (dataSource.data.length > 0) { - - - - - +
+
Category - {{ budget.category?.name ?? budget.categoryId }} -
+ + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - -
Item + {{ budget.name }} + - Budget - - - Category + {{ budget.category?.name ?? budget.categoryId }} + - Spent - - - Account + {{ budget.account?.name ?? '-' }} + - Remaining - - - Type + {{ getBudgetTypeLabel(budget.type) }} + Progress - - + Budget + + + - - - + Spent + + +
+ + + Remaining + + + + + + + + Progress + + + + + + + + + + + + + + + + + } @else if (!isLoading) {
No budgets have been created for this month. diff --git a/apps/client/src/app/pages/portfolio/budget/budget-page.scss b/apps/client/src/app/pages/portfolio/budget/budget-page.scss index 469db050c..7def019ae 100644 --- a/apps/client/src/app/pages/portfolio/budget/budget-page.scss +++ b/apps/client/src/app/pages/portfolio/budget/budget-page.scss @@ -17,3 +17,26 @@ gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); } + +.budget-table-wrapper { + overflow-x: auto; +} + +.budget-table { + min-width: 56rem; +} + +.budget-table .mat-column-actions { + white-space: nowrap; + width: 6rem; +} + +.budget-action-button { + height: 2.25rem; + line-height: 2.25rem; + width: 2.25rem; +} + +.budget-action-button ion-icon { + font-size: 1.125rem; +} diff --git a/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.component.spec.ts b/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.component.spec.ts index 1d2240cad..53d83b1fe 100644 --- a/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.component.spec.ts +++ b/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.component.spec.ts @@ -10,12 +10,22 @@ import { GfCreateOrUpdateBudgetDialogComponent } from './create-or-update-budget-dialog.component'; +(global as any).$localize = ( + messageParts: TemplateStringsArray, + ...expressions: any[] +) => { + return String.raw({ raw: messageParts }, ...expressions); +}; + describe('GfCreateOrUpdateBudgetDialogComponent', () => { let component: GfCreateOrUpdateBudgetDialogComponent; let dataService: jest.Mocked< Pick< DataService, - 'createBudget' | 'fetchExpenseCategories' | 'updateBudget' + | 'createBudget' + | 'fetchAccounts' + | 'fetchExpenseCategories' + | 'updateBudget' > >; let dialogRef: jest.Mocked< @@ -27,12 +37,30 @@ describe('GfCreateOrUpdateBudgetDialogComponent', () => { dataService = { createBudget: jest.fn().mockReturnValue( of({ + accountId: 'checking', amount: 500, categoryId: 'food', id: 'budget-1', month: '2026-06', + name: 'Food shop', remaining: 500, - spent: 0 + spent: 0, + type: 'EXPENSE' + }) + ), + fetchAccounts: jest.fn().mockReturnValue( + of({ + accounts: [ + { + balance: 0, + createdAt: new Date('2026-06-01'), + id: 'checking', + isExcluded: false, + name: 'Checking', + updatedAt: new Date('2026-06-01'), + userId: 'user-1' + } + ] }) ), fetchExpenseCategories: jest.fn().mockReturnValue( @@ -48,12 +76,15 @@ describe('GfCreateOrUpdateBudgetDialogComponent', () => { ), updateBudget: jest.fn().mockReturnValue( of({ + accountId: 'checking', amount: 650, categoryId: 'food', id: 'budget-1', month: '2026-06', + name: 'Food shop', remaining: 650, - spent: 0 + spent: 0, + type: 'EXPENSE' }) ) }; @@ -92,20 +123,27 @@ describe('GfCreateOrUpdateBudgetDialogComponent', () => { }); component.budgetForm.setValue({ + accountId: 'checking', amount: 500, categoryId: 'food', - month: '2026-06' + month: '2026-06', + name: 'Food shop', + type: 'EXPENSE' }); component.onSubmit(); await fixture.whenStable(); expect(dataService.fetchExpenseCategories).toHaveBeenCalled(); + expect(dataService.fetchAccounts).toHaveBeenCalled(); expect(dataService.createBudget).toHaveBeenCalledWith({ + accountId: 'checking', amount: 500, categoryId: 'food', currency: 'USD', - month: '2026-06' + month: '2026-06', + name: 'Food shop', + type: 'EXPENSE' }); expect(dialogRef.close).toHaveBeenCalledWith({ refresh: true }); }); @@ -113,6 +151,7 @@ describe('GfCreateOrUpdateBudgetDialogComponent', () => { it('updates a budget and closes with refresh', async () => { await setup({ budget: { + accountId: 'checking', amount: 500, category: { id: 'food', @@ -123,8 +162,10 @@ describe('GfCreateOrUpdateBudgetDialogComponent', () => { currency: 'USD', id: 'budget-1', month: '2026-06', + name: 'Food shop', remaining: 500, spent: 0, + type: 'EXPENSE', updatedAt: new Date('2026-06-01') }, currency: 'USD', @@ -132,9 +173,12 @@ describe('GfCreateOrUpdateBudgetDialogComponent', () => { }); component.budgetForm.setValue({ + accountId: '', amount: 650, categoryId: 'food', - month: '2026-06' + month: '2026-06', + name: 'Food shop', + type: 'EXPENSE' }); component.onSubmit(); @@ -146,7 +190,9 @@ describe('GfCreateOrUpdateBudgetDialogComponent', () => { categoryId: 'food', currency: 'USD', id: 'budget-1', - month: '2026-06' + month: '2026-06', + name: 'Food shop', + type: 'EXPENSE' }, id: 'budget-1' }); diff --git a/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.component.ts b/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.component.ts index 11442d642..fffd9adca 100644 --- a/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.component.ts @@ -1,8 +1,12 @@ -import { CreateBudgetDto, UpdateBudgetDto } from '@ghostfolio/common/dtos'; -import { - BudgetResponse, - ExpenseCategoryResponse -} from '@ghostfolio/common/interfaces'; +import type { + CreateBudgetDto, + ManualBudgetType +} from '@ghostfolio/common/dtos/create-budget.dto'; +import type { UpdateBudgetDto } from '@ghostfolio/common/dtos/update-budget.dto'; +import type { AccountsResponse } from '@ghostfolio/common/interfaces/responses/accounts-response.interface'; +import type { BudgetResponse } from '@ghostfolio/common/interfaces/responses/budget-response.interface'; +import type { ExpenseCategoryResponse } from '@ghostfolio/common/interfaces/responses/expense-category-response.interface'; +import type { AccountWithValue } from '@ghostfolio/common/types'; import { DataService } from '@ghostfolio/ui/services'; import { CommonModule } from '@angular/common'; @@ -46,6 +50,9 @@ export interface CreateOrUpdateBudgetDialogData { }) export class GfCreateOrUpdateBudgetDialogComponent implements OnInit { public budgetForm = new FormGroup({ + accountId: new FormControl(this.data.budget?.accountId ?? '', { + nonNullable: true + }), amount: new FormControl(this.data.budget?.amount ?? 0, { nonNullable: true, validators: [Validators.required, Validators.min(0.01)] @@ -57,9 +64,27 @@ export class GfCreateOrUpdateBudgetDialogComponent implements OnInit { month: new FormControl(this.data.budget?.month ?? this.data.month, { nonNullable: true, validators: [Validators.required] - }) + }), + name: new FormControl(this.data.budget?.name ?? '', { + nonNullable: true, + validators: [Validators.required] + }), + type: new FormControl( + (this.data.budget?.type as ManualBudgetType) ?? 'EXPENSE', + { + nonNullable: true, + validators: [Validators.required] + } + ) }); + public accounts: AccountWithValue[] = []; + public budgetTypes: Array<{ label: string; value: ManualBudgetType }> = [ + { label: $localize`Expense`, value: 'EXPENSE' }, + { label: $localize`Cash savings`, value: 'CASH_SAVINGS' }, + { label: $localize`Investment savings`, value: 'INVESTMENT_SAVINGS' } + ]; public categories: ExpenseCategoryResponse[] = []; + public isLoadingAccounts = true; public isLoadingCategories = true; public constructor( @@ -70,6 +95,14 @@ export class GfCreateOrUpdateBudgetDialogComponent implements OnInit { ) {} public ngOnInit() { + this.dataService + .fetchAccounts() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ accounts }: AccountsResponse) => { + this.accounts = accounts; + this.isLoadingAccounts = false; + }); + this.dataService .fetchExpenseCategories() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -85,8 +118,10 @@ export class GfCreateOrUpdateBudgetDialogComponent implements OnInit { return; } + const { accountId, ...formValue } = this.budgetForm.getRawValue(); const budget = { - ...this.budgetForm.getRawValue(), + ...formValue, + ...(accountId ? { accountId } : {}), currency: this.data.currency }; diff --git a/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.html b/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.html index 267b92b16..202e4396b 100644 --- a/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.html +++ b/apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/create-or-update-budget-dialog.html @@ -8,11 +8,27 @@
+ + Item name + + + Month + + Type + + @for (budgetType of budgetTypes; track budgetType.value) { + + {{ budgetType.label }} + + } + + + Category @@ -30,6 +46,22 @@ + + Account + + No account + @if (isLoadingAccounts) { + Loading accounts... + } @else { + @for (account of accounts; track account.id) { + + {{ account.name ?? account.id }} + + } + } + + + Amount { + const { Component, Input } = require('@angular/core'); + + @Component({ + selector: 'ion-icon', + template: '' + }) + class IonIcon { + @Input() public name: string; + } + + return { IonIcon }; +}); + +jest.mock('ionicons', () => { + return { + addIcons: jest.fn() + }; +}); + +jest.mock('ionicons/icons', () => { + return { + createOutline: {}, + trashOutline: {} + }; +}); + describe('GfManageBudgetCategoriesDialogComponent', () => { const createdAt = new Date('2026-06-01'); const updatedAt = new Date('2026-06-01'); @@ -85,6 +112,17 @@ describe('GfManageBudgetCategoriesDialogComponent', () => { it('loads expense categories', async () => { await fixture.whenStable(); + expect( + fixture.nativeElement.querySelector( + '[aria-label="Edit category"] ion-icon' + ) + ).not.toBeNull(); + expect( + fixture.nativeElement.querySelector( + '[aria-label="Delete category"] ion-icon' + ) + ).not.toBeNull(); + expect(fixture.nativeElement.textContent).not.toContain('editdelete'); expect(dataService.fetchExpenseCategories).toHaveBeenCalled(); expect(component.dataSource.data).toEqual([ { @@ -115,6 +153,68 @@ describe('GfManageBudgetCategoriesDialogComponent', () => { expect(dataService.fetchExpenseCategories).toHaveBeenCalledTimes(2); }); + it('resets the category form interaction state after creating a category', async () => { + await fixture.whenStable(); + + component.categoryForm.setValue({ + color: '#0055aa', + name: 'Transport' + }); + component.categoryForm.markAllAsTouched(); + + component.onSubmit(); + await fixture.whenStable(); + + expect(component.categoryForm.controls.name.value).toBe(''); + expect(component.categoryForm.controls.name.touched).toBe(false); + expect(component.categoryForm.controls.name.dirty).toBe(false); + }); + + it('does not show the name field as invalid after creating a category', async () => { + await fixture.whenStable(); + + component.categoryForm.setValue({ + color: '#0055aa', + name: 'Transport' + }); + + const form: HTMLFormElement = + fixture.nativeElement.querySelector('.category-form'); + + form.dispatchEvent(new Event('submit')); + await fixture.whenStable(); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector( + '.category-name-field.mat-form-field-invalid' + ) + ).toBeNull(); + }); + + it('updates the color form value from the color picker', async () => { + await fixture.whenStable(); + + const colorInput: HTMLInputElement = fixture.nativeElement.querySelector( + 'input[type="color"]' + ); + + colorInput.value = '#aa5500'; + colorInput.dispatchEvent(new Event('input')); + fixture.detectChanges(); + + expect(component.categoryForm.controls.color.value).toBe('#aa5500'); + expect(fixture.nativeElement.textContent).toContain('#aa5500'); + }); + + it('clears the selected category color', async () => { + await fixture.whenStable(); + + component.onClearColor(); + + expect(component.categoryForm.controls.color.value).toBe(''); + }); + it('updates an expense category and reloads categories', async () => { await fixture.whenStable(); diff --git a/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.ts b/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.ts index b00dbe107..97e7e4745 100644 --- a/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.ts @@ -12,23 +12,26 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, FormGroup, + FormGroupDirective, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { IonIcon } from '@ionic/angular/standalone'; +import { addIcons } from 'ionicons'; +import { createOutline, trashOutline } from 'ionicons/icons'; @Component({ imports: [ CommonModule, + IonIcon, MatButtonModule, MatDialogModule, MatFormFieldModule, - MatIconModule, MatInputModule, MatTableModule, ReactiveFormsModule @@ -39,7 +42,7 @@ import { MatTableDataSource, MatTableModule } from '@angular/material/table'; }) export class GfManageBudgetCategoriesDialogComponent implements OnInit { public categoryForm = new FormGroup({ - color: new FormControl('', { + color: new FormControl('#0055aa', { nonNullable: true, validators: [Validators.pattern(/^#[0-9a-fA-F]{6}$/)] }), @@ -58,18 +61,27 @@ export class GfManageBudgetCategoriesDialogComponent implements OnInit { private dataService: DataService, private destroyRef: DestroyRef, private dialogRef: MatDialogRef - ) {} + ) { + addIcons({ createOutline, trashOutline }); + } public ngOnInit() { this.fetchCategories(); } - public onCancelEdit() { + public onCancelEdit(formDirective?: FormGroupDirective) { this.editingCategory = undefined; - this.categoryForm.reset({ - color: '', + + const formValue = { + color: '#0055aa', name: '' - }); + }; + + if (formDirective) { + formDirective.resetForm(formValue); + } else { + this.categoryForm.reset(formValue); + } } public onClose() { @@ -91,9 +103,15 @@ export class GfManageBudgetCategoriesDialogComponent implements OnInit { color: category.color ?? '', name: category.name }); + this.categoryForm.markAsPristine(); + this.categoryForm.markAsUntouched(); + } + + public onClearColor() { + this.categoryForm.controls.color.setValue(''); } - public onSubmit() { + public onSubmit(formDirective?: FormGroupDirective) { if (this.categoryForm.invalid) { this.categoryForm.markAllAsTouched(); return; @@ -112,7 +130,7 @@ export class GfManageBudgetCategoriesDialogComponent implements OnInit { }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { - this.onCancelEdit(); + this.onCancelEdit(formDirective); this.fetchCategories(); }); } else { @@ -120,7 +138,7 @@ export class GfManageBudgetCategoriesDialogComponent implements OnInit { .createExpenseCategory(category) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { - this.onCancelEdit(); + this.onCancelEdit(formDirective); this.fetchCategories(); }); } diff --git a/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html b/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html index a12f575bc..23b0410a3 100644 --- a/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html +++ b/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html @@ -2,19 +2,55 @@ - + Name - - Color - - +
+ +
+ + + + + + {{ categoryForm.controls.color.value || '#0055aa' }} + + @if (categoryForm.controls.color.value) { + + } +
+
@if (editingCategory) { @@ -40,7 +76,11 @@ @if (dataSource.data.length > 0) { - +
@@ -62,23 +102,33 @@ - - + diff --git a/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.scss b/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.scss index 7b34e313e..bd230a6e1 100644 --- a/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.scss +++ b/apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.scss @@ -1,25 +1,147 @@ +:host { + display: block; +} + .category-form { + align-items: flex-start; display: grid; - gap: 1rem; - grid-template-columns: minmax(0, 1fr) minmax(0, 10rem) auto; + gap: 1rem 1rem; + grid-template-columns: minmax(14rem, 1fr) minmax(14rem, 16rem) auto; + padding-top: 0.75rem; } .category-form-actions { - align-items: center; + align-items: flex-start; display: flex; gap: 0.5rem; justify-content: flex-end; + padding-top: 0.6875rem; +} + +.category-name-field { + width: 100%; +} + +.category-color-picker-field { + position: relative; +} + +.category-color-picker-label { + background-color: var(--light-background); + color: rgba(var(--palette-foreground-text), 0.72); + font-size: 0.75rem; + left: 0.75rem; + line-height: 1rem; + padding: 0 0.25rem; + position: absolute; + top: -0.5rem; + z-index: 1; +} + +:host-context(.theme-dark) .category-color-picker-label { + background-color: var(--dark-background); + color: rgba(var(--palette-foreground-text-dark), 0.72); +} + +.category-color-picker-control { + align-items: center; + border: 1px solid rgba(var(--palette-foreground-divider), 0.32); + border-radius: 0.25rem; + display: flex; + gap: 0.75rem; + height: 3.5rem; + padding: 0 0.75rem; +} + +.category-color-picker-control:focus-within { + border-color: #1ec8c8; + box-shadow: inset 0 0 0 1px #1ec8c8; +} + +.category-color-picker-swatch-wrap { + border-radius: 0.25rem; + display: inline-flex; + flex: 0 0 auto; + height: 2rem; + overflow: hidden; + position: relative; + width: 2rem; +} + +.category-color-picker-swatch { + border: 1px solid rgba(var(--palette-foreground-divider), 0.24); + border-radius: 0.25rem; + display: block; + height: 100%; + width: 100%; +} + +.category-color-picker { + appearance: none; + -webkit-appearance: none; + background: none; + border: 0; + cursor: pointer; + inset: 0; + opacity: 0; + padding: 0; + position: absolute; + width: 100%; +} + +.category-color-picker-value { + font-family: monospace; + font-size: 0.875rem; + white-space: nowrap; +} + +.category-clear-color-button { + margin-left: auto; + min-width: auto; + padding: 0 0.5rem; +} + +.category-table { + table-layout: fixed; +} + +.category-table .mat-column-name { + width: 45%; +} + +.category-table .mat-column-color { + width: 35%; +} + +.category-actions-column { + white-space: nowrap; + width: 6rem; +} + +.category-action-button { + height: 2.25rem; + line-height: 2.25rem; + width: 2.25rem; +} + +.category-action-button ion-icon { + font-size: 1.125rem; } .category-color { align-items: center; display: inline-flex; gap: 0.5rem; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .category-color-swatch { border: 1px solid var(--border-color); display: inline-block; + flex: 0 0 auto; height: 1rem; width: 1rem; } @@ -31,5 +153,14 @@ .category-form-actions { justify-content: flex-start; + min-height: auto; + } + + .category-table { + min-width: 32rem; + } + + mat-dialog-content { + overflow-x: auto; } } diff --git a/docs/superpowers/plans/2026-06-24-budget-ui-repair.md b/docs/superpowers/plans/2026-06-24-budget-ui-repair.md new file mode 100644 index 000000000..de26b1f1e --- /dev/null +++ b/docs/superpowers/plans/2026-06-24-budget-ui-repair.md @@ -0,0 +1,547 @@ +# Budget UI Repair Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix the broken Budget page category management UI so the dialog is readable, actions render as compact icons, and the layout works in dark mode at desktop and narrow widths. + +**Architecture:** Keep the existing Angular Material dialog/table pattern, but stop relying on Material icon font ligatures for action buttons in the Budget feature. Use Ghostfolio's existing Ionic icon pattern for edit/delete actions, constrain action columns, and make the category form/table responsive with stable widths. + +**Tech Stack:** Angular 21 standalone components, Angular Material dialog/table/form controls, Ionic standalone icons, Jest, Nx. + +--- + +## Current Breakage From Screenshots + +- In `Manage categories`, action icons render as visible text (`edit delete`) and overflow the table row. +- The dialog content is too narrow for the form plus action column, so controls feel cramped and clipped. +- The category table action column has no stable width, so icon/text fallback can steal space from data columns. +- The Budget page uses the same `mat-icon` pattern for budget row edit/delete actions, so it should be fixed in the same pass. + +## File Structure + +- Modify `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.ts`: replace `MatIconModule` dependency with `IonIcon`, register `createOutline` and `trashOutline`. +- Modify `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html`: replace `` action content with ``, add action button classes, and improve table/action markup. +- Modify `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.scss`: add responsive dialog form layout, stable action column/button sizing, color swatch truncation, and mobile table behavior. +- Modify `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts`: assert icon buttons exist by aria labels, form actions remain usable, and category color renders without exposing raw icon ligature text. +- Modify `apps/client/src/app/pages/portfolio/budget/budget-page.component.ts`: replace `MatIconModule` dependency with `IonIcon`, register `createOutline` and `trashOutline`, widen category dialog. +- Modify `apps/client/src/app/pages/portfolio/budget/budget-page.html`: replace budget row `` actions with Ionic icons and stable action button classes. +- Modify `apps/client/src/app/pages/portfolio/budget/budget-page.scss`: add stable table action sizing and horizontal overflow handling for dense budget rows. +- Modify `apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts`: assert budget row actions render without raw icon words and dialog opens with the wider dimensions. + +--- + +## Task 1: Replace Broken Material Icon Ligatures In Category Dialog + +**Files:** + +- Modify: `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.ts` +- Modify: `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html` +- Modify: `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts` + +- [ ] **Step 1: Write the failing icon rendering test** + +Add this assertion to `it('loads expense categories', ...)` after `await fixture.whenStable();`: + +```ts +expect( + fixture.nativeElement.querySelector('[aria-label="Edit category"] ion-icon') +).not.toBeNull(); +expect( + fixture.nativeElement.querySelector('[aria-label="Delete category"] ion-icon') +).not.toBeNull(); +expect(fixture.nativeElement.textContent).not.toContain('editdelete'); +``` + +- [ ] **Step 2: Run the category dialog spec and confirm it fails** + +Run: + +```bash +./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts --runInBand --watchman=false +``` + +Expected: FAIL because the template still uses `` instead of ``. + +- [ ] **Step 3: Replace Material icons with Ionic icons in the component** + +In `manage-budget-categories-dialog.component.ts`, replace the `MatIconModule` import and component dependency with Ionic icons: + +```ts +import { IonIcon } from '@ionic/angular/standalone'; +import { addIcons } from 'ionicons'; +import { createOutline, trashOutline } from 'ionicons/icons'; +``` + +Remove: + +```ts +import { MatIconModule } from '@angular/material/icon'; +``` + +Update `imports`: + +```ts +imports: [ + CommonModule, + IonIcon, + MatButtonModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + MatTableModule, + ReactiveFormsModule +], +``` + +Add this constructor body after dependency injection parameters: + +```ts + ) { + addIcons({ createOutline, trashOutline }); + } +``` + +- [ ] **Step 4: Replace the category action button markup** + +In `manage-budget-categories-dialog.html`, replace the two `` button bodies with: + +```html + + +``` + +- [ ] **Step 5: Run the category dialog spec and confirm it passes** + +Run: + +```bash +./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts --runInBand --watchman=false +``` + +Expected: PASS. + +--- + +## Task 2: Repair Category Dialog Layout And Responsiveness + +**Files:** + +- Modify: `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html` +- Modify: `apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.scss` +- Modify: `apps/client/src/app/pages/portfolio/budget/budget-page.component.ts` + +- [ ] **Step 1: Widen the dialog safely** + +In `budget-page.component.ts`, update `onManageCategories()`: + +```ts +this.dialog + .open(GfManageBudgetCategoriesDialogComponent, { + maxWidth: 'calc(100vw - 2rem)', + width: '42rem' + }) +``` + +- [ ] **Step 2: Add stable table classes in the category template** + +In `manage-budget-categories-dialog.html`, update the table element: + +```html +
Name {{ category.name }} +
+``` + +Update the actions column cells: + +```html + +
+``` + +- [ ] **Step 3: Add defensive layout CSS** + +Replace `manage-budget-categories-dialog.scss` with: + +```scss +:host { + display: block; +} + +.category-form { + align-items: start; + display: grid; + gap: 1rem; + grid-template-columns: minmax(12rem, 1fr) minmax(8rem, 10rem) auto; +} + +.category-form-actions { + align-items: center; + display: flex; + gap: 0.5rem; + justify-content: flex-end; + min-height: 3.5rem; +} + +.category-table { + table-layout: fixed; +} + +.category-table .mat-column-name { + width: 45%; +} + +.category-table .mat-column-color { + width: 35%; +} + +.category-actions-column { + white-space: nowrap; + width: 6rem; +} + +.category-action-button { + height: 2.25rem; + line-height: 2.25rem; + width: 2.25rem; +} + +.category-action-button ion-icon { + font-size: 1.125rem; +} + +.category-color { + align-items: center; + display: inline-flex; + gap: 0.5rem; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.category-color-swatch { + border: 1px solid var(--border-color); + display: inline-block; + flex: 0 0 auto; + height: 1rem; + width: 1rem; +} + +@media (max-width: 767.98px) { + .category-form { + grid-template-columns: 1fr; + } + + .category-form-actions { + justify-content: flex-start; + min-height: auto; + } + + .category-table { + min-width: 32rem; + } + + mat-dialog-content { + overflow-x: auto; + } +} +``` + +- [ ] **Step 4: Update the budget page dialog width test** + +In `budget-page.component.spec.ts`, update the category dialog expectation to: + +```ts +expect(dialog.open).toHaveBeenCalledWith(expect.any(Function), { + maxWidth: 'calc(100vw - 2rem)', + width: '42rem' +}); +``` + +- [ ] **Step 5: Run the page and category specs** + +Run: + +```bash +./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts --runInBand --watchman=false +./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts --runInBand --watchman=false +``` + +Expected: both PASS. + +--- + +## Task 3: Fix Budget Table Row Action Icons And Dense Table Overflow + +**Files:** + +- Modify: `apps/client/src/app/pages/portfolio/budget/budget-page.component.ts` +- Modify: `apps/client/src/app/pages/portfolio/budget/budget-page.html` +- Modify: `apps/client/src/app/pages/portfolio/budget/budget-page.scss` +- Modify: `apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts` + +- [ ] **Step 1: Add failing budget action icon assertions** + +In `budget-page.component.spec.ts`, inside `it('loads and renders budgets for the selected month', ...)`, add: + +```ts +expect( + fixture.nativeElement.querySelector('[aria-label="Edit budget"] ion-icon') +).not.toBeNull(); +expect( + fixture.nativeElement.querySelector('[aria-label="Delete budget"] ion-icon') +).not.toBeNull(); +expect(fixture.nativeElement.textContent).not.toContain('editdelete'); +``` + +- [ ] **Step 2: Run the budget page spec and confirm it fails** + +Run: + +```bash +./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts --runInBand --watchman=false +``` + +Expected: FAIL because budget row actions still use ``. + +- [ ] **Step 3: Replace Material icon dependency in the budget page** + +In `budget-page.component.ts`, add: + +```ts +import { IonIcon } from '@ionic/angular/standalone'; +import { addIcons } from 'ionicons'; +import { createOutline, trashOutline } from 'ionicons/icons'; +``` + +Remove: + +```ts +import { MatIconModule } from '@angular/material/icon'; +``` + +Update `imports`: + +```ts +imports: [ + CommonModule, + GfValueComponent, + IonIcon, + MatButtonModule, + MatProgressBarModule, + MatTableModule, + ReactiveFormsModule +], +``` + +Add this constructor body after dependency injection parameters: + +```ts + ) { + addIcons({ createOutline, trashOutline }); + } +``` + +- [ ] **Step 4: Replace budget row action markup** + +In `budget-page.html`, replace the two budget action button bodies with: + +```html + + +``` + +- [ ] **Step 5: Add stable budget table action CSS** + +Append to `budget-page.scss`: + +```scss +.budget-table-wrapper { + overflow-x: auto; +} + +.budget-table { + min-width: 56rem; +} + +.budget-table .mat-column-actions { + white-space: nowrap; + width: 6rem; +} + +.budget-action-button { + height: 2.25rem; + line-height: 2.25rem; + width: 2.25rem; +} + +.budget-action-button ion-icon { + font-size: 1.125rem; +} +``` + +Wrap the table in `budget-page.html`: + +```html +
+ + ... +
+
+``` + +- [ ] **Step 6: Run the budget page spec** + +Run: + +```bash +./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts --runInBand --watchman=false +``` + +Expected: PASS. + +--- + +## Task 4: Visual Verification In Browser + +**Files:** + +- No source edits expected unless verification finds a regression. + +- [ ] **Step 1: Start the dev server** + +Run: + +```bash +pnpm start:dev +``` + +If the project uses the existing Nx command instead, run: + +```bash +./node_modules/.bin/nx run client:serve:development-en --hmr -o +``` + +Expected: Budget page is available on the local dev server. + +- [ ] **Step 2: Open Budget page and Manage categories** + +Use the browser to navigate to the Budget page, then click `Manage categories`. + +Expected: + +- Dialog width is about `42rem` on desktop. +- Form fields and Create button are aligned on one row at desktop width. +- Table actions are two compact icon buttons, not visible `edit delete` text. +- The close button remains visible and aligned to the right. + +- [ ] **Step 3: Verify narrow width** + +Resize the viewport below `768px`. + +Expected: + +- Category form stacks vertically. +- Table can scroll horizontally if needed. +- No controls overlap or clip text. + +--- + +## Task 5: Final Verification And Commit + +**Files:** + +- All modified Budget UI files. + +- [ ] **Step 1: Run focused tests** + +Run: + +```bash +./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts --runInBand --watchman=false +./node_modules/.bin/jest --config apps/client/jest.config.ts apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts --runInBand --watchman=false +``` + +Expected: both PASS. + +- [ ] **Step 2: Run Angular compiler** + +Run: + +```bash +./node_modules/.bin/ngc -p apps/client/tsconfig.app.json +``` + +Expected: exits with code 0. + +- [ ] **Step 3: Run client build if the local environment allows it** + +Run: + +```bash +./node_modules/.bin/nx run client:build:development-en +``` + +Expected: PASS. If it still exits without diagnostics in this sandbox, record that `ngc` and focused specs passed, then rerun in the normal user terminal before merging. + +- [ ] **Step 4: Commit** + +Run: + +```bash +git add apps/client/src/app/pages/portfolio/budget +git commit -m "fix: repair budget category dialog layout" +``` + +Expected: commit succeeds. + +--- + +## Self-Review + +- Spec coverage: The plan directly addresses the visible broken category dialog, icon text overflow, table action clipping, and responsive layout. +- Placeholder scan: No `TODO`, `TBD`, or unspecified implementation steps remain. +- Type consistency: Uses `IonIcon`, `addIcons`, `createOutline`, and `trashOutline` consistently in both Budget page and category dialog. diff --git a/libs/common/src/lib/dtos/create-budget.dto.ts b/libs/common/src/lib/dtos/create-budget.dto.ts index a58a25afa..4167dc139 100644 --- a/libs/common/src/lib/dtos/create-budget.dto.ts +++ b/libs/common/src/lib/dtos/create-budget.dto.ts @@ -1,8 +1,27 @@ import { IsCurrencyCode } from '@ghostfolio/common/validators/is-currency-code'; -import { IsNumber, IsString, Matches, Min } from 'class-validator'; +import { + IsIn, + IsNumber, + IsOptional, + IsString, + Matches, + Min +} from 'class-validator'; + +export const MANUAL_BUDGET_TYPES = [ + 'EXPENSE', + 'CASH_SAVINGS', + 'INVESTMENT_SAVINGS' +] as const; + +export type ManualBudgetType = (typeof MANUAL_BUDGET_TYPES)[number]; export class CreateBudgetDto { + @IsOptional() + @IsString() + accountId?: string; + @IsNumber() @Min(0) amount: number; @@ -15,4 +34,10 @@ export class CreateBudgetDto { @Matches(/^\d{4}-\d{2}$/) month: string; + + @IsString() + name: string; + + @IsIn(MANUAL_BUDGET_TYPES) + type: ManualBudgetType; } diff --git a/libs/common/src/lib/dtos/index.ts b/libs/common/src/lib/dtos/index.ts index fdd967002..90883e9cb 100644 --- a/libs/common/src/lib/dtos/index.ts +++ b/libs/common/src/lib/dtos/index.ts @@ -5,7 +5,8 @@ import { CreateAccountWithBalancesDto } from './create-account-with-balances.dto import { CreateAccountDto } from './create-account.dto'; import { CreateAssetProfileWithMarketDataDto } from './create-asset-profile-with-market-data.dto'; import { CreateAssetProfileDto } from './create-asset-profile.dto'; -import { CreateBudgetDto } from './create-budget.dto'; +import { CreateBudgetDto, MANUAL_BUDGET_TYPES } from './create-budget.dto'; +import type { ManualBudgetType } from './create-budget.dto'; import { CreateExpenseCategoryDto } from './create-expense-category.dto'; import { CreateExpenseDto } from './create-expense.dto'; import { CreateOrderDto } from './create-order.dto'; @@ -39,6 +40,8 @@ export { CreateAssetProfileDto, CreateAssetProfileWithMarketDataDto, CreateBudgetDto, + MANUAL_BUDGET_TYPES, + type ManualBudgetType, CreateExpenseCategoryDto, CreateExpenseDto, CreateOrderDto, diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index e3f67799e..39d819449 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -53,7 +53,9 @@ import type { AssetResponse } from './responses/asset-response.interface'; import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface'; import type { BenchmarkResponse } from './responses/benchmark-response.interface'; import type { + BudgetAccountResponse, BudgetResponse, + BudgetType, BudgetsResponse } from './responses/budget-response.interface'; import type { CreateStripeCheckoutSessionResponse } from './responses/create-stripe-checkout-session-response.interface'; @@ -140,7 +142,9 @@ export { BenchmarkMarketDataDetailsResponse, BenchmarkProperty, BenchmarkResponse, + type BudgetAccountResponse, BudgetResponse, + type BudgetType, BudgetsResponse, Coupon, CreateStripeCheckoutSessionResponse, diff --git a/libs/common/src/lib/interfaces/responses/budget-response.interface.ts b/libs/common/src/lib/interfaces/responses/budget-response.interface.ts index ec4cd9ea4..11ff3f616 100644 --- a/libs/common/src/lib/interfaces/responses/budget-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/budget-response.interface.ts @@ -1,6 +1,20 @@ import { ExpenseCategoryResponse } from './expense-category-response.interface'; +export interface BudgetAccountResponse { + id: string; + name?: string; +} + +export type BudgetType = + | 'CASH_SAVINGS' + | 'EXPENSE' + | 'INVESTMENT_SAVINGS' + | 'LIABILITY_AUTOMATIC' + | 'YEARLY_EXPENSE_AUTOMATIC'; + export interface BudgetResponse { + account?: BudgetAccountResponse; + accountId?: string; amount: number; category: ExpenseCategoryResponse; categoryId: string; @@ -8,14 +22,18 @@ export interface BudgetResponse { currency: string; id: string; month: string; + name: string; remaining: number; spent: number; + type: BudgetType; updatedAt: Date; } export interface BudgetsResponse { budgets: BudgetResponse[]; totalBudgeted: number; + totalMonthlySavings: number; + totalPlannedSpend: number; totalRemaining: number; totalSpent: number; } diff --git a/libs/ui/src/lib/services/data.service.spec.ts b/libs/ui/src/lib/services/data.service.spec.ts index b121c3374..56f11b748 100644 --- a/libs/ui/src/lib/services/data.service.spec.ts +++ b/libs/ui/src/lib/services/data.service.spec.ts @@ -50,10 +50,13 @@ describe('DataService budget methods', () => { it('creates a budget', () => { const budget: CreateBudgetDto = { + accountId: 'account-1', amount: 500, categoryId: 'category-1', currency: 'USD', - month: '2026-06' + month: '2026-06', + name: 'Groceries', + type: 'EXPENSE' }; dataService.createBudget(budget).subscribe(); @@ -70,7 +73,9 @@ describe('DataService budget methods', () => { categoryId: 'category-1', currency: 'USD', id: 'budget-1', - month: '2026-06' + month: '2026-06', + name: 'Groceries', + type: 'EXPENSE' }; dataService.updateBudget({ budget, id: 'budget-1' }).subscribe(); diff --git a/package-lock.json b/package-lock.json index 467dfcd0b..4339af98d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -503,6 +503,7 @@ "integrity": "sha512-eUWcWiMOUg01QyJkaO/csyTy8ZecW2HrMvOlgM+IWLagDyqFQVcSwcx9/NJy/42YA+EtpBcCnXPK3vk5NVob7w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "0.2102.6", @@ -898,6 +899,7 @@ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.6.tgz", "integrity": "sha512-u5gPTAY7MC02uACQE39xxiFcm1hslF+ih/f2borMWnhER0JNTpHjLiLRXFkq7or7+VVHU30zfhK4XNAuO4WTIg==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "8.18.0", "ajv-formats": "3.0.1", @@ -934,6 +936,7 @@ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.6.tgz", "integrity": "sha512-hk2duJlPJyiMaI9MVWA5XpmlpD9C4n8qgquV/MJ7/n+ZRSwW3w1ndL5qUmA1ki+4Da54v/Rc8Wt5tUS955+93w==", "license": "MIT", + "peer": true, "dependencies": { "@angular-devkit/core": "21.2.6", "jsonc-parser": "3.3.1", @@ -1006,6 +1009,7 @@ "integrity": "sha512-TCb3qYOC/uXKZCo56cJ6N9sHeWdFhyVqrbbYfFjTi09081T6jllgHDZL5Ms7gOMNY8KywWGGbhxwvzeA0RwTgA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@angular-eslint/bundled-angular-compiler": "21.2.0", "eslint-scope": "^9.0.0" @@ -1035,6 +1039,7 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.7.tgz", "integrity": "sha512-h8tUjQVSWfi2fohzxXeDDTjCfWABioYlPMrV1j98wCcFJad3FSnKCY0/gq8B4X6V81NGV29nEnhPyV0GinUBpQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1150,7 +1155,6 @@ "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.18.1" } @@ -1160,6 +1164,7 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.5.tgz", "integrity": "sha512-F1sVqMAGYoiJNYYaR2cerqTo7IqpxQ3ZtMDxR3rtB0rSSd5UPOIQoqpsfSd6uH8FVnuzKaBII8Mg6YrjClFsng==", "license": "MIT", + "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -1177,6 +1182,7 @@ "integrity": "sha512-I5DOFcIT1HKymyy2f78fjgD0Iv6jG46GbBZ/VxejcnhjubFpuN4CwPdugXf9rIDs8KZQqBzDBFUbq11vnk8h0A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@angular-devkit/architect": "0.2102.6", "@angular-devkit/core": "21.2.6", @@ -1250,6 +1256,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.7.tgz", "integrity": "sha512-YFdnU5z8JloJjLYa52OyCOULQhqEE/ym7vKfABySWDsiVXZr9FNmKMeZi/lUcg7ZO22UbBihqW9a9D6VSHOo+g==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1266,6 +1273,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.7.tgz", "integrity": "sha512-4J0Nl5gGmr5SKgR3FHK4J6rdG0aP5zAsY3AJU8YXH+D98CeNTjQUD8XHsdD2cTwo08V5mDdFa5VCsREpMPJ5gQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1278,6 +1286,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.7.tgz", "integrity": "sha512-r76vKBM7Wu0N8PTeec7340Gtv1wC7IBQGJOQnukshPgzaabgNKxmUiChGxi+RJNo/Tsdiw9ZfddcBgBjq79ZIg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.29.0", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -1310,6 +1319,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.7.tgz", "integrity": "sha512-4bnskeRNNOZMn3buVw47Zz9Py4B8AZgYHe5xBEMOY5/yrldb7OFje5gWCWls23P18FKwhl+Xx1hgnOEPSs29gw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1335,6 +1345,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.7.tgz", "integrity": "sha512-YD/h07cdEeAUs41ysTk6820T0lG/XiQmFiq02d3IsiHYI5Vaj2pg9Ti1wWZYEBM//hVAPTzV0dwdV7Q1Gxju1w==", "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "tslib": "^2.3.0" @@ -1365,6 +1376,7 @@ "integrity": "sha512-EcZHeEuEy9Fr4cWOwdJudPVbjfqso82bAXmnAE0f+Rt5LB3Le8O5RDGgV52EuLo8gzTiWzhVUI7Zg1tYkBqnSQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.29.0", "@types/babel__core": "7.20.5", @@ -1406,6 +1418,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.7.tgz", "integrity": "sha512-nklVhstRZL4wpYg9Cyae/Eyfa7LMpgb0TyD/F//qCuohhM8nM7F+O0ekykGD6H+I34jsvqx6yLS7MicndWVz7Q==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1428,6 +1441,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.7.tgz", "integrity": "sha512-P7s9ABgJqRyUS5HNRJFRr15xaXbWfSH9KjSxkBYYJbXIA986uGb6qjnHepu+2xMyZDlt2nd9Ms+t4M60/GM+FQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1471,6 +1485,7 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.7.tgz", "integrity": "sha512-Ina6XgtpvXT1OsLAomURHJGQDOkIVGrguWAOZ7+gOjsJEjUfpxTktFter+/K59KMC2yv6yneLvYSn3AswTYx7A==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1489,6 +1504,7 @@ "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-21.2.7.tgz", "integrity": "sha512-vTZvoV/Tp/cetrsvlsLBkisTUr5RISLmJPo+2outfhfOM6nJNZ1Flc66I46HpAYCAYgrcuyDxCtivjF4SP+yUQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1509,7 +1525,6 @@ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" @@ -1524,7 +1539,6 @@ "integrity": "sha512-OISPR9c2uPo23rUdvfEQiLPjoMLOpEeLNnP5iGkxr6tDDxJd3NjD+6fxY0mdaMbIPUjFGL4HFOJqLvow5q4aqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", @@ -1541,7 +1555,6 @@ "integrity": "sha512-erMO6FgtM02dC24NGm0xufMzWz5OF0wXKR7BpvGD973bq/GbmR8/DbxNZbj0YevQ5hlToJaWSVK/G9/NDgGEVw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", @@ -1557,8 +1570,7 @@ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@babel/code-frame": { "version": "7.29.0", @@ -3518,8 +3530,7 @@ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@bramus/specificity": { "version": "2.4.2", @@ -3527,7 +3538,6 @@ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "css-tree": "^3.0.0" }, @@ -3547,6 +3557,7 @@ "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-7.2.1.tgz", "integrity": "sha512-ldRG4POJLHf6oDrbDA7AsbTKliBmV4eySlwdUAumiRDtfvtbRSdXGE4Md2uPDova1r/ck7ExEe1+pHEQAZElqw==", "license": "MIT", + "peer": true, "dependencies": { "redis-info": "^3.1.0" }, @@ -3597,6 +3608,7 @@ "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-7.2.1.tgz", "integrity": "sha512-O4ykrXrl2UJNHnhJrCvJxrw1ar+DlUBgyZUeZ8Ci+Ne5Wbq6rBv1gfpQH54/eu3IFbLso0S/kjc6WUGb2HPqZw==", "license": "MIT", + "peer": true, "dependencies": { "@bull-board/api": "7.2.1" } @@ -3606,7 +3618,6 @@ "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", "license": "MIT", - "peer": true, "dependencies": { "hashery": "^1.5.1", "keyv": "^5.6.0" @@ -3618,7 +3629,6 @@ "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" @@ -3630,7 +3640,6 @@ "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@chevrotain/types": "12.0.0" } @@ -3640,24 +3649,21 @@ "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", "license": "Apache-2.0", - "optional": true, - "peer": true + "optional": true }, "node_modules/@chevrotain/types": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", "license": "Apache-2.0", - "optional": true, - "peer": true + "optional": true }, "node_modules/@chevrotain/utils": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", "license": "Apache-2.0", - "optional": true, - "peer": true + "optional": true }, "node_modules/@codewithdan/observable-store": { "version": "2.2.15", @@ -3715,7 +3721,6 @@ } ], "license": "MIT-0", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -3736,7 +3741,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -3761,7 +3765,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" @@ -3814,7 +3817,6 @@ } ], "license": "MIT-0", - "peer": true, "peerDependencies": { "css-tree": "^3.2.1" }, @@ -3906,7 +3908,8 @@ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@electric-sql/pglite-socket": { "version": "0.1.1", @@ -3937,6 +3940,7 @@ "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -3948,6 +3952,7 @@ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -4536,7 +4541,6 @@ "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, @@ -4641,8 +4645,7 @@ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@iconify/utils": { "version": "3.1.0", @@ -4650,7 +4653,6 @@ "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", @@ -4889,6 +4891,7 @@ "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", @@ -7184,8 +7187,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@kurkle/color": { "version": "0.3.4", @@ -7354,7 +7356,6 @@ "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "langium": "^4.0.0" } @@ -7487,6 +7488,7 @@ "integrity": "sha512-P91tzwyKSCQ6AwirqvAvTqWqmTY79ndpH0uenejFw+bbLpWrjuY0q+iZUXCV/7CSNmqwH2bkA/ssuyZljmcMVQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/bridge-react-webpack-plugin": "2.5.0", "@module-federation/cli": "2.5.0", @@ -7608,6 +7610,7 @@ "integrity": "sha512-oKoLm7dqb5EvkiNIfsEdLmmBX7XLWHtPSx3M9kEYuXAaNAppoRWC9WtgrrZYXWErB2BG9wxMlx/8Xq3awRUCdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/enhanced": "2.5.0", "@module-federation/runtime": "2.5.0", @@ -7683,6 +7686,7 @@ "integrity": "sha512-fR3Na6V78ov3/O17Mev+1vydfmqlYWP4ZNxD/bBkmqKhCO7jMdthNTT02yDljlCyhYl6+X90UJlFhwFle6rIsw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime": "2.5.0", "@module-federation/webpack-bundler-runtime": "2.5.0" @@ -8151,6 +8155,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.4.tgz", "integrity": "sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "2.8.1" }, @@ -8177,6 +8182,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.21.tgz", "integrity": "sha512-YV1HYDGsm2rnR0vrLKidtrG6jYX5yqiIjeur1j8++dKGqhhsJ6cjMs0RfQRSTUH7IjgDemA59/znQ8nRrE0D9g==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -8236,6 +8242,7 @@ "integrity": "sha512-fqo0BHgny3MOuAL8GSfG3ZUKFVVBaBQD/0iyibnwTONT5vPexjQxJzu+945iloVvBDmrnAaRWxC1gqCDEs/AXQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -8312,6 +8319,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.21.tgz", "integrity": "sha512-lA3ViycOnz4Df3EstIKpuAVFhqxQixTnjAVk0M+LRyNBlGM6VSCaNJaAIrb9Pcry39T4hTHpNVbRqGLSvhL8gA==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -8411,24 +8419,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@nestjs/schematics/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nestjs/schematics/node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -8523,7 +8513,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -10095,6 +10084,7 @@ "integrity": "sha512-FolcIAH5FW4J2FET+qwjd1kNeFbCkd0VLuIHO0thyolEjaPSxw5qxG67DA7BZGm6PVcoiSgPLks1DL6eZ8c+fA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.21.6", "@rspack/binding": "1.6.8", @@ -10800,6 +10790,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -12270,7 +12261,6 @@ "integrity": "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==", "dev": true, "license": "MIT", - "peer": true, "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.11", "@rspack/binding-darwin-x64": "1.7.11", @@ -12444,7 +12434,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", @@ -12463,8 +12452,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rspack/binding/node_modules/@rspack/binding-darwin-x64": { "version": "1.7.11", @@ -12478,8 +12466,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@rspack/binding/node_modules/@rspack/binding-linux-arm64-gnu": { "version": "1.7.11", @@ -12493,8 +12480,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rspack/binding/node_modules/@rspack/binding-linux-arm64-musl": { "version": "1.7.11", @@ -12508,8 +12494,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rspack/binding/node_modules/@rspack/binding-linux-x64-gnu": { "version": "1.7.11", @@ -12523,8 +12508,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rspack/binding/node_modules/@rspack/binding-linux-x64-musl": { "version": "1.7.11", @@ -12538,8 +12522,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rspack/binding/node_modules/@rspack/binding-wasm32-wasi": { "version": "1.7.11", @@ -12551,7 +12534,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" } @@ -12568,8 +12550,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rspack/binding/node_modules/@rspack/binding-win32-ia32-msvc": { "version": "1.7.11", @@ -12583,8 +12564,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rspack/binding/node_modules/@rspack/binding-win32-x64-msvc": { "version": "1.7.11", @@ -12598,8 +12578,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@rspack/core": { "version": "1.7.11", @@ -12630,8 +12609,7 @@ "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@rspack/core/node_modules/@module-federation/runtime": { "version": "0.22.0", @@ -12639,7 +12617,6 @@ "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/runtime-core": "0.22.0", @@ -12652,7 +12629,6 @@ "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/sdk": "0.22.0" @@ -12664,7 +12640,6 @@ "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/webpack-bundler-runtime": "0.22.0" @@ -12675,8 +12650,7 @@ "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@rspack/core/node_modules/@module-federation/webpack-bundler-runtime": { "version": "0.22.0", @@ -12684,7 +12658,6 @@ "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/sdk": "0.22.0" @@ -12739,6 +12712,7 @@ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -13204,6 +13178,7 @@ "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.6.tgz", "integrity": "sha512-KpLD8R2S762jbLdNEepE+b7KjhVOKPFHHdgNqhPv0NiGLdsvXSOx1e63JvFacoCZdmP7n3/gwmyT/utcVvnsag==", "license": "MIT", + "peer": true, "dependencies": { "@angular-devkit/core": "21.2.6", "@angular-devkit/schematics": "21.2.6", @@ -13670,7 +13645,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -13691,7 +13665,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -13705,7 +13678,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -13716,7 +13688,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -13731,8 +13702,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", @@ -13971,8 +13941,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -14095,7 +14064,6 @@ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", @@ -14134,8 +14102,7 @@ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-axis": { "version": "3.0.6", @@ -14143,7 +14110,6 @@ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-selection": "*" } @@ -14154,7 +14120,6 @@ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-selection": "*" } @@ -14164,16 +14129,14 @@ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "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", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-contour": { "version": "3.0.6", @@ -14181,7 +14144,6 @@ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" @@ -14192,16 +14154,14 @@ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "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", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-drag": { "version": "3.0.7", @@ -14209,7 +14169,6 @@ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-selection": "*" } @@ -14219,16 +14178,14 @@ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "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", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-fetch": { "version": "3.0.7", @@ -14236,7 +14193,6 @@ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-dsv": "*" } @@ -14246,16 +14202,14 @@ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "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", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-geo": { "version": "3.1.0", @@ -14263,7 +14217,6 @@ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/geojson": "*" } @@ -14273,8 +14226,7 @@ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", @@ -14282,7 +14234,6 @@ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-color": "*" } @@ -14292,32 +14243,28 @@ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-polygon": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "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", - "optional": true, - "peer": true + "optional": true }, "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", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-scale": { "version": "4.0.9", @@ -14325,7 +14272,6 @@ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-time": "*" } @@ -14335,16 +14281,14 @@ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-selection": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-shape": { "version": "3.1.8", @@ -14352,7 +14296,6 @@ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-path": "*" } @@ -14362,24 +14305,21 @@ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-time-format": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "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", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/d3-transition": { "version": "3.0.9", @@ -14387,7 +14327,6 @@ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-selection": "*" } @@ -14398,7 +14337,6 @@ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" @@ -14462,6 +14400,7 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -14517,8 +14456,7 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/google-spreadsheet": { "version": "3.1.5", @@ -14696,6 +14634,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -14917,8 +14856,7 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/validator": { "version": "13.15.10", @@ -15062,6 +15000,7 @@ "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", @@ -15334,6 +15273,7 @@ "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -15417,6 +15357,7 @@ "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.1", @@ -15930,7 +15871,6 @@ "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", "license": "MIT", "optional": true, - "peer": true, "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" @@ -16230,6 +16170,7 @@ "integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -16287,6 +16228,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -16403,6 +16345,7 @@ "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.174.tgz", "integrity": "sha512-bTrfLUWHWtkjzWyCY4bmyuk4Qvmj4S4NSNsXyNSVVqkmftQNtxRj7dzUoMeQDBBwlJO6fC7m2Q/lNOPqQQfAGA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@ai-sdk/gateway": "3.0.109", "@ai-sdk/provider": "3.0.10", @@ -16421,6 +16364,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -17228,7 +17172,6 @@ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "require-from-string": "^2.0.2" } @@ -17385,6 +17328,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -17464,6 +17408,7 @@ "resolved": "https://registry.npmjs.org/bull/-/bull-4.16.5.tgz", "integrity": "sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==", "license": "MIT", + "peer": true, "dependencies": { "cron-parser": "^4.9.0", "get-port": "^5.1.1", @@ -17895,6 +17840,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -18052,31 +17998,12 @@ "node": ">=20.18.1" } }, - "node_modules/chevrotain": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", - "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "@chevrotain/cst-dts-gen": "12.0.0", - "@chevrotain/gast": "12.0.0", - "@chevrotain/regexp-to-ast": "12.0.0", - "@chevrotain/types": "12.0.0", - "@chevrotain/utils": "12.0.0" - }, - "engines": { - "node": ">=22.0.0" - } - }, "node_modules/chevrotain-allstar": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz", "integrity": "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "lodash-es": "^4.17.21" }, @@ -18089,6 +18016,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^5.0.0" }, @@ -18146,13 +18074,15 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/class-validator": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -18278,19 +18208,6 @@ "node": ">= 12" } }, - "node_modules/clipboard": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", - "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "good-listener": "^1.2.2", - "select": "^1.1.2", - "tiny-emitter": "^2.0.0" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -18641,8 +18558,7 @@ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/confusing-browser-globals": { "version": "1.0.11", @@ -18817,7 +18733,6 @@ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "layout-base": "^1.0.0" } @@ -19123,6 +19038,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -19786,6 +19702,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -19809,6 +19726,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -19825,19 +19743,7 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, - "license": "MIT", - "peer": true - }, - "node_modules/cytoscape": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", - "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10" - } + "license": "MIT" }, "node_modules/cytoscape-cose-bilkent": { "version": "4.1.0", @@ -19845,7 +19751,6 @@ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "cose-base": "^1.0.0" }, @@ -19859,7 +19764,6 @@ "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "cose-base": "^2.2.0" }, @@ -19873,7 +19777,6 @@ "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "layout-base": "^2.0.0" } @@ -19883,8 +19786,7 @@ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/d3": { "version": "7.9.0", @@ -19892,7 +19794,6 @@ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-array": "3", "d3-axis": "3", @@ -19935,7 +19836,6 @@ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "internmap": "1 - 2" }, @@ -19949,7 +19849,6 @@ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -19960,7 +19859,6 @@ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", @@ -19978,7 +19876,6 @@ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-path": "1 - 3" }, @@ -19992,7 +19889,6 @@ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -20003,7 +19899,6 @@ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-array": "^3.2.0" }, @@ -20017,7 +19912,6 @@ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "delaunator": "5" }, @@ -20031,7 +19925,6 @@ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -20042,7 +19935,6 @@ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" @@ -20057,7 +19949,6 @@ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "commander": "7", "iconv-lite": "0.6", @@ -20084,7 +19975,6 @@ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10" } @@ -20095,7 +19985,6 @@ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -20109,7 +19998,6 @@ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -20120,7 +20008,6 @@ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-dsv": "1 - 3" }, @@ -20134,7 +20021,6 @@ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", @@ -20150,7 +20036,6 @@ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -20161,7 +20046,6 @@ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-array": "2.5.0 - 3" }, @@ -20175,7 +20059,6 @@ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -20186,7 +20069,6 @@ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-color": "1 - 3" }, @@ -20200,7 +20082,6 @@ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -20211,7 +20092,6 @@ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -20222,7 +20102,6 @@ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -20233,7 +20112,6 @@ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -20244,7 +20122,6 @@ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", "license": "BSD-3-Clause", "optional": true, - "peer": true, "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" @@ -20256,7 +20133,6 @@ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", "license": "BSD-3-Clause", "optional": true, - "peer": true, "dependencies": { "internmap": "^1.0.0" } @@ -20266,8 +20142,7 @@ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", "license": "BSD-3-Clause", - "optional": true, - "peer": true + "optional": true }, "node_modules/d3-sankey/node_modules/d3-shape": { "version": "1.3.7", @@ -20275,7 +20150,6 @@ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", "license": "BSD-3-Clause", "optional": true, - "peer": true, "dependencies": { "d3-path": "1" } @@ -20285,8 +20159,7 @@ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", "license": "ISC", - "optional": true, - "peer": true + "optional": true }, "node_modules/d3-scale": { "version": "4.0.2", @@ -20294,7 +20167,6 @@ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", @@ -20312,7 +20184,6 @@ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" @@ -20321,24 +20192,12 @@ "node": ">=12" } }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "optional": true, - "peer": true, - "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", "optional": true, - "peer": true, "dependencies": { "d3-path": "^3.1.0" }, @@ -20352,7 +20211,6 @@ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-array": "2 - 3" }, @@ -20366,7 +20224,6 @@ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-time": "1 - 3" }, @@ -20380,7 +20237,6 @@ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -20391,7 +20247,6 @@ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", @@ -20412,7 +20267,6 @@ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", @@ -20430,7 +20284,6 @@ "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" @@ -20442,7 +20295,6 @@ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" @@ -20457,7 +20309,6 @@ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20" } @@ -20521,6 +20372,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -20531,8 +20383,7 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/debug": { "version": "4.4.3", @@ -20721,7 +20572,6 @@ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "license": "ISC", "optional": true, - "peer": true, "dependencies": { "robust-predicates": "^3.0.2" } @@ -20741,8 +20591,7 @@ "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/denque": { "version": "2.1.0", @@ -20768,7 +20617,6 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -20876,8 +20724,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-converter": { "version": "0.2.0", @@ -20936,7 +20783,6 @@ "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", "optional": true, - "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -21094,14 +20940,6 @@ "dev": true, "license": "MIT" }, - "node_modules/emoji-toolkit": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-10.0.0.tgz", - "integrity": "sha512-GkIAvgutEVbkqcT2HjBzV002SWvpdNaT3aP9q/YjQ6hlgDq8HhE9GcqxWkyYkRRQnLADGpwDoj1heTw9KzO9wQ==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -21574,6 +21412,7 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -21635,6 +21474,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -22138,6 +21978,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -22861,6 +22702,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -23543,7 +23385,6 @@ "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "delegate": "^3.1.2" } @@ -23677,8 +23518,7 @@ "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/handle-thing": { "version": "2.0.1", @@ -23779,7 +23619,6 @@ "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", "license": "MIT", - "peer": true, "dependencies": { "hookified": "^1.15.0" }, @@ -23836,6 +23675,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -24024,6 +23864,7 @@ "integrity": "sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", @@ -24524,7 +24365,6 @@ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -25391,6 +25231,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -26581,6 +26422,7 @@ "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/environment-jsdom-abstract": "30.2.0", @@ -28535,7 +28377,6 @@ "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", @@ -28577,7 +28418,6 @@ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@exodus/bytes": "^1.6.0" }, @@ -28591,7 +28431,6 @@ "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "engines": { "node": "20 || >=22" } @@ -28602,7 +28441,6 @@ "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20.18.1" } @@ -28613,7 +28451,6 @@ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20" } @@ -28851,7 +28688,6 @@ ], "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^8.3.0" }, @@ -28865,7 +28701,6 @@ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 12" } @@ -28884,8 +28719,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", - "optional": true, - "peer": true + "optional": true }, "node_modules/kill-port": { "version": "1.6.1", @@ -28917,7 +28751,6 @@ "integrity": "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", @@ -28947,8 +28780,7 @@ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/less": { "version": "4.4.2", @@ -28956,6 +28788,7 @@ "integrity": "sha512-j1n1IuTX1VQjIy3tT7cyGbX7nvQOsFLoIqobZv4ttI5axP923gA44zUj6miiA6R5Aoms4sEGVIIcucXUbRI14g==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -29124,6 +28957,7 @@ "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", @@ -29288,6 +29122,7 @@ "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 12.13.0" } @@ -29638,7 +29473,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -29724,6 +29558,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.2.tgz", "integrity": "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -29804,7 +29639,6 @@ "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", @@ -29835,7 +29669,6 @@ "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", "license": "MIT", "optional": true, - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -29853,7 +29686,6 @@ ], "license": "MIT", "optional": true, - "peer": true, "bin": { "uuid": "dist/esm/bin/uuid" } @@ -30141,7 +29973,6 @@ "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", @@ -30888,6 +30719,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@emnapi/core": "1.4.5", "@emnapi/runtime": "1.4.5", @@ -31895,8 +31727,7 @@ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/pacote": { "version": "21.3.1", @@ -32190,6 +32021,7 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", + "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -32285,8 +32117,7 @@ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/path-exists": { "version": "4.0.0", @@ -32402,6 +32233,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -32629,7 +32461,6 @@ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", @@ -32682,8 +32513,7 @@ "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/points-on-path": { "version": "0.2.1", @@ -32691,7 +32521,6 @@ "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" @@ -32752,6 +32581,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -33063,6 +32893,7 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -33132,6 +32963,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", @@ -33159,17 +32991,6 @@ } } }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -33429,6 +33250,7 @@ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -33442,6 +33264,7 @@ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -33592,7 +33415,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -34163,8 +33987,7 @@ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense", - "optional": true, - "peer": true + "optional": true }, "node_modules/rolldown": { "version": "1.0.0-rc.4", @@ -34202,6 +34025,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -34351,7 +34175,6 @@ "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", @@ -34424,14 +34247,14 @@ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause", - "optional": true, - "peer": true + "optional": true }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -35136,8 +34959,7 @@ "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/select-hose": { "version": "2.0.0", @@ -36116,6 +35938,7 @@ "integrity": "sha512-oK0t0jEogiKKfv5Z1ao4Of99+xWw1TMUGuGRYDQS4kp2yyBsJQEgu7NI7OLYsCDI6gzt5p3RPtl1lqdeVLUi8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.0", @@ -36509,8 +36332,7 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/supports-color": { "version": "7.2.0", @@ -36873,6 +36695,7 @@ "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "devOptional": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -37024,8 +36847,7 @@ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/tiny-invariant": { "version": "1.3.3", @@ -37040,7 +36862,6 @@ "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -37087,7 +36908,6 @@ "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tldts-core": "^7.0.28" }, @@ -37100,8 +36920,7 @@ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz", "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tmp": { "version": "0.2.6", @@ -37166,7 +36985,6 @@ "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "tldts": "^7.0.5" }, @@ -37216,7 +37034,6 @@ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "punycode": "^2.3.1" }, @@ -37807,6 +37624,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -37926,7 +37744,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsyringe": { "version": "4.10.0", @@ -38113,6 +37932,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -38126,8 +37946,7 @@ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/uid": { "version": "2.0.2", @@ -38594,7 +38413,6 @@ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=14.0.0" } @@ -38605,7 +38423,6 @@ "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, @@ -38619,7 +38436,6 @@ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" @@ -38630,24 +38446,21 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/vscode-languageserver-types": { "version": "3.17.5", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", @@ -38718,7 +38531,6 @@ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=20" } @@ -38729,6 +38541,7 @@ "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -38838,6 +38651,7 @@ "integrity": "sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -38896,6 +38710,7 @@ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -39359,6 +39174,7 @@ "integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-html-community": "0.0.8", "html-entities": "^2.1.0", @@ -39555,7 +39371,6 @@ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", @@ -39815,6 +39630,7 @@ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -39957,6 +39773,7 @@ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "devOptional": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -40159,6 +39976,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -40176,7 +39994,8 @@ "version": "0.16.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.1.tgz", "integrity": "sha512-dpvY17vxYIW3+bNrP0ClUlaiY0CiIRK3tnoLaGoQsQcY9/I/NpzIWQ7tQNhbV7LacQMpCII6wVzuL3tuWOyfuA==", - "license": "MIT" + "license": "MIT", + "peer": true } } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6b0aa770f..aedf5c273 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,6 +29,7 @@ model Account { activities Order[] balance Float @default(0) balances AccountBalance[] + budgets Budget[] comment String? createdAt DateTime @default(now()) currency String? @@ -117,20 +118,26 @@ model AuthDevice { } model Budget { - amount Float - category ExpenseCategory @relation(fields: [categoryId], onDelete: Cascade, references: [id]) - categoryId String - createdAt DateTime @default(now()) - currency String - id String @id @default(uuid()) - month DateTime - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], onDelete: Cascade, references: [id]) - userId String + account Account? @relation(fields: [accountId, accountUserId], references: [id, userId]) + accountId String? + accountUserId String? + amount Float + category ExpenseCategory @relation(fields: [categoryId], onDelete: Cascade, references: [id]) + categoryId String + createdAt DateTime @default(now()) + currency String + id String @id @default(uuid()) + month DateTime + name String @default("") + type BudgetType @default(EXPENSE) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], onDelete: Cascade, references: [id]) + userId String - @@unique([categoryId, month, userId]) + @@index([accountId]) @@index([categoryId]) @@index([month]) + @@index([type]) @@index([userId]) } @@ -383,6 +390,14 @@ enum AssetSubClass { STOCK } +enum BudgetType { + CASH_SAVINGS + EXPENSE + INVESTMENT_SAVINGS + LIABILITY_AUTOMATIC + YEARLY_EXPENSE_AUTOMATIC +} + enum DataGatheringFrequency { DAILY HOURLY