diff --git a/.env.development b/.env.development index ff9ff89dd..e3dc2d594 100644 --- a/.env.development +++ b/.env.development @@ -223,6 +223,13 @@ CLOCK_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/clock TODO_BACKEND_PORT=3018 TODO_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/todo +# ============================================ +# MOODLIT PROJECT +# ============================================ + +MOODLIT_BACKEND_PORT=3012 +MOODLIT_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/moods + # ============================================ # MANA-GAMES PROJECT # ============================================ @@ -244,3 +251,10 @@ MANA_GAMES_AZURE_OPENAI_DEPLOYMENT=gpt-4o MANA_GAMES_GITHUB_TOKEN=your_github_token_here MANA_GAMES_GITHUB_OWNER=tillschneider MANA_GAMES_GITHUB_REPO=mana-games + +# ============================================ +# FINANCE PROJECT +# ============================================ + +FINANCE_BACKEND_PORT=3019 +FINANCE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/finance diff --git a/apps/finance/CLAUDE.md b/apps/finance/CLAUDE.md new file mode 100644 index 000000000..7c9ee7b29 --- /dev/null +++ b/apps/finance/CLAUDE.md @@ -0,0 +1,320 @@ +# Finance Project Guide + +## Overview + +**Finance** is a personal finance and budget tracking application for the ManaCore ecosystem. It supports multi-currency accounts, expense/income tracking, budgets, reports, and prepares for future bank synchronization. + +| App | Port | URL | +|-----|------|-----| +| Backend | 3019 | http://localhost:3019 | +| Web App | 5189 | http://localhost:5189 | +| Landing Page | 4324 | http://localhost:4324 | + +## Project Structure + +``` +apps/finance/ +├── apps/ +│ ├── backend/ # NestJS API server (@finance/backend) +│ ├── web/ # SvelteKit web application (@finance/web) +│ ├── mobile/ # Expo React Native app (@finance/mobile) +│ └── landing/ # Astro marketing landing page (@finance/landing) +├── packages/ +│ └── shared/ # Shared types, utils, constants (@finance/shared) +├── package.json +└── CLAUDE.md +``` + +## Commands + +### Root Level (from monorepo root) + +```bash +# All apps +pnpm finance:dev # Run all finance apps + +# Individual apps +pnpm dev:finance:backend # Start backend server (port 3019) +pnpm dev:finance:web # Start web app (port 5189) +pnpm dev:finance:mobile # Start mobile app +pnpm dev:finance:landing # Start landing page (port 4324) +pnpm dev:finance:app # Start web + backend together + +# Database +pnpm finance:db:push # Push schema to database +pnpm finance:db:studio # Open Drizzle Studio +pnpm finance:db:seed # Seed initial data (default categories) +``` + +### Backend (apps/finance/apps/backend) + +```bash +pnpm dev # Start with hot reload +pnpm build # Build for production +pnpm start:prod # Start production server +pnpm db:push # Push schema to database +pnpm db:studio # Open Drizzle Studio +pnpm db:seed # Seed initial data +``` + +### Web App (apps/finance/apps/web) + +```bash +pnpm dev # Start dev server (port 5189) +pnpm build # Build for production +pnpm preview # Preview production build +``` + +## Technology Stack + +| Layer | Technology | +|-------|------------| +| **Backend** | NestJS 10, Drizzle ORM, PostgreSQL | +| **Web** | SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS 4 | +| **Mobile** | Expo, React Native, NativeWind | +| **Landing** | Astro 5.x, Tailwind CSS | +| **Auth** | Mana Core Auth (JWT) | +| **Charts** | Chart.js with svelte-chartjs | +| **i18n** | svelte-i18n (DE, EN) | +| **Dates** | date-fns | + +## Core Features + +1. **Accounts** - Multiple accounts (checking, savings, credit card, cash, investment) +2. **Categories** - Income/expense categories with colors and icons +3. **Transactions** - Full CRUD with filtering, search, recurring support +4. **Budgets** - Monthly budget limits per category with alerts +5. **Transfers** - Move money between accounts +6. **Reports** - Dashboard, monthly summaries, trends, category breakdown +7. **Multi-Currency** - Support for multiple currencies with exchange rates +8. **Bank Sync (Prepared)** - Architecture ready for Plaid/GoCardless integration + +## Views + +| View | Route | Description | +|------|-------|-------------| +| **Dashboard** | `/` | Overview with totals, budget progress, recent transactions | +| **Transactions** | `/transactions` | All transactions with filters | +| **Accounts** | `/accounts` | Account list and management | +| **Account Detail** | `/accounts/[id]` | Account transactions and details | +| **Categories** | `/categories` | Category management | +| **Budgets** | `/budgets` | Budget setup and tracking | +| **Reports** | `/reports` | Report overview | +| **Monthly Report** | `/reports/monthly` | Monthly income/expense breakdown | +| **Trends** | `/reports/trends` | Spending trends over time | +| **Settings** | `/settings` | User preferences, currency, locale | + +## API Endpoints + +### Accounts + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/accounts` | GET | List user's accounts | +| `/api/v1/accounts` | POST | Create account | +| `/api/v1/accounts/:id` | GET | Get account details | +| `/api/v1/accounts/:id` | PUT | Update account | +| `/api/v1/accounts/:id` | DELETE | Delete account | +| `/api/v1/accounts/:id/archive` | POST | Archive/unarchive | +| `/api/v1/accounts/totals` | GET | Get totals by currency | +| `/api/v1/accounts/reorder` | PUT | Reorder accounts | + +### Categories + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/categories` | GET | List categories (filter: type) | +| `/api/v1/categories` | POST | Create category | +| `/api/v1/categories/:id` | PUT | Update category | +| `/api/v1/categories/:id` | DELETE | Delete category | +| `/api/v1/categories/seed` | POST | Seed default categories | + +### Transactions + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/transactions` | GET | Query transactions (filters) | +| `/api/v1/transactions` | POST | Create transaction | +| `/api/v1/transactions/:id` | GET | Get transaction details | +| `/api/v1/transactions/:id` | PUT | Update transaction | +| `/api/v1/transactions/:id` | DELETE | Delete transaction | +| `/api/v1/transactions/recent` | GET | Recent transactions | + +**Query Parameters:** +- `accountId` - Filter by account +- `categoryId` - Filter by category +- `type` - income/expense +- `startDate`, `endDate` - Date range +- `minAmount`, `maxAmount` - Amount range +- `search` - Search description/payee +- `isPending` - Pending only +- `isRecurring` - Recurring only +- `limit`, `offset` - Pagination + +### Budgets + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/budgets` | GET | List budgets | +| `/api/v1/budgets` | POST | Create/update budget | +| `/api/v1/budgets/:id` | PUT | Update budget | +| `/api/v1/budgets/:id` | DELETE | Delete budget | +| `/api/v1/budgets/month/:year/:month` | GET | Budgets with spending | +| `/api/v1/budgets/copy` | POST | Copy from previous month | + +### Transfers + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/transfers` | GET | List transfers | +| `/api/v1/transfers` | POST | Create transfer | +| `/api/v1/transfers/:id` | PUT | Update transfer | +| `/api/v1/transfers/:id` | DELETE | Delete transfer | + +### Reports + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/reports/dashboard` | GET | Dashboard aggregations | +| `/api/v1/reports/monthly-summary` | GET | Monthly income/expense | +| `/api/v1/reports/category-breakdown` | GET | Spending by category | +| `/api/v1/reports/trends` | GET | Trends over time | +| `/api/v1/reports/cash-flow` | GET | Cash flow analysis | + +## Database Schema + +### accounts +- `id` (UUID) - Primary key +- `user_id` (UUID) - Owner +- `name` (VARCHAR) - Account name +- `type` (VARCHAR) - checking/savings/credit_card/cash/investment/loan +- `balance` (DECIMAL) - Current balance +- `currency` (VARCHAR) - Currency code (EUR, USD, etc.) +- `color`, `icon` - Display options +- `is_archived` - Soft delete +- `include_in_total` - Include in dashboard totals + +### categories +- `id` (UUID) - Primary key +- `user_id` (UUID) - Owner +- `name` (VARCHAR) - Category name +- `type` (VARCHAR) - income/expense +- `parent_id` (UUID) - For subcategories +- `color`, `icon` - Display options +- `is_system` - Default categories +- `is_archived` - Soft delete + +### transactions +- `id` (UUID) - Primary key +- `user_id` (UUID) - Owner +- `account_id` (UUID) - FK to accounts +- `category_id` (UUID) - FK to categories +- `type` (VARCHAR) - income/expense +- `amount` (DECIMAL) - Transaction amount +- `currency` (VARCHAR) - Currency code +- `date` (DATE) - Transaction date +- `description` (TEXT) - Description +- `payee` (VARCHAR) - Payee/payer name +- `is_recurring` (BOOLEAN) - Recurring flag +- `recurrence_rule` (JSONB) - Recurrence pattern +- `is_pending` (BOOLEAN) - Pending flag +- `tags` (JSONB) - Tag array + +### budgets +- `id` (UUID) - Primary key +- `user_id` (UUID) - Owner +- `category_id` (UUID) - FK to categories (null = overall) +- `month`, `year` (INTEGER) - Budget period +- `amount` (DECIMAL) - Budget limit +- `alert_threshold` (DECIMAL) - Alert at percentage +- `rollover_enabled` (BOOLEAN) - Carry unused budget + +### transfers +- `id` (UUID) - Primary key +- `user_id` (UUID) - Owner +- `from_account_id`, `to_account_id` (UUID) - Account references +- `amount` (DECIMAL) - Transfer amount +- `date` (DATE) - Transfer date + +### exchange_rates +- `id` (UUID) - Primary key +- `from_currency`, `to_currency` (VARCHAR) - Currency pair +- `rate` (DECIMAL) - Exchange rate +- `date` (DATE) - Rate date + +### user_settings +- `id` (UUID) - Primary key +- `user_id` (UUID) - Owner +- `default_currency` (VARCHAR) - Default currency +- `locale` (VARCHAR) - User locale +- `date_format` (VARCHAR) - Preferred date format + +### connected_accounts (Bank Sync Preparation) +- `id` (UUID) - Primary key +- `user_id` (UUID) - Owner +- `account_id` (UUID) - FK to accounts +- `provider` (VARCHAR) - plaid/gocardless/etc. +- `external_id` (VARCHAR) - Provider account ID +- `status` (VARCHAR) - active/disconnected/error +- `last_sync_at` (TIMESTAMP) - Last sync time +- `metadata` (JSONB) - Provider-specific data + +## Environment Variables + +### Backend (.env) + +```env +NODE_ENV=development +PORT=3019 +DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/finance +MANA_CORE_AUTH_URL=http://localhost:3001 +CORS_ORIGINS=http://localhost:5173,http://localhost:5189,http://localhost:8081 +``` + +### Web (.env) + +```env +PUBLIC_BACKEND_URL=http://localhost:3019 +PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 +``` + +## Default Categories (Seeded) + +### Expense +- Lebensmittel (Groceries) - green +- Restaurant (Dining) - orange +- Transport - blue +- Wohnen (Housing) - purple +- Versicherungen (Insurance) - gray +- Gesundheit (Health) - red +- Unterhaltung (Entertainment) - pink +- Shopping - yellow +- Bildung (Education) - indigo +- Reisen (Travel) - cyan +- Abonnements (Subscriptions) - violet +- Sonstiges (Other) - gray + +### Income +- Gehalt (Salary) - green +- Nebeneinkommen (Side Income) - blue +- Investitionen (Investments) - purple +- Geschenke (Gifts) - pink +- Sonstiges (Other) - gray + +## Code Style Guidelines + +- **TypeScript**: Strict typing with interfaces +- **Web**: Svelte 5 runes mode (`$state`, `$derived`, `$effect`) +- **Styling**: Tailwind CSS with CSS variables +- **Formatting**: Prettier with project config +- **i18n**: All UI text in locale files +- **Currency**: Always use DECIMAL(15,2) for money + +## Important Notes + +1. **Authentication**: Uses Mana Core Auth (JWT in Authorization header) +2. **Database**: PostgreSQL with Drizzle ORM (port 5432) +3. **Ports**: Backend=3019, Web=5189, Landing=4324 +4. **Multi-Currency**: Exchange rates table for conversions +5. **Bank Sync**: Architecture prepared, implementation deferred +6. **Balance Updates**: Transactions automatically update account balances diff --git a/apps/finance/apps/backend/drizzle.config.ts b/apps/finance/apps/backend/drizzle.config.ts new file mode 100644 index 000000000..ff3e7b88a --- /dev/null +++ b/apps/finance/apps/backend/drizzle.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'drizzle-kit'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +export default defineConfig({ + schema: './src/db/schema/index.ts', + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/apps/finance/apps/backend/nest-cli.json b/apps/finance/apps/backend/nest-cli.json new file mode 100644 index 000000000..95538fb90 --- /dev/null +++ b/apps/finance/apps/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/finance/apps/backend/package.json b/apps/finance/apps/backend/package.json new file mode 100644 index 000000000..ed4b2daf9 --- /dev/null +++ b/apps/finance/apps/backend/package.json @@ -0,0 +1,42 @@ +{ + "name": "@finance/backend", + "version": "1.0.0", + "private": true, + "description": "Finance Backend API", + "scripts": { + "dev": "nest start --watch", + "build": "nest build", + "start": "nest start", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio", + "db:seed": "tsx src/db/seed.ts", + "db:generate": "drizzle-kit generate" + }, + "dependencies": { + "@finance/shared": "workspace:*", + "@manacore/shared-nestjs-auth": "workspace:*", + "@nestjs/common": "^10.4.9", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.9", + "@nestjs/platform-express": "^10.4.9", + "@nestjs/schedule": "^4.1.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dotenv": "^16.4.7", + "drizzle-orm": "^0.38.3", + "postgres": "^3.4.5", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.9", + "@nestjs/schematics": "^10.2.3", + "@types/express": "^5.0.1", + "@types/node": "^22.15.21", + "drizzle-kit": "^0.30.2", + "tsx": "^4.19.4", + "typescript": "^5.9.3" + } +} diff --git a/apps/finance/apps/backend/src/account/account.controller.ts b/apps/finance/apps/backend/src/account/account.controller.ts new file mode 100644 index 000000000..333cc21f3 --- /dev/null +++ b/apps/finance/apps/backend/src/account/account.controller.ts @@ -0,0 +1,74 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + UseGuards, + ParseUUIDPipe, +} from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, type CurrentUserData } from '@manacore/shared-nestjs-auth'; +import { AccountService } from './account.service'; +import { CreateAccountDto, UpdateAccountDto } from './dto'; + +@Controller('accounts') +@UseGuards(JwtAuthGuard) +export class AccountController { + constructor(private readonly accountService: AccountService) {} + + @Get() + findAll(@CurrentUser() user: CurrentUserData) { + return this.accountService.findAll(user.userId); + } + + @Get('all') + findAllIncludingArchived(@CurrentUser() user: CurrentUserData) { + return this.accountService.findAllIncludingArchived(user.userId); + } + + @Get('totals') + getTotals(@CurrentUser() user: CurrentUserData) { + return this.accountService.getTotals(user.userId); + } + + @Get(':id') + findOne(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.accountService.findOne(user.userId, id); + } + + @Post() + create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateAccountDto) { + return this.accountService.create(user.userId, dto); + } + + @Put('reorder') + reorder(@CurrentUser() user: CurrentUserData, @Body('accountIds') accountIds: string[]) { + return this.accountService.reorder(user.userId, accountIds); + } + + @Put(':id') + update( + @CurrentUser() user: CurrentUserData, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateAccountDto + ) { + return this.accountService.update(user.userId, id, dto); + } + + @Post(':id/archive') + archive(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.accountService.archive(user.userId, id, true); + } + + @Post(':id/unarchive') + unarchive(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.accountService.archive(user.userId, id, false); + } + + @Delete(':id') + delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.accountService.delete(user.userId, id); + } +} diff --git a/apps/finance/apps/backend/src/account/account.module.ts b/apps/finance/apps/backend/src/account/account.module.ts new file mode 100644 index 000000000..b9c481ea4 --- /dev/null +++ b/apps/finance/apps/backend/src/account/account.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AccountController } from './account.controller'; +import { AccountService } from './account.service'; + +@Module({ + controllers: [AccountController], + providers: [AccountService], + exports: [AccountService], +}) +export class AccountModule {} diff --git a/apps/finance/apps/backend/src/account/account.service.ts b/apps/finance/apps/backend/src/account/account.service.ts new file mode 100644 index 000000000..eca0b313b --- /dev/null +++ b/apps/finance/apps/backend/src/account/account.service.ts @@ -0,0 +1,155 @@ +import { Injectable, Inject, NotFoundException } from '@nestjs/common'; +import { eq, and, asc, sql } from 'drizzle-orm'; +import { DATABASE_CONNECTION, type Database } from '../db/connection'; +import { accounts } from '../db/schema'; +import { CreateAccountDto, UpdateAccountDto } from './dto'; + +@Injectable() +export class AccountService { + constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} + + async findAll(userId: string) { + return this.db + .select() + .from(accounts) + .where(and(eq(accounts.userId, userId), eq(accounts.isArchived, false))) + .orderBy(asc(accounts.order), asc(accounts.createdAt)); + } + + async findAllIncludingArchived(userId: string) { + return this.db + .select() + .from(accounts) + .where(eq(accounts.userId, userId)) + .orderBy(asc(accounts.order), asc(accounts.createdAt)); + } + + async findOne(userId: string, id: string) { + const [account] = await this.db + .select() + .from(accounts) + .where(and(eq(accounts.id, id), eq(accounts.userId, userId))); + + if (!account) { + throw new NotFoundException(`Account with ID ${id} not found`); + } + + return account; + } + + async create(userId: string, dto: CreateAccountDto) { + // Get the highest order value + const [maxOrder] = await this.db + .select({ maxOrder: sql`COALESCE(MAX(${accounts.order}), 0)` }) + .from(accounts) + .where(eq(accounts.userId, userId)); + + const [account] = await this.db + .insert(accounts) + .values({ + userId, + name: dto.name, + type: dto.type, + balance: dto.balance?.toString() ?? '0', + currency: dto.currency ?? 'EUR', + color: dto.color, + icon: dto.icon, + includeInTotal: dto.includeInTotal ?? true, + order: (maxOrder?.maxOrder ?? 0) + 1, + }) + .returning(); + + return account; + } + + async update(userId: string, id: string, dto: UpdateAccountDto) { + // Verify ownership + await this.findOne(userId, id); + + const [account] = await this.db + .update(accounts) + .set({ + ...(dto.name !== undefined && { name: dto.name }), + ...(dto.type !== undefined && { type: dto.type }), + ...(dto.balance !== undefined && { balance: dto.balance.toString() }), + ...(dto.currency !== undefined && { currency: dto.currency }), + ...(dto.color !== undefined && { color: dto.color }), + ...(dto.icon !== undefined && { icon: dto.icon }), + ...(dto.includeInTotal !== undefined && { includeInTotal: dto.includeInTotal }), + ...(dto.isArchived !== undefined && { isArchived: dto.isArchived }), + ...(dto.order !== undefined && { order: dto.order }), + updatedAt: new Date(), + }) + .where(and(eq(accounts.id, id), eq(accounts.userId, userId))) + .returning(); + + return account; + } + + async delete(userId: string, id: string) { + // Verify ownership + await this.findOne(userId, id); + + await this.db.delete(accounts).where(and(eq(accounts.id, id), eq(accounts.userId, userId))); + + return { success: true }; + } + + async archive(userId: string, id: string, archive = true) { + return this.update(userId, id, { isArchived: archive }); + } + + async getTotals(userId: string) { + const result = await this.db + .select({ + currency: accounts.currency, + total: sql`SUM(${accounts.balance})`, + count: sql`COUNT(*)`, + }) + .from(accounts) + .where( + and( + eq(accounts.userId, userId), + eq(accounts.isArchived, false), + eq(accounts.includeInTotal, true) + ) + ) + .groupBy(accounts.currency); + + return result.map((r) => ({ + currency: r.currency, + total: parseFloat(r.total ?? '0'), + count: Number(r.count), + })); + } + + async reorder(userId: string, accountIds: string[]) { + // Update order for each account + await Promise.all( + accountIds.map((id, index) => + this.db + .update(accounts) + .set({ order: index + 1 }) + .where(and(eq(accounts.id, id), eq(accounts.userId, userId))) + ) + ); + + return this.findAll(userId); + } + + async updateBalance(userId: string, id: string, amount: number) { + const account = await this.findOne(userId, id); + const newBalance = parseFloat(account.balance) + amount; + + const [updated] = await this.db + .update(accounts) + .set({ + balance: newBalance.toString(), + updatedAt: new Date(), + }) + .where(and(eq(accounts.id, id), eq(accounts.userId, userId))) + .returning(); + + return updated; + } +} diff --git a/apps/finance/apps/backend/src/account/dto/create-account.dto.ts b/apps/finance/apps/backend/src/account/dto/create-account.dto.ts new file mode 100644 index 000000000..513bdb293 --- /dev/null +++ b/apps/finance/apps/backend/src/account/dto/create-account.dto.ts @@ -0,0 +1,45 @@ +import { + IsString, + IsNotEmpty, + IsOptional, + IsBoolean, + IsNumber, + MaxLength, + IsIn, +} from 'class-validator'; + +const ACCOUNT_TYPES = ['checking', 'savings', 'credit_card', 'cash', 'investment', 'loan'] as const; + +export class CreateAccountDto { + @IsString() + @IsNotEmpty() + @MaxLength(100) + name: string; + + @IsString() + @IsIn(ACCOUNT_TYPES) + type: (typeof ACCOUNT_TYPES)[number]; + + @IsOptional() + @IsNumber() + balance?: number; + + @IsOptional() + @IsString() + @MaxLength(3) + currency?: string; + + @IsOptional() + @IsString() + @MaxLength(20) + color?: string; + + @IsOptional() + @IsString() + @MaxLength(50) + icon?: string; + + @IsOptional() + @IsBoolean() + includeInTotal?: boolean; +} diff --git a/apps/finance/apps/backend/src/account/dto/index.ts b/apps/finance/apps/backend/src/account/dto/index.ts new file mode 100644 index 000000000..51a808245 --- /dev/null +++ b/apps/finance/apps/backend/src/account/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-account.dto'; +export * from './update-account.dto'; diff --git a/apps/finance/apps/backend/src/account/dto/update-account.dto.ts b/apps/finance/apps/backend/src/account/dto/update-account.dto.ts new file mode 100644 index 000000000..32e32e7c6 --- /dev/null +++ b/apps/finance/apps/backend/src/account/dto/update-account.dto.ts @@ -0,0 +1,46 @@ +import { IsString, IsOptional, IsBoolean, IsNumber, MaxLength, IsIn } from 'class-validator'; + +const ACCOUNT_TYPES = ['checking', 'savings', 'credit_card', 'cash', 'investment', 'loan'] as const; + +export class UpdateAccountDto { + @IsOptional() + @IsString() + @MaxLength(100) + name?: string; + + @IsOptional() + @IsString() + @IsIn(ACCOUNT_TYPES) + type?: (typeof ACCOUNT_TYPES)[number]; + + @IsOptional() + @IsNumber() + balance?: number; + + @IsOptional() + @IsString() + @MaxLength(3) + currency?: string; + + @IsOptional() + @IsString() + @MaxLength(20) + color?: string; + + @IsOptional() + @IsString() + @MaxLength(50) + icon?: string; + + @IsOptional() + @IsBoolean() + includeInTotal?: boolean; + + @IsOptional() + @IsBoolean() + isArchived?: boolean; + + @IsOptional() + @IsNumber() + order?: number; +} diff --git a/apps/finance/apps/backend/src/app.module.ts b/apps/finance/apps/backend/src/app.module.ts new file mode 100644 index 000000000..bffbac1d5 --- /dev/null +++ b/apps/finance/apps/backend/src/app.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; +import { DatabaseModule } from './db/database.module'; +import { HealthModule } from './health/health.module'; +import { AccountModule } from './account/account.module'; +import { CategoryModule } from './category/category.module'; +import { TransactionModule } from './transaction/transaction.module'; +import { BudgetModule } from './budget/budget.module'; +import { TransferModule } from './transfer/transfer.module'; +import { ReportModule } from './report/report.module'; +import { SettingsModule } from './settings/settings.module'; +import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: '.env', + }), + ScheduleModule.forRoot(), + DatabaseModule, + HealthModule, + AccountModule, + CategoryModule, + TransactionModule, + BudgetModule, + TransferModule, + ReportModule, + SettingsModule, + ExchangeRateModule, + ], +}) +export class AppModule {} diff --git a/apps/finance/apps/backend/src/budget/budget.controller.ts b/apps/finance/apps/backend/src/budget/budget.controller.ts new file mode 100644 index 000000000..7d47a860f --- /dev/null +++ b/apps/finance/apps/backend/src/budget/budget.controller.ts @@ -0,0 +1,69 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UseGuards, + ParseUUIDPipe, + ParseIntPipe, +} from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, type CurrentUserData } from '@manacore/shared-nestjs-auth'; +import { BudgetService } from './budget.service'; +import { CreateBudgetDto, UpdateBudgetDto } from './dto'; + +@Controller('budgets') +@UseGuards(JwtAuthGuard) +export class BudgetController { + constructor(private readonly budgetService: BudgetService) {} + + @Get() + findAll(@CurrentUser() user: CurrentUserData) { + return this.budgetService.findAll(user.userId); + } + + @Get('month/:year/:month') + findByMonth( + @CurrentUser() user: CurrentUserData, + @Param('year', ParseIntPipe) year: number, + @Param('month', ParseIntPipe) month: number + ) { + return this.budgetService.findByMonth(user.userId, year, month); + } + + @Get(':id') + findOne(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.budgetService.findOne(user.userId, id); + } + + @Post() + create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateBudgetDto) { + return this.budgetService.create(user.userId, dto); + } + + @Post('copy') + copyFromPreviousMonth( + @CurrentUser() user: CurrentUserData, + @Body('year') year: number, + @Body('month') month: number + ) { + return this.budgetService.copyFromPreviousMonth(user.userId, year, month); + } + + @Put(':id') + update( + @CurrentUser() user: CurrentUserData, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateBudgetDto + ) { + return this.budgetService.update(user.userId, id, dto); + } + + @Delete(':id') + delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.budgetService.delete(user.userId, id); + } +} diff --git a/apps/finance/apps/backend/src/budget/budget.module.ts b/apps/finance/apps/backend/src/budget/budget.module.ts new file mode 100644 index 000000000..f4ccdc8d3 --- /dev/null +++ b/apps/finance/apps/backend/src/budget/budget.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { BudgetController } from './budget.controller'; +import { BudgetService } from './budget.service'; + +@Module({ + controllers: [BudgetController], + providers: [BudgetService], + exports: [BudgetService], +}) +export class BudgetModule {} diff --git a/apps/finance/apps/backend/src/budget/budget.service.ts b/apps/finance/apps/backend/src/budget/budget.service.ts new file mode 100644 index 000000000..5b81f45d6 --- /dev/null +++ b/apps/finance/apps/backend/src/budget/budget.service.ts @@ -0,0 +1,220 @@ +import { Injectable, Inject, NotFoundException } from '@nestjs/common'; +import { eq, and, sql, gte, lte } from 'drizzle-orm'; +import { DATABASE_CONNECTION, type Database } from '../db/connection'; +import { budgets, transactions, categories } from '../db/schema'; +import { CreateBudgetDto, UpdateBudgetDto } from './dto'; + +@Injectable() +export class BudgetService { + constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} + + async findAll(userId: string) { + return this.db + .select({ + budget: budgets, + category: { + id: categories.id, + name: categories.name, + color: categories.color, + icon: categories.icon, + }, + }) + .from(budgets) + .leftJoin(categories, eq(budgets.categoryId, categories.id)) + .where(eq(budgets.userId, userId)); + } + + async findOne(userId: string, id: string) { + const [result] = await this.db + .select({ + budget: budgets, + category: { + id: categories.id, + name: categories.name, + color: categories.color, + icon: categories.icon, + }, + }) + .from(budgets) + .leftJoin(categories, eq(budgets.categoryId, categories.id)) + .where(and(eq(budgets.id, id), eq(budgets.userId, userId))); + + if (!result) { + throw new NotFoundException(`Budget with ID ${id} not found`); + } + + return { + ...result.budget, + category: result.category, + }; + } + + async findByMonth(userId: string, year: number, month: number) { + // Get budgets for this month + const monthBudgets = await this.db + .select({ + budget: budgets, + category: { + id: categories.id, + name: categories.name, + color: categories.color, + icon: categories.icon, + }, + }) + .from(budgets) + .leftJoin(categories, eq(budgets.categoryId, categories.id)) + .where(and(eq(budgets.userId, userId), eq(budgets.month, month), eq(budgets.year, year))); + + // Calculate spending for each budget + const startDate = `${year}-${String(month).padStart(2, '0')}-01`; + const endDate = new Date(year, month, 0).toISOString().split('T')[0]; // Last day of month + + const spending = await this.db + .select({ + categoryId: transactions.categoryId, + total: sql`SUM(${transactions.amount})`, + }) + .from(transactions) + .where( + and( + eq(transactions.userId, userId), + eq(transactions.type, 'expense'), + gte(transactions.date, startDate), + lte(transactions.date, endDate) + ) + ) + .groupBy(transactions.categoryId); + + const spendingMap = new Map(spending.map((s) => [s.categoryId, parseFloat(s.total ?? '0')])); + + // Calculate total spending for overall budget + const [totalSpending] = await this.db + .select({ + total: sql`SUM(${transactions.amount})`, + }) + .from(transactions) + .where( + and( + eq(transactions.userId, userId), + eq(transactions.type, 'expense'), + gte(transactions.date, startDate), + lte(transactions.date, endDate) + ) + ); + + return monthBudgets.map((b) => ({ + ...b.budget, + category: b.category, + spent: b.budget.categoryId + ? (spendingMap.get(b.budget.categoryId) ?? 0) + : parseFloat(totalSpending?.total ?? '0'), + remaining: + parseFloat(b.budget.amount) - + (b.budget.categoryId + ? (spendingMap.get(b.budget.categoryId) ?? 0) + : parseFloat(totalSpending?.total ?? '0')), + percentage: + (b.budget.categoryId + ? (spendingMap.get(b.budget.categoryId) ?? 0) + : parseFloat(totalSpending?.total ?? '0')) / parseFloat(b.budget.amount), + })); + } + + async create(userId: string, dto: CreateBudgetDto) { + // Check if budget already exists for this category/month + const existing = await this.db + .select() + .from(budgets) + .where( + and( + eq(budgets.userId, userId), + eq(budgets.month, dto.month), + eq(budgets.year, dto.year), + dto.categoryId + ? eq(budgets.categoryId, dto.categoryId) + : sql`${budgets.categoryId} IS NULL` + ) + ); + + if (existing.length > 0) { + // Update existing budget + return this.update(userId, existing[0].id, { + amount: dto.amount, + alertThreshold: dto.alertThreshold, + rolloverEnabled: dto.rolloverEnabled, + }); + } + + const [budget] = await this.db + .insert(budgets) + .values({ + userId, + categoryId: dto.categoryId, + month: dto.month, + year: dto.year, + amount: dto.amount.toString(), + alertThreshold: dto.alertThreshold?.toString() ?? '0.80', + rolloverEnabled: dto.rolloverEnabled ?? false, + }) + .returning(); + + return budget; + } + + async update(userId: string, id: string, dto: UpdateBudgetDto) { + await this.findOne(userId, id); + + const [budget] = await this.db + .update(budgets) + .set({ + ...(dto.categoryId !== undefined && { categoryId: dto.categoryId }), + ...(dto.amount !== undefined && { amount: dto.amount.toString() }), + ...(dto.alertThreshold !== undefined && { alertThreshold: dto.alertThreshold.toString() }), + ...(dto.rolloverEnabled !== undefined && { rolloverEnabled: dto.rolloverEnabled }), + updatedAt: new Date(), + }) + .where(and(eq(budgets.id, id), eq(budgets.userId, userId))) + .returning(); + + return budget; + } + + async delete(userId: string, id: string) { + await this.findOne(userId, id); + await this.db.delete(budgets).where(and(eq(budgets.id, id), eq(budgets.userId, userId))); + return { success: true }; + } + + async copyFromPreviousMonth(userId: string, year: number, month: number) { + // Calculate previous month + const prevMonth = month === 1 ? 12 : month - 1; + const prevYear = month === 1 ? year - 1 : year; + + // Get previous month budgets + const prevBudgets = await this.db + .select() + .from(budgets) + .where( + and(eq(budgets.userId, userId), eq(budgets.month, prevMonth), eq(budgets.year, prevYear)) + ); + + if (prevBudgets.length === 0) { + return { message: 'No budgets found in previous month', copied: 0 }; + } + + // Create budgets for current month + const newBudgets = prevBudgets.map((b) => ({ + userId, + categoryId: b.categoryId, + month, + year, + amount: b.amount, + alertThreshold: b.alertThreshold, + rolloverEnabled: b.rolloverEnabled, + })); + + await this.db.insert(budgets).values(newBudgets).onConflictDoNothing(); + + return { message: 'Budgets copied', copied: newBudgets.length }; + } +} diff --git a/apps/finance/apps/backend/src/budget/dto/create-budget.dto.ts b/apps/finance/apps/backend/src/budget/dto/create-budget.dto.ts new file mode 100644 index 000000000..c652c7212 --- /dev/null +++ b/apps/finance/apps/backend/src/budget/dto/create-budget.dto.ts @@ -0,0 +1,31 @@ +import { IsString, IsOptional, IsBoolean, IsNumber, IsUUID, Min, Max } from 'class-validator'; + +export class CreateBudgetDto { + @IsOptional() + @IsUUID() + categoryId?: string; + + @IsNumber() + @Min(1) + @Max(12) + month: number; + + @IsNumber() + @Min(2000) + @Max(2100) + year: number; + + @IsNumber() + @Min(0) + amount: number; + + @IsOptional() + @IsNumber() + @Min(0) + @Max(1) + alertThreshold?: number; + + @IsOptional() + @IsBoolean() + rolloverEnabled?: boolean; +} diff --git a/apps/finance/apps/backend/src/budget/dto/index.ts b/apps/finance/apps/backend/src/budget/dto/index.ts new file mode 100644 index 000000000..8b46f3e52 --- /dev/null +++ b/apps/finance/apps/backend/src/budget/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-budget.dto'; +export * from './update-budget.dto'; diff --git a/apps/finance/apps/backend/src/budget/dto/update-budget.dto.ts b/apps/finance/apps/backend/src/budget/dto/update-budget.dto.ts new file mode 100644 index 000000000..80116b2a5 --- /dev/null +++ b/apps/finance/apps/backend/src/budget/dto/update-budget.dto.ts @@ -0,0 +1,22 @@ +import { IsOptional, IsBoolean, IsNumber, IsUUID, Min, Max } from 'class-validator'; + +export class UpdateBudgetDto { + @IsOptional() + @IsUUID() + categoryId?: string | null; + + @IsOptional() + @IsNumber() + @Min(0) + amount?: number; + + @IsOptional() + @IsNumber() + @Min(0) + @Max(1) + alertThreshold?: number; + + @IsOptional() + @IsBoolean() + rolloverEnabled?: boolean; +} diff --git a/apps/finance/apps/backend/src/category/category.controller.ts b/apps/finance/apps/backend/src/category/category.controller.ts new file mode 100644 index 000000000..5993cd81f --- /dev/null +++ b/apps/finance/apps/backend/src/category/category.controller.ts @@ -0,0 +1,65 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UseGuards, + ParseUUIDPipe, +} from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, type CurrentUserData } from '@manacore/shared-nestjs-auth'; +import { CategoryService } from './category.service'; +import { CreateCategoryDto, UpdateCategoryDto } from './dto'; + +@Controller('categories') +@UseGuards(JwtAuthGuard) +export class CategoryController { + constructor(private readonly categoryService: CategoryService) {} + + @Get() + findAll(@CurrentUser() user: CurrentUserData, @Query('type') type?: 'income' | 'expense') { + return this.categoryService.findAll(user.userId, type); + } + + @Get('all') + findAllIncludingArchived(@CurrentUser() user: CurrentUserData) { + return this.categoryService.findAllIncludingArchived(user.userId); + } + + @Get('tree') + getTree(@CurrentUser() user: CurrentUserData) { + return this.categoryService.getTree(user.userId); + } + + @Get(':id') + findOne(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.categoryService.findOne(user.userId, id); + } + + @Post() + create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateCategoryDto) { + return this.categoryService.create(user.userId, dto); + } + + @Post('seed') + seed(@CurrentUser() user: CurrentUserData) { + return this.categoryService.seed(user.userId); + } + + @Put(':id') + update( + @CurrentUser() user: CurrentUserData, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateCategoryDto + ) { + return this.categoryService.update(user.userId, id, dto); + } + + @Delete(':id') + delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.categoryService.delete(user.userId, id); + } +} diff --git a/apps/finance/apps/backend/src/category/category.module.ts b/apps/finance/apps/backend/src/category/category.module.ts new file mode 100644 index 000000000..8a7ff5cfa --- /dev/null +++ b/apps/finance/apps/backend/src/category/category.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { CategoryController } from './category.controller'; +import { CategoryService } from './category.service'; + +@Module({ + controllers: [CategoryController], + providers: [CategoryService], + exports: [CategoryService], +}) +export class CategoryModule {} diff --git a/apps/finance/apps/backend/src/category/category.service.ts b/apps/finance/apps/backend/src/category/category.service.ts new file mode 100644 index 000000000..da578cffd --- /dev/null +++ b/apps/finance/apps/backend/src/category/category.service.ts @@ -0,0 +1,173 @@ +import { Injectable, Inject, NotFoundException } from '@nestjs/common'; +import { eq, and, asc, isNull } from 'drizzle-orm'; +import { DATABASE_CONNECTION, type Database } from '../db/connection'; +import { categories } from '../db/schema'; +import { CreateCategoryDto, UpdateCategoryDto } from './dto'; + +// Default categories to seed +const DEFAULT_CATEGORIES = { + expense: [ + { name: 'Lebensmittel', color: '#22c55e', icon: 'shopping-cart' }, + { name: 'Restaurant', color: '#f97316', icon: 'utensils' }, + { name: 'Transport', color: '#3b82f6', icon: 'car' }, + { name: 'Wohnen', color: '#a855f7', icon: 'home' }, + { name: 'Versicherungen', color: '#6b7280', icon: 'shield' }, + { name: 'Gesundheit', color: '#ef4444', icon: 'heart' }, + { name: 'Unterhaltung', color: '#ec4899', icon: 'film' }, + { name: 'Shopping', color: '#eab308', icon: 'shopping-bag' }, + { name: 'Bildung', color: '#6366f1', icon: 'book' }, + { name: 'Reisen', color: '#06b6d4', icon: 'plane' }, + { name: 'Abonnements', color: '#8b5cf6', icon: 'credit-card' }, + { name: 'Sonstiges', color: '#9ca3af', icon: 'more-horizontal' }, + ], + income: [ + { name: 'Gehalt', color: '#22c55e', icon: 'briefcase' }, + { name: 'Nebeneinkommen', color: '#3b82f6', icon: 'trending-up' }, + { name: 'Investitionen', color: '#a855f7', icon: 'bar-chart' }, + { name: 'Geschenke', color: '#ec4899', icon: 'gift' }, + { name: 'Sonstiges', color: '#9ca3af', icon: 'more-horizontal' }, + ], +}; + +@Injectable() +export class CategoryService { + constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} + + async findAll(userId: string, type?: 'income' | 'expense') { + const conditions = [eq(categories.userId, userId), eq(categories.isArchived, false)]; + + if (type) { + conditions.push(eq(categories.type, type)); + } + + return this.db + .select() + .from(categories) + .where(and(...conditions)) + .orderBy(asc(categories.name)); + } + + async findAllIncludingArchived(userId: string) { + return this.db + .select() + .from(categories) + .where(eq(categories.userId, userId)) + .orderBy(asc(categories.name)); + } + + async findOne(userId: string, id: string) { + const [category] = await this.db + .select() + .from(categories) + .where(and(eq(categories.id, id), eq(categories.userId, userId))); + + if (!category) { + throw new NotFoundException(`Category with ID ${id} not found`); + } + + return category; + } + + async create(userId: string, dto: CreateCategoryDto) { + const [category] = await this.db + .insert(categories) + .values({ + userId, + name: dto.name, + type: dto.type, + parentId: dto.parentId, + color: dto.color, + icon: dto.icon, + isSystem: false, + }) + .returning(); + + return category; + } + + async update(userId: string, id: string, dto: UpdateCategoryDto) { + // Verify ownership + await this.findOne(userId, id); + + const [category] = await this.db + .update(categories) + .set({ + ...(dto.name !== undefined && { name: dto.name }), + ...(dto.type !== undefined && { type: dto.type }), + ...(dto.parentId !== undefined && { parentId: dto.parentId }), + ...(dto.color !== undefined && { color: dto.color }), + ...(dto.icon !== undefined && { icon: dto.icon }), + ...(dto.isArchived !== undefined && { isArchived: dto.isArchived }), + updatedAt: new Date(), + }) + .where(and(eq(categories.id, id), eq(categories.userId, userId))) + .returning(); + + return category; + } + + async delete(userId: string, id: string) { + // Verify ownership + const category = await this.findOne(userId, id); + + // Don't allow deleting system categories + if (category.isSystem) { + throw new Error('Cannot delete system categories'); + } + + await this.db + .delete(categories) + .where(and(eq(categories.id, id), eq(categories.userId, userId))); + + return { success: true }; + } + + async seed(userId: string) { + // Check if user already has categories + const existing = await this.db + .select() + .from(categories) + .where(eq(categories.userId, userId)) + .limit(1); + + if (existing.length > 0) { + return { message: 'Categories already exist', seeded: false }; + } + + const categoriesToInsert = [ + ...DEFAULT_CATEGORIES.expense.map((c) => ({ + userId, + name: c.name, + type: 'expense' as const, + color: c.color, + icon: c.icon, + isSystem: true, + })), + ...DEFAULT_CATEGORIES.income.map((c) => ({ + userId, + name: c.name, + type: 'income' as const, + color: c.color, + icon: c.icon, + isSystem: true, + })), + ]; + + await this.db.insert(categories).values(categoriesToInsert); + + return { message: 'Categories seeded', seeded: true, count: categoriesToInsert.length }; + } + + async getTree(userId: string) { + const allCategories = await this.findAll(userId); + + // Build tree structure + const rootCategories = allCategories.filter((c) => !c.parentId); + const childCategories = allCategories.filter((c) => c.parentId); + + return rootCategories.map((parent) => ({ + ...parent, + children: childCategories.filter((c) => c.parentId === parent.id), + })); + } +} diff --git a/apps/finance/apps/backend/src/category/dto/create-category.dto.ts b/apps/finance/apps/backend/src/category/dto/create-category.dto.ts new file mode 100644 index 000000000..c63d4ee51 --- /dev/null +++ b/apps/finance/apps/backend/src/category/dto/create-category.dto.ts @@ -0,0 +1,36 @@ +import { + IsString, + IsNotEmpty, + IsOptional, + IsBoolean, + IsUUID, + MaxLength, + IsIn, +} from 'class-validator'; + +const CATEGORY_TYPES = ['income', 'expense'] as const; + +export class CreateCategoryDto { + @IsString() + @IsNotEmpty() + @MaxLength(100) + name: string; + + @IsString() + @IsIn(CATEGORY_TYPES) + type: (typeof CATEGORY_TYPES)[number]; + + @IsOptional() + @IsUUID() + parentId?: string; + + @IsOptional() + @IsString() + @MaxLength(20) + color?: string; + + @IsOptional() + @IsString() + @MaxLength(50) + icon?: string; +} diff --git a/apps/finance/apps/backend/src/category/dto/index.ts b/apps/finance/apps/backend/src/category/dto/index.ts new file mode 100644 index 000000000..40b39c243 --- /dev/null +++ b/apps/finance/apps/backend/src/category/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-category.dto'; +export * from './update-category.dto'; diff --git a/apps/finance/apps/backend/src/category/dto/update-category.dto.ts b/apps/finance/apps/backend/src/category/dto/update-category.dto.ts new file mode 100644 index 000000000..cb1582578 --- /dev/null +++ b/apps/finance/apps/backend/src/category/dto/update-category.dto.ts @@ -0,0 +1,33 @@ +import { IsString, IsOptional, IsBoolean, IsUUID, MaxLength, IsIn } from 'class-validator'; + +const CATEGORY_TYPES = ['income', 'expense'] as const; + +export class UpdateCategoryDto { + @IsOptional() + @IsString() + @MaxLength(100) + name?: string; + + @IsOptional() + @IsString() + @IsIn(CATEGORY_TYPES) + type?: (typeof CATEGORY_TYPES)[number]; + + @IsOptional() + @IsUUID() + parentId?: string | null; + + @IsOptional() + @IsString() + @MaxLength(20) + color?: string; + + @IsOptional() + @IsString() + @MaxLength(50) + icon?: string; + + @IsOptional() + @IsBoolean() + isArchived?: boolean; +} diff --git a/apps/finance/apps/backend/src/db/connection.ts b/apps/finance/apps/backend/src/db/connection.ts new file mode 100644 index 000000000..c569f9b6f --- /dev/null +++ b/apps/finance/apps/backend/src/db/connection.ts @@ -0,0 +1,29 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as schema from './schema'; + +let connection: ReturnType | null = null; +let db: ReturnType | null = null; + +export function getDb(databaseUrl: string) { + if (!db) { + connection = postgres(databaseUrl, { + max: 10, + idle_timeout: 20, + connect_timeout: 10, + }); + db = drizzle(connection, { schema }); + } + return db; +} + +export async function closeConnection() { + if (connection) { + await connection.end(); + connection = null; + db = null; + } +} + +export type Database = ReturnType; +export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; diff --git a/apps/finance/apps/backend/src/db/database.module.ts b/apps/finance/apps/backend/src/db/database.module.ts new file mode 100644 index 000000000..e3ea25e98 --- /dev/null +++ b/apps/finance/apps/backend/src/db/database.module.ts @@ -0,0 +1,26 @@ +import { Global, Module, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { getDb, closeConnection, DATABASE_CONNECTION, type Database } from './connection'; + +@Global() +@Module({ + providers: [ + { + provide: DATABASE_CONNECTION, + useFactory: (configService: ConfigService): Database => { + const databaseUrl = configService.get('DATABASE_URL'); + if (!databaseUrl) { + throw new Error('DATABASE_URL environment variable is not set'); + } + return getDb(databaseUrl); + }, + inject: [ConfigService], + }, + ], + exports: [DATABASE_CONNECTION], +}) +export class DatabaseModule implements OnModuleDestroy { + async onModuleDestroy() { + await closeConnection(); + } +} diff --git a/apps/finance/apps/backend/src/db/schema/accounts.schema.ts b/apps/finance/apps/backend/src/db/schema/accounts.schema.ts new file mode 100644 index 000000000..9ea026254 --- /dev/null +++ b/apps/finance/apps/backend/src/db/schema/accounts.schema.ts @@ -0,0 +1,63 @@ +import { + pgTable, + uuid, + timestamp, + varchar, + text, + boolean, + decimal, + integer, + index, +} from 'drizzle-orm/pg-core'; + +export type AccountType = + | 'checking' + | 'savings' + | 'credit_card' + | 'cash' + | 'investment' + | 'loan' + | 'other'; + +export const accounts = pgTable( + 'accounts', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull(), + + // Basic info + name: varchar('name', { length: 100 }).notNull(), + type: varchar('type', { length: 20 }).notNull().$type(), + + // Balance + balance: decimal('balance', { precision: 15, scale: 2 }).default('0').notNull(), + currency: varchar('currency', { length: 3 }).default('EUR').notNull(), + + // Display + color: varchar('color', { length: 7 }).default('#3B82F6'), + icon: varchar('icon', { length: 50 }).default('wallet'), + + // Status + isArchived: boolean('is_archived').default(false).notNull(), + includeInTotal: boolean('include_in_total').default(true).notNull(), + + // Ordering + order: integer('order').default(0).notNull(), + + // Metadata + description: text('description'), + institutionName: varchar('institution_name', { length: 100 }), + accountNumber: varchar('account_number', { length: 50 }), // Last 4 digits only + + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => ({ + userIdx: index('accounts_user_idx').on(table.userId), + typeIdx: index('accounts_type_idx').on(table.type), + orderIdx: index('accounts_order_idx').on(table.order), + }) +); + +export type Account = typeof accounts.$inferSelect; +export type NewAccount = typeof accounts.$inferInsert; diff --git a/apps/finance/apps/backend/src/db/schema/budgets.schema.ts b/apps/finance/apps/backend/src/db/schema/budgets.schema.ts new file mode 100644 index 000000000..b78efa431 --- /dev/null +++ b/apps/finance/apps/backend/src/db/schema/budgets.schema.ts @@ -0,0 +1,51 @@ +import { + pgTable, + uuid, + timestamp, + varchar, + decimal, + integer, + boolean, + index, +} from 'drizzle-orm/pg-core'; +import { categories } from './categories.schema'; + +export const budgets = pgTable( + 'budgets', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull(), + + // Category (null = overall budget) + categoryId: uuid('category_id').references(() => categories.id, { onDelete: 'cascade' }), + + // Period + month: integer('month').notNull(), // 1-12 + year: integer('year').notNull(), + + // Amount + amount: decimal('amount', { precision: 15, scale: 2 }).notNull(), + currency: varchar('currency', { length: 3 }).default('EUR').notNull(), + + // Alert settings + alertThreshold: decimal('alert_threshold', { precision: 5, scale: 2 }) + .default('0.80') + .notNull(), // 80% + alertEnabled: boolean('alert_enabled').default(true).notNull(), + + // Rollover (unused budget carries to next month) + rolloverEnabled: boolean('rollover_enabled').default(false).notNull(), + rolloverAmount: decimal('rollover_amount', { precision: 15, scale: 2 }).default('0').notNull(), + + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => ({ + userIdx: index('budgets_user_idx').on(table.userId), + categoryIdx: index('budgets_category_idx').on(table.categoryId), + periodIdx: index('budgets_period_idx').on(table.year, table.month), + }) +); + +export type Budget = typeof budgets.$inferSelect; +export type NewBudget = typeof budgets.$inferInsert; diff --git a/apps/finance/apps/backend/src/db/schema/categories.schema.ts b/apps/finance/apps/backend/src/db/schema/categories.schema.ts new file mode 100644 index 000000000..02dd66bb4 --- /dev/null +++ b/apps/finance/apps/backend/src/db/schema/categories.schema.ts @@ -0,0 +1,41 @@ +import { pgTable, uuid, timestamp, varchar, boolean, integer, index } from 'drizzle-orm/pg-core'; + +export type CategoryType = 'income' | 'expense'; + +export const categories = pgTable( + 'categories', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull(), + + // Basic info + name: varchar('name', { length: 100 }).notNull(), + type: varchar('type', { length: 10 }).notNull().$type(), + + // Hierarchy (for subcategories) + parentId: uuid('parent_id'), + + // Display + color: varchar('color', { length: 7 }).default('#6B7280'), + icon: varchar('icon', { length: 50 }).default('tag'), + + // Ordering + order: integer('order').default(0).notNull(), + + // Status + isSystem: boolean('is_system').default(false).notNull(), // For default categories + isArchived: boolean('is_archived').default(false).notNull(), + + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => ({ + userIdx: index('categories_user_idx').on(table.userId), + typeIdx: index('categories_type_idx').on(table.type), + parentIdx: index('categories_parent_idx').on(table.parentId), + orderIdx: index('categories_order_idx').on(table.order), + }) +); + +export type Category = typeof categories.$inferSelect; +export type NewCategory = typeof categories.$inferInsert; diff --git a/apps/finance/apps/backend/src/db/schema/connected-accounts.schema.ts b/apps/finance/apps/backend/src/db/schema/connected-accounts.schema.ts new file mode 100644 index 000000000..222cfbde1 --- /dev/null +++ b/apps/finance/apps/backend/src/db/schema/connected-accounts.schema.ts @@ -0,0 +1,42 @@ +import { pgTable, uuid, timestamp, varchar, jsonb, index } from 'drizzle-orm/pg-core'; +import { accounts } from './accounts.schema'; + +export type ConnectionStatus = 'active' | 'disconnected' | 'error'; + +export const connectedAccounts = pgTable( + 'connected_accounts', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull(), + + // Link to local account + accountId: uuid('account_id') + .references(() => accounts.id, { onDelete: 'cascade' }) + .notNull(), + + // Provider info + provider: varchar('provider', { length: 50 }).notNull(), // plaid, gocardless, etc. + externalId: varchar('external_id', { length: 255 }).notNull(), + + // Status + status: varchar('status', { length: 20 }).default('active').notNull().$type(), + + // Sync info + lastSyncAt: timestamp('last_sync_at', { withTimezone: true }), + + // Provider-specific metadata + metadata: jsonb('metadata').$type>(), + + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => ({ + userIdx: index('connected_accounts_user_idx').on(table.userId), + accountIdx: index('connected_accounts_account_idx').on(table.accountId), + providerIdx: index('connected_accounts_provider_idx').on(table.provider), + externalIdx: index('connected_accounts_external_idx').on(table.externalId), + }) +); + +export type ConnectedAccount = typeof connectedAccounts.$inferSelect; +export type NewConnectedAccount = typeof connectedAccounts.$inferInsert; diff --git a/apps/finance/apps/backend/src/db/schema/exchange-rates.schema.ts b/apps/finance/apps/backend/src/db/schema/exchange-rates.schema.ts new file mode 100644 index 000000000..cf1e18f16 --- /dev/null +++ b/apps/finance/apps/backend/src/db/schema/exchange-rates.schema.ts @@ -0,0 +1,28 @@ +import { pgTable, uuid, varchar, decimal, date, index } from 'drizzle-orm/pg-core'; + +export const exchangeRates = pgTable( + 'exchange_rates', + { + id: uuid('id').primaryKey().defaultRandom(), + + // Currency pair + fromCurrency: varchar('from_currency', { length: 3 }).notNull(), + toCurrency: varchar('to_currency', { length: 3 }).notNull(), + + // Rate + rate: decimal('rate', { precision: 15, scale: 6 }).notNull(), + + // Date + date: date('date').notNull(), + }, + (table) => ({ + currencyPairIdx: index('exchange_rates_currency_pair_idx').on( + table.fromCurrency, + table.toCurrency + ), + dateIdx: index('exchange_rates_date_idx').on(table.date), + }) +); + +export type ExchangeRate = typeof exchangeRates.$inferSelect; +export type NewExchangeRate = typeof exchangeRates.$inferInsert; diff --git a/apps/finance/apps/backend/src/db/schema/index.ts b/apps/finance/apps/backend/src/db/schema/index.ts new file mode 100644 index 000000000..b5216e8a9 --- /dev/null +++ b/apps/finance/apps/backend/src/db/schema/index.ts @@ -0,0 +1,8 @@ +export * from './accounts.schema'; +export * from './categories.schema'; +export * from './transactions.schema'; +export * from './budgets.schema'; +export * from './transfers.schema'; +export * from './exchange-rates.schema'; +export * from './user-settings.schema'; +export * from './connected-accounts.schema'; diff --git a/apps/finance/apps/backend/src/db/schema/transactions.schema.ts b/apps/finance/apps/backend/src/db/schema/transactions.schema.ts new file mode 100644 index 000000000..16c8f1024 --- /dev/null +++ b/apps/finance/apps/backend/src/db/schema/transactions.schema.ts @@ -0,0 +1,83 @@ +import { + pgTable, + uuid, + timestamp, + varchar, + text, + boolean, + decimal, + date, + jsonb, + index, +} from 'drizzle-orm/pg-core'; +import { accounts } from './accounts.schema'; +import { categories } from './categories.schema'; + +export type TransactionType = 'income' | 'expense'; + +export interface RecurrenceRule { + frequency: 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'yearly'; + interval: number; + endDate?: string; + dayOfMonth?: number; + dayOfWeek?: number; +} + +export const transactions = pgTable( + 'transactions', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull(), + + // Relations + accountId: uuid('account_id') + .references(() => accounts.id, { onDelete: 'cascade' }) + .notNull(), + categoryId: uuid('category_id').references(() => categories.id, { onDelete: 'set null' }), + + // Transaction details + type: varchar('type', { length: 10 }).notNull().$type(), + amount: decimal('amount', { precision: 15, scale: 2 }).notNull(), + currency: varchar('currency', { length: 3 }).default('EUR').notNull(), + + // Date + date: date('date').notNull(), + + // Description + description: text('description'), + notes: text('notes'), + + // Payee/Payer + payee: varchar('payee', { length: 200 }), + + // Recurrence + isRecurring: boolean('is_recurring').default(false).notNull(), + recurrenceRule: jsonb('recurrence_rule').$type(), + parentTransactionId: uuid('parent_transaction_id'), // For recurring instances + + // Status + isPending: boolean('is_pending').default(false).notNull(), + isReconciled: boolean('is_reconciled').default(false).notNull(), + + // Tags (stored as array) + tags: jsonb('tags').$type().default([]), + + // Attachments (receipt images, etc.) + attachments: jsonb('attachments').$type().default([]), + + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => ({ + userIdx: index('transactions_user_idx').on(table.userId), + accountIdx: index('transactions_account_idx').on(table.accountId), + categoryIdx: index('transactions_category_idx').on(table.categoryId), + dateIdx: index('transactions_date_idx').on(table.date), + typeIdx: index('transactions_type_idx').on(table.type), + recurringIdx: index('transactions_recurring_idx').on(table.isRecurring), + parentIdx: index('transactions_parent_idx').on(table.parentTransactionId), + }) +); + +export type Transaction = typeof transactions.$inferSelect; +export type NewTransaction = typeof transactions.$inferInsert; diff --git a/apps/finance/apps/backend/src/db/schema/transfers.schema.ts b/apps/finance/apps/backend/src/db/schema/transfers.schema.ts new file mode 100644 index 000000000..4f7f8c1bb --- /dev/null +++ b/apps/finance/apps/backend/src/db/schema/transfers.schema.ts @@ -0,0 +1,39 @@ +import { pgTable, uuid, timestamp, text, decimal, date, index } from 'drizzle-orm/pg-core'; +import { accounts } from './accounts.schema'; + +export const transfers = pgTable( + 'transfers', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull(), + + // Accounts + fromAccountId: uuid('from_account_id') + .references(() => accounts.id, { onDelete: 'cascade' }) + .notNull(), + toAccountId: uuid('to_account_id') + .references(() => accounts.id, { onDelete: 'cascade' }) + .notNull(), + + // Amount + amount: decimal('amount', { precision: 15, scale: 2 }).notNull(), + + // Date + date: date('date').notNull(), + + // Description + description: text('description'), + + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => ({ + userIdx: index('transfers_user_idx').on(table.userId), + fromAccountIdx: index('transfers_from_account_idx').on(table.fromAccountId), + toAccountIdx: index('transfers_to_account_idx').on(table.toAccountId), + dateIdx: index('transfers_date_idx').on(table.date), + }) +); + +export type Transfer = typeof transfers.$inferSelect; +export type NewTransfer = typeof transfers.$inferInsert; diff --git a/apps/finance/apps/backend/src/db/schema/user-settings.schema.ts b/apps/finance/apps/backend/src/db/schema/user-settings.schema.ts new file mode 100644 index 000000000..65b1bbd4a --- /dev/null +++ b/apps/finance/apps/backend/src/db/schema/user-settings.schema.ts @@ -0,0 +1,30 @@ +import { pgTable, uuid, timestamp, varchar, integer, index } from 'drizzle-orm/pg-core'; + +export const userSettings = pgTable( + 'user_settings', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id').notNull().unique(), + + // Currency + defaultCurrency: varchar('default_currency', { length: 3 }).default('EUR').notNull(), + + // Locale + locale: varchar('locale', { length: 10 }).default('de-DE').notNull(), + + // Date format + dateFormat: varchar('date_format', { length: 20 }).default('dd.MM.yyyy').notNull(), + + // Week start (0 = Sunday, 1 = Monday) + weekStartsOn: integer('week_starts_on').default(1).notNull(), + + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => ({ + userIdx: index('user_settings_user_idx').on(table.userId), + }) +); + +export type UserSettings = typeof userSettings.$inferSelect; +export type NewUserSettings = typeof userSettings.$inferInsert; diff --git a/apps/finance/apps/backend/src/exchange-rate/exchange-rate.controller.ts b/apps/finance/apps/backend/src/exchange-rate/exchange-rate.controller.ts new file mode 100644 index 000000000..6a4a5aa4f --- /dev/null +++ b/apps/finance/apps/backend/src/exchange-rate/exchange-rate.controller.ts @@ -0,0 +1,38 @@ +import { Controller, Get, Post, Body, Query, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from '@manacore/shared-nestjs-auth'; +import { ExchangeRateService } from './exchange-rate.service'; + +@Controller('exchange-rates') +@UseGuards(JwtAuthGuard) +export class ExchangeRateController { + constructor(private readonly exchangeRateService: ExchangeRateService) {} + + @Get() + getAllRates(@Query('base') baseCurrency?: string) { + return this.exchangeRateService.getAllRates(baseCurrency); + } + + @Get('rate') + getRate(@Query('from') fromCurrency: string, @Query('to') toCurrency: string) { + return this.exchangeRateService.getRate(fromCurrency, toCurrency); + } + + @Get('convert') + convert( + @Query('amount') amount: number, + @Query('from') fromCurrency: string, + @Query('to') toCurrency: string + ) { + return this.exchangeRateService.convert(amount, fromCurrency, toCurrency); + } + + @Post('seed') + seedRates() { + return this.exchangeRateService.seedRates(); + } + + @Post('fetch') + fetchRates() { + return this.exchangeRateService.fetchRates(); + } +} diff --git a/apps/finance/apps/backend/src/exchange-rate/exchange-rate.module.ts b/apps/finance/apps/backend/src/exchange-rate/exchange-rate.module.ts new file mode 100644 index 000000000..5e8ceb1a7 --- /dev/null +++ b/apps/finance/apps/backend/src/exchange-rate/exchange-rate.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ExchangeRateController } from './exchange-rate.controller'; +import { ExchangeRateService } from './exchange-rate.service'; + +@Module({ + controllers: [ExchangeRateController], + providers: [ExchangeRateService], + exports: [ExchangeRateService], +}) +export class ExchangeRateModule {} diff --git a/apps/finance/apps/backend/src/exchange-rate/exchange-rate.service.ts b/apps/finance/apps/backend/src/exchange-rate/exchange-rate.service.ts new file mode 100644 index 000000000..a0ac28ac0 --- /dev/null +++ b/apps/finance/apps/backend/src/exchange-rate/exchange-rate.service.ts @@ -0,0 +1,190 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { eq, and, desc } from 'drizzle-orm'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { DATABASE_CONNECTION, type Database } from '../db/connection'; +import { exchangeRates } from '../db/schema'; + +// Common currencies +const SUPPORTED_CURRENCIES = [ + 'EUR', + 'USD', + 'GBP', + 'CHF', + 'JPY', + 'CAD', + 'AUD', + 'CNY', + 'INR', + 'BRL', + 'MXN', + 'PLN', + 'SEK', +]; + +@Injectable() +export class ExchangeRateService { + constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} + + async getRate(fromCurrency: string, toCurrency: string): Promise { + if (fromCurrency === toCurrency) { + return 1; + } + + // Try direct rate + const [directRate] = await this.db + .select() + .from(exchangeRates) + .where( + and(eq(exchangeRates.fromCurrency, fromCurrency), eq(exchangeRates.toCurrency, toCurrency)) + ) + .orderBy(desc(exchangeRates.date)) + .limit(1); + + if (directRate) { + return parseFloat(directRate.rate); + } + + // Try inverse rate + const [inverseRate] = await this.db + .select() + .from(exchangeRates) + .where( + and(eq(exchangeRates.fromCurrency, toCurrency), eq(exchangeRates.toCurrency, fromCurrency)) + ) + .orderBy(desc(exchangeRates.date)) + .limit(1); + + if (inverseRate) { + return 1 / parseFloat(inverseRate.rate); + } + + // Try through EUR as base + const [toEur] = await this.db + .select() + .from(exchangeRates) + .where(and(eq(exchangeRates.fromCurrency, fromCurrency), eq(exchangeRates.toCurrency, 'EUR'))) + .orderBy(desc(exchangeRates.date)) + .limit(1); + + const [fromEur] = await this.db + .select() + .from(exchangeRates) + .where(and(eq(exchangeRates.fromCurrency, 'EUR'), eq(exchangeRates.toCurrency, toCurrency))) + .orderBy(desc(exchangeRates.date)) + .limit(1); + + if (toEur && fromEur) { + return parseFloat(toEur.rate) * parseFloat(fromEur.rate); + } + + // Default fallback + return 1; + } + + async convert(amount: number, fromCurrency: string, toCurrency: string): Promise { + const rate = await this.getRate(fromCurrency, toCurrency); + return amount * rate; + } + + async getAllRates(baseCurrency = 'EUR') { + const rates = await this.db + .select() + .from(exchangeRates) + .where(eq(exchangeRates.fromCurrency, baseCurrency)) + .orderBy(desc(exchangeRates.date)); + + // Get latest rate for each currency pair + const latestRates = new Map(); + rates.forEach((rate) => { + if (!latestRates.has(rate.toCurrency)) { + latestRates.set(rate.toCurrency, rate); + } + }); + + return Array.from(latestRates.values()).map((r) => ({ + fromCurrency: r.fromCurrency, + toCurrency: r.toCurrency, + rate: parseFloat(r.rate), + date: r.date, + })); + } + + async setRate(fromCurrency: string, toCurrency: string, rate: number) { + const today = new Date().toISOString().split('T')[0]; + + // Upsert rate + const [existing] = await this.db + .select() + .from(exchangeRates) + .where( + and( + eq(exchangeRates.fromCurrency, fromCurrency), + eq(exchangeRates.toCurrency, toCurrency), + eq(exchangeRates.date, today) + ) + ); + + if (existing) { + const [updated] = await this.db + .update(exchangeRates) + .set({ rate: rate.toString() }) + .where(eq(exchangeRates.id, existing.id)) + .returning(); + return updated; + } + + const [created] = await this.db + .insert(exchangeRates) + .values({ + fromCurrency, + toCurrency, + rate: rate.toString(), + date: today, + }) + .returning(); + + return created; + } + + // Fetch rates from ECB (free, no API key required) + @Cron(CronExpression.EVERY_DAY_AT_6AM) + async fetchRates() { + try { + const response = await fetch('https://api.frankfurter.app/latest?from=EUR'); + const data = await response.json(); + + if (data.rates) { + const today = data.date; + const rates = Object.entries(data.rates) as [string, number][]; + + for (const [currency, rate] of rates) { + await this.db + .insert(exchangeRates) + .values({ + fromCurrency: 'EUR', + toCurrency: currency, + rate: rate.toString(), + date: today, + }) + .onConflictDoNothing(); + } + + console.log(`Fetched ${rates.length} exchange rates for ${today}`); + } + } catch (error) { + console.error('Failed to fetch exchange rates:', error); + } + } + + async seedRates() { + // Seed some default rates if none exist + const existing = await this.db.select().from(exchangeRates).limit(1); + + if (existing.length > 0) { + return { message: 'Rates already exist', seeded: false }; + } + + await this.fetchRates(); + return { message: 'Rates seeded', seeded: true }; + } +} diff --git a/apps/finance/apps/backend/src/health/health.controller.ts b/apps/finance/apps/backend/src/health/health.controller.ts new file mode 100644 index 000000000..d9a40cd99 --- /dev/null +++ b/apps/finance/apps/backend/src/health/health.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('health') +export class HealthController { + @Get() + check() { + return { + status: 'ok', + timestamp: new Date().toISOString(), + service: 'finance-backend', + }; + } +} diff --git a/apps/finance/apps/backend/src/health/health.module.ts b/apps/finance/apps/backend/src/health/health.module.ts new file mode 100644 index 000000000..a61d8b044 --- /dev/null +++ b/apps/finance/apps/backend/src/health/health.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { HealthController } from './health.controller'; + +@Module({ + controllers: [HealthController], +}) +export class HealthModule {} diff --git a/apps/finance/apps/backend/src/main.ts b/apps/finance/apps/backend/src/main.ts new file mode 100644 index 000000000..6cd5d0006 --- /dev/null +++ b/apps/finance/apps/backend/src/main.ts @@ -0,0 +1,45 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const configService = app.get(ConfigService); + + // CORS configuration + const corsOrigins = configService.get('CORS_ORIGINS')?.split(',') || [ + 'http://localhost:5173', + 'http://localhost:5189', + 'http://localhost:8081', + ]; + + app.enableCors({ + origin: corsOrigins, + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + credentials: true, + }); + + // Global validation pipe + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + transformOptions: { + enableImplicitConversion: true, + }, + }) + ); + + // API prefix + app.setGlobalPrefix('api/v1'); + + const port = configService.get('PORT') || 3019; + await app.listen(port); + + console.log(`Finance Backend running on http://localhost:${port}`); + console.log(`Health check: http://localhost:${port}/api/v1/health`); +} + +bootstrap(); diff --git a/apps/finance/apps/backend/src/report/report.controller.ts b/apps/finance/apps/backend/src/report/report.controller.ts new file mode 100644 index 000000000..0cd43f73c --- /dev/null +++ b/apps/finance/apps/backend/src/report/report.controller.ts @@ -0,0 +1,50 @@ +import { Controller, Get, Query, UseGuards, ParseIntPipe, DefaultValuePipe } from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, type CurrentUserData } from '@manacore/shared-nestjs-auth'; +import { ReportService } from './report.service'; + +@Controller('reports') +@UseGuards(JwtAuthGuard) +export class ReportController { + constructor(private readonly reportService: ReportService) {} + + @Get('dashboard') + getDashboard(@CurrentUser() user: CurrentUserData) { + return this.reportService.getDashboard(user.userId); + } + + @Get('monthly-summary') + getMonthlySummary( + @CurrentUser() user: CurrentUserData, + @Query('year', new DefaultValuePipe(new Date().getFullYear()), ParseIntPipe) year: number, + @Query('month', new DefaultValuePipe(new Date().getMonth() + 1), ParseIntPipe) month: number + ) { + return this.reportService.getMonthlySummary(user.userId, year, month); + } + + @Get('category-breakdown') + getCategoryBreakdown( + @CurrentUser() user: CurrentUserData, + @Query('startDate') startDate: string, + @Query('endDate') endDate: string, + @Query('type') type?: 'income' | 'expense' + ) { + return this.reportService.getCategoryBreakdown(user.userId, startDate, endDate, type); + } + + @Get('trends') + getTrends( + @CurrentUser() user: CurrentUserData, + @Query('months', new DefaultValuePipe(6), ParseIntPipe) months: number + ) { + return this.reportService.getTrends(user.userId, months); + } + + @Get('cash-flow') + getCashFlow( + @CurrentUser() user: CurrentUserData, + @Query('startDate') startDate: string, + @Query('endDate') endDate: string + ) { + return this.reportService.getCashFlow(user.userId, startDate, endDate); + } +} diff --git a/apps/finance/apps/backend/src/report/report.module.ts b/apps/finance/apps/backend/src/report/report.module.ts new file mode 100644 index 000000000..23194ff11 --- /dev/null +++ b/apps/finance/apps/backend/src/report/report.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ReportController } from './report.controller'; +import { ReportService } from './report.service'; + +@Module({ + controllers: [ReportController], + providers: [ReportService], + exports: [ReportService], +}) +export class ReportModule {} diff --git a/apps/finance/apps/backend/src/report/report.service.ts b/apps/finance/apps/backend/src/report/report.service.ts new file mode 100644 index 000000000..2edd006ee --- /dev/null +++ b/apps/finance/apps/backend/src/report/report.service.ts @@ -0,0 +1,396 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { eq, and, sql, gte, lte, desc } from 'drizzle-orm'; +import { DATABASE_CONNECTION, type Database } from '../db/connection'; +import { transactions, accounts, categories, budgets } from '../db/schema'; + +@Injectable() +export class ReportService { + constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} + + async getDashboard(userId: string) { + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + + // Current month range + const startOfMonth = `${year}-${String(month).padStart(2, '0')}-01`; + const endOfMonth = new Date(year, month, 0).toISOString().split('T')[0]; + + // Account totals + const accountTotals = await this.db + .select({ + currency: accounts.currency, + total: sql`SUM(${accounts.balance})`, + }) + .from(accounts) + .where( + and( + eq(accounts.userId, userId), + eq(accounts.isArchived, false), + eq(accounts.includeInTotal, true) + ) + ) + .groupBy(accounts.currency); + + // Current month income/expense + const monthlyTotals = await this.db + .select({ + type: transactions.type, + total: sql`SUM(${transactions.amount})`, + }) + .from(transactions) + .where( + and( + eq(transactions.userId, userId), + gte(transactions.date, startOfMonth), + lte(transactions.date, endOfMonth) + ) + ) + .groupBy(transactions.type); + + const income = monthlyTotals.find((t) => t.type === 'income'); + const expense = monthlyTotals.find((t) => t.type === 'expense'); + + // Budget progress + const budgetProgress = await this.db + .select({ + budget: budgets, + category: { + id: categories.id, + name: categories.name, + color: categories.color, + }, + }) + .from(budgets) + .leftJoin(categories, eq(budgets.categoryId, categories.id)) + .where(and(eq(budgets.userId, userId), eq(budgets.month, month), eq(budgets.year, year))); + + // Get spending per category + const categorySpending = await this.db + .select({ + categoryId: transactions.categoryId, + total: sql`SUM(${transactions.amount})`, + }) + .from(transactions) + .where( + and( + eq(transactions.userId, userId), + eq(transactions.type, 'expense'), + gte(transactions.date, startOfMonth), + lte(transactions.date, endOfMonth) + ) + ) + .groupBy(transactions.categoryId); + + const spendingMap = new Map( + categorySpending.map((s) => [s.categoryId, parseFloat(s.total ?? '0')]) + ); + + // Recent transactions + const recentTransactions = await this.db + .select({ + transaction: transactions, + category: { + id: categories.id, + name: categories.name, + color: categories.color, + icon: categories.icon, + }, + account: { + id: accounts.id, + name: accounts.name, + color: accounts.color, + }, + }) + .from(transactions) + .leftJoin(categories, eq(transactions.categoryId, categories.id)) + .leftJoin(accounts, eq(transactions.accountId, accounts.id)) + .where(eq(transactions.userId, userId)) + .orderBy(desc(transactions.date), desc(transactions.createdAt)) + .limit(5); + + return { + totals: accountTotals.map((t) => ({ + currency: t.currency, + amount: parseFloat(t.total ?? '0'), + })), + currentMonth: { + year, + month, + income: parseFloat(income?.total ?? '0'), + expense: parseFloat(expense?.total ?? '0'), + net: parseFloat(income?.total ?? '0') - parseFloat(expense?.total ?? '0'), + }, + budgets: budgetProgress.map((b) => ({ + id: b.budget.id, + category: b.category, + amount: parseFloat(b.budget.amount), + spent: b.budget.categoryId + ? (spendingMap.get(b.budget.categoryId) ?? 0) + : parseFloat(expense?.total ?? '0'), + percentage: + (b.budget.categoryId + ? (spendingMap.get(b.budget.categoryId) ?? 0) + : parseFloat(expense?.total ?? '0')) / parseFloat(b.budget.amount), + })), + recentTransactions: recentTransactions.map((r) => ({ + ...r.transaction, + category: r.category, + account: r.account, + })), + }; + } + + async getMonthlySummary(userId: string, year: number, month: number) { + const startDate = `${year}-${String(month).padStart(2, '0')}-01`; + const endDate = new Date(year, month, 0).toISOString().split('T')[0]; + + // Totals by type + const totals = await this.db + .select({ + type: transactions.type, + total: sql`SUM(${transactions.amount})`, + count: sql`COUNT(*)`, + }) + .from(transactions) + .where( + and( + eq(transactions.userId, userId), + gte(transactions.date, startDate), + lte(transactions.date, endDate) + ) + ) + .groupBy(transactions.type); + + // Daily breakdown + const dailyBreakdown = await this.db + .select({ + date: transactions.date, + type: transactions.type, + total: sql`SUM(${transactions.amount})`, + }) + .from(transactions) + .where( + and( + eq(transactions.userId, userId), + gte(transactions.date, startDate), + lte(transactions.date, endDate) + ) + ) + .groupBy(transactions.date, transactions.type) + .orderBy(transactions.date); + + // Top expenses + const topExpenses = await this.db + .select({ + transaction: transactions, + category: { + id: categories.id, + name: categories.name, + color: categories.color, + }, + }) + .from(transactions) + .leftJoin(categories, eq(transactions.categoryId, categories.id)) + .where( + and( + eq(transactions.userId, userId), + eq(transactions.type, 'expense'), + gte(transactions.date, startDate), + lte(transactions.date, endDate) + ) + ) + .orderBy(desc(transactions.amount)) + .limit(10); + + const income = totals.find((t) => t.type === 'income'); + const expense = totals.find((t) => t.type === 'expense'); + + return { + year, + month, + income: parseFloat(income?.total ?? '0'), + expense: parseFloat(expense?.total ?? '0'), + net: parseFloat(income?.total ?? '0') - parseFloat(expense?.total ?? '0'), + incomeCount: Number(income?.count ?? 0), + expenseCount: Number(expense?.count ?? 0), + dailyBreakdown: dailyBreakdown.map((d) => ({ + date: d.date, + type: d.type, + amount: parseFloat(d.total ?? '0'), + })), + topExpenses: topExpenses.map((e) => ({ + ...e.transaction, + category: e.category, + })), + }; + } + + async getCategoryBreakdown( + userId: string, + startDate: string, + endDate: string, + type: 'income' | 'expense' = 'expense' + ) { + const breakdown = await this.db + .select({ + categoryId: transactions.categoryId, + categoryName: categories.name, + categoryColor: categories.color, + categoryIcon: categories.icon, + total: sql`SUM(${transactions.amount})`, + count: sql`COUNT(*)`, + }) + .from(transactions) + .leftJoin(categories, eq(transactions.categoryId, categories.id)) + .where( + and( + eq(transactions.userId, userId), + eq(transactions.type, type), + gte(transactions.date, startDate), + lte(transactions.date, endDate) + ) + ) + .groupBy(transactions.categoryId, categories.name, categories.color, categories.icon) + .orderBy(desc(sql`SUM(${transactions.amount})`)); + + const total = breakdown.reduce((sum, b) => sum + parseFloat(b.total ?? '0'), 0); + + return { + startDate, + endDate, + type, + total, + categories: breakdown.map((b) => ({ + categoryId: b.categoryId, + name: b.categoryName ?? 'Uncategorized', + color: b.categoryColor, + icon: b.categoryIcon, + amount: parseFloat(b.total ?? '0'), + count: Number(b.count), + percentage: total > 0 ? parseFloat(b.total ?? '0') / total : 0, + })), + }; + } + + async getTrends(userId: string, months = 6) { + const trends = []; + const now = new Date(); + + for (let i = 0; i < months; i++) { + const date = new Date(now.getFullYear(), now.getMonth() - i, 1); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + + const startDate = `${year}-${String(month).padStart(2, '0')}-01`; + const endDate = new Date(year, month, 0).toISOString().split('T')[0]; + + const totals = await this.db + .select({ + type: transactions.type, + total: sql`SUM(${transactions.amount})`, + }) + .from(transactions) + .where( + and( + eq(transactions.userId, userId), + gte(transactions.date, startDate), + lte(transactions.date, endDate) + ) + ) + .groupBy(transactions.type); + + const income = totals.find((t) => t.type === 'income'); + const expense = totals.find((t) => t.type === 'expense'); + + trends.unshift({ + year, + month, + income: parseFloat(income?.total ?? '0'), + expense: parseFloat(expense?.total ?? '0'), + net: parseFloat(income?.total ?? '0') - parseFloat(expense?.total ?? '0'), + }); + } + + return { + months, + data: trends, + averages: { + income: trends.reduce((sum, t) => sum + t.income, 0) / months, + expense: trends.reduce((sum, t) => sum + t.expense, 0) / months, + net: trends.reduce((sum, t) => sum + t.net, 0) / months, + }, + }; + } + + async getCashFlow(userId: string, startDate: string, endDate: string) { + // Get starting balance + const startBalance = await this.db + .select({ + total: sql`SUM(${accounts.balance})`, + }) + .from(accounts) + .where( + and( + eq(accounts.userId, userId), + eq(accounts.isArchived, false), + eq(accounts.includeInTotal, true) + ) + ); + + // Get daily transactions + const dailyFlow = await this.db + .select({ + date: transactions.date, + type: transactions.type, + total: sql`SUM(${transactions.amount})`, + }) + .from(transactions) + .where( + and( + eq(transactions.userId, userId), + gte(transactions.date, startDate), + lte(transactions.date, endDate) + ) + ) + .groupBy(transactions.date, transactions.type) + .orderBy(transactions.date); + + // Build cumulative cash flow + let runningTotal = parseFloat(startBalance[0]?.total ?? '0'); + const cashFlow: { date: string; balance: number; income: number; expense: number }[] = []; + + // Group by date + const byDate = new Map(); + dailyFlow.forEach((d) => { + if (!byDate.has(d.date)) { + byDate.set(d.date, { income: 0, expense: 0 }); + } + const entry = byDate.get(d.date)!; + if (d.type === 'income') { + entry.income = parseFloat(d.total ?? '0'); + } else { + entry.expense = parseFloat(d.total ?? '0'); + } + }); + + // Convert to array with running balance + byDate.forEach((value, date) => { + runningTotal += value.income - value.expense; + cashFlow.push({ + date, + balance: runningTotal, + income: value.income, + expense: value.expense, + }); + }); + + return { + startDate, + endDate, + startingBalance: parseFloat(startBalance[0]?.total ?? '0'), + endingBalance: runningTotal, + data: cashFlow, + }; + } +} diff --git a/apps/finance/apps/backend/src/settings/dto/index.ts b/apps/finance/apps/backend/src/settings/dto/index.ts new file mode 100644 index 000000000..dd653c685 --- /dev/null +++ b/apps/finance/apps/backend/src/settings/dto/index.ts @@ -0,0 +1 @@ +export * from './update-settings.dto'; diff --git a/apps/finance/apps/backend/src/settings/dto/update-settings.dto.ts b/apps/finance/apps/backend/src/settings/dto/update-settings.dto.ts new file mode 100644 index 000000000..92d3e71af --- /dev/null +++ b/apps/finance/apps/backend/src/settings/dto/update-settings.dto.ts @@ -0,0 +1,24 @@ +import { IsString, IsOptional, IsNumber, MaxLength, Min, Max } from 'class-validator'; + +export class UpdateSettingsDto { + @IsOptional() + @IsString() + @MaxLength(3) + defaultCurrency?: string; + + @IsOptional() + @IsString() + @MaxLength(10) + locale?: string; + + @IsOptional() + @IsString() + @MaxLength(20) + dateFormat?: string; + + @IsOptional() + @IsNumber() + @Min(0) + @Max(6) + weekStartsOn?: number; +} diff --git a/apps/finance/apps/backend/src/settings/settings.controller.ts b/apps/finance/apps/backend/src/settings/settings.controller.ts new file mode 100644 index 000000000..ec30faa19 --- /dev/null +++ b/apps/finance/apps/backend/src/settings/settings.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get, Put, Body, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, type CurrentUserData } from '@manacore/shared-nestjs-auth'; +import { SettingsService } from './settings.service'; +import { UpdateSettingsDto } from './dto'; + +@Controller('settings') +@UseGuards(JwtAuthGuard) +export class SettingsController { + constructor(private readonly settingsService: SettingsService) {} + + @Get() + get(@CurrentUser() user: CurrentUserData) { + return this.settingsService.get(user.userId); + } + + @Put() + update(@CurrentUser() user: CurrentUserData, @Body() dto: UpdateSettingsDto) { + return this.settingsService.update(user.userId, dto); + } +} diff --git a/apps/finance/apps/backend/src/settings/settings.module.ts b/apps/finance/apps/backend/src/settings/settings.module.ts new file mode 100644 index 000000000..8e2e2dd24 --- /dev/null +++ b/apps/finance/apps/backend/src/settings/settings.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SettingsController } from './settings.controller'; +import { SettingsService } from './settings.service'; + +@Module({ + controllers: [SettingsController], + providers: [SettingsService], + exports: [SettingsService], +}) +export class SettingsModule {} diff --git a/apps/finance/apps/backend/src/settings/settings.service.ts b/apps/finance/apps/backend/src/settings/settings.service.ts new file mode 100644 index 000000000..702100e80 --- /dev/null +++ b/apps/finance/apps/backend/src/settings/settings.service.ts @@ -0,0 +1,57 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { DATABASE_CONNECTION, type Database } from '../db/connection'; +import { userSettings } from '../db/schema'; +import { UpdateSettingsDto } from './dto'; + +const DEFAULT_SETTINGS = { + defaultCurrency: 'EUR', + locale: 'de-DE', + dateFormat: 'dd.MM.yyyy', + weekStartsOn: 1, // Monday +}; + +@Injectable() +export class SettingsService { + constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} + + async get(userId: string) { + const [settings] = await this.db + .select() + .from(userSettings) + .where(eq(userSettings.userId, userId)); + + if (!settings) { + // Create default settings + const [newSettings] = await this.db + .insert(userSettings) + .values({ + userId, + ...DEFAULT_SETTINGS, + }) + .returning(); + return newSettings; + } + + return settings; + } + + async update(userId: string, dto: UpdateSettingsDto) { + // Ensure settings exist + await this.get(userId); + + const [settings] = await this.db + .update(userSettings) + .set({ + ...(dto.defaultCurrency !== undefined && { defaultCurrency: dto.defaultCurrency }), + ...(dto.locale !== undefined && { locale: dto.locale }), + ...(dto.dateFormat !== undefined && { dateFormat: dto.dateFormat }), + ...(dto.weekStartsOn !== undefined && { weekStartsOn: dto.weekStartsOn }), + updatedAt: new Date(), + }) + .where(eq(userSettings.userId, userId)) + .returning(); + + return settings; + } +} diff --git a/apps/finance/apps/backend/src/transaction/dto/create-transaction.dto.ts b/apps/finance/apps/backend/src/transaction/dto/create-transaction.dto.ts new file mode 100644 index 000000000..9fe49c08d --- /dev/null +++ b/apps/finance/apps/backend/src/transaction/dto/create-transaction.dto.ts @@ -0,0 +1,86 @@ +import { + IsString, + IsNotEmpty, + IsOptional, + IsBoolean, + IsNumber, + IsUUID, + IsDateString, + IsArray, + IsIn, + ValidateNested, + MaxLength, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +const TRANSACTION_TYPES = ['income', 'expense'] as const; +const RECURRENCE_FREQUENCIES = ['daily', 'weekly', 'monthly', 'yearly'] as const; + +export class RecurrenceRuleDto { + @IsString() + @IsIn(RECURRENCE_FREQUENCIES) + frequency: (typeof RECURRENCE_FREQUENCIES)[number]; + + @IsNumber() + interval: number; + + @IsOptional() + @IsDateString() + endDate?: string; + + @IsOptional() + @IsNumber() + count?: number; +} + +export class CreateTransactionDto { + @IsUUID() + accountId: string; + + @IsOptional() + @IsUUID() + categoryId?: string; + + @IsString() + @IsIn(TRANSACTION_TYPES) + type: (typeof TRANSACTION_TYPES)[number]; + + @IsNumber() + amount: number; + + @IsDateString() + date: string; + + @IsOptional() + @IsString() + @MaxLength(500) + description?: string; + + @IsOptional() + @IsString() + @MaxLength(200) + payee?: string; + + @IsOptional() + @IsBoolean() + isRecurring?: boolean; + + @IsOptional() + @ValidateNested() + @Type(() => RecurrenceRuleDto) + recurrenceRule?: RecurrenceRuleDto; + + @IsOptional() + @IsBoolean() + isPending?: boolean; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + tags?: string[]; + + @IsOptional() + @IsString() + @MaxLength(3) + currency?: string; +} diff --git a/apps/finance/apps/backend/src/transaction/dto/index.ts b/apps/finance/apps/backend/src/transaction/dto/index.ts new file mode 100644 index 000000000..97807d286 --- /dev/null +++ b/apps/finance/apps/backend/src/transaction/dto/index.ts @@ -0,0 +1,3 @@ +export * from './create-transaction.dto'; +export * from './update-transaction.dto'; +export * from './query-transaction.dto'; diff --git a/apps/finance/apps/backend/src/transaction/dto/query-transaction.dto.ts b/apps/finance/apps/backend/src/transaction/dto/query-transaction.dto.ts new file mode 100644 index 000000000..fb37269d8 --- /dev/null +++ b/apps/finance/apps/backend/src/transaction/dto/query-transaction.dto.ts @@ -0,0 +1,69 @@ +import { + IsString, + IsOptional, + IsBoolean, + IsNumber, + IsUUID, + IsDateString, + IsIn, +} from 'class-validator'; +import { Transform } from 'class-transformer'; + +const TRANSACTION_TYPES = ['income', 'expense'] as const; + +export class QueryTransactionDto { + @IsOptional() + @IsUUID() + accountId?: string; + + @IsOptional() + @IsUUID() + categoryId?: string; + + @IsOptional() + @IsString() + @IsIn(TRANSACTION_TYPES) + type?: (typeof TRANSACTION_TYPES)[number]; + + @IsOptional() + @IsDateString() + startDate?: string; + + @IsOptional() + @IsDateString() + endDate?: string; + + @IsOptional() + @Transform(({ value }) => parseFloat(value)) + @IsNumber() + minAmount?: number; + + @IsOptional() + @Transform(({ value }) => parseFloat(value)) + @IsNumber() + maxAmount?: number; + + @IsOptional() + @IsString() + search?: string; + + @IsOptional() + @Transform(({ value }) => value === 'true' || value === true) + @IsBoolean() + isPending?: boolean; + + @IsOptional() + @Transform(({ value }) => value === 'true' || value === true) + @IsBoolean() + isRecurring?: boolean; + + @IsOptional() + @Transform(({ value }) => parseInt(value, 10)) + @IsNumber() + limit?: number; + + @IsOptional() + @Transform(({ value }) => parseInt(value, 10)) + @IsNumber() + offset?: number; +} diff --git a/apps/finance/apps/backend/src/transaction/dto/update-transaction.dto.ts b/apps/finance/apps/backend/src/transaction/dto/update-transaction.dto.ts new file mode 100644 index 000000000..24e38db70 --- /dev/null +++ b/apps/finance/apps/backend/src/transaction/dto/update-transaction.dto.ts @@ -0,0 +1,72 @@ +import { + IsString, + IsOptional, + IsBoolean, + IsNumber, + IsUUID, + IsDateString, + IsArray, + IsIn, + ValidateNested, + MaxLength, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { RecurrenceRuleDto } from './create-transaction.dto'; + +const TRANSACTION_TYPES = ['income', 'expense'] as const; + +export class UpdateTransactionDto { + @IsOptional() + @IsUUID() + accountId?: string; + + @IsOptional() + @IsUUID() + categoryId?: string | null; + + @IsOptional() + @IsString() + @IsIn(TRANSACTION_TYPES) + type?: (typeof TRANSACTION_TYPES)[number]; + + @IsOptional() + @IsNumber() + amount?: number; + + @IsOptional() + @IsDateString() + date?: string; + + @IsOptional() + @IsString() + @MaxLength(500) + description?: string; + + @IsOptional() + @IsString() + @MaxLength(200) + payee?: string; + + @IsOptional() + @IsBoolean() + isRecurring?: boolean; + + @IsOptional() + @ValidateNested() + @Type(() => RecurrenceRuleDto) + recurrenceRule?: RecurrenceRuleDto | null; + + @IsOptional() + @IsBoolean() + isPending?: boolean; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + tags?: string[]; + + @IsOptional() + @IsString() + @MaxLength(3) + currency?: string; +} diff --git a/apps/finance/apps/backend/src/transaction/transaction.controller.ts b/apps/finance/apps/backend/src/transaction/transaction.controller.ts new file mode 100644 index 000000000..2bbd3c383 --- /dev/null +++ b/apps/finance/apps/backend/src/transaction/transaction.controller.ts @@ -0,0 +1,64 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UseGuards, + ParseUUIDPipe, +} from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, type CurrentUserData } from '@manacore/shared-nestjs-auth'; +import { TransactionService } from './transaction.service'; +import { CreateTransactionDto, UpdateTransactionDto, QueryTransactionDto } from './dto'; + +@Controller('transactions') +@UseGuards(JwtAuthGuard) +export class TransactionController { + constructor(private readonly transactionService: TransactionService) {} + + @Get() + findAll(@CurrentUser() user: CurrentUserData, @Query() query: QueryTransactionDto) { + return this.transactionService.findAll(user.userId, query); + } + + @Get('recent') + findRecent(@CurrentUser() user: CurrentUserData, @Query('limit') limit?: number) { + return this.transactionService.findRecent(user.userId, limit); + } + + @Get('summary') + getSummary( + @CurrentUser() user: CurrentUserData, + @Query('startDate') startDate: string, + @Query('endDate') endDate: string + ) { + return this.transactionService.getSummary(user.userId, startDate, endDate); + } + + @Get(':id') + findOne(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.transactionService.findOne(user.userId, id); + } + + @Post() + create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateTransactionDto) { + return this.transactionService.create(user.userId, dto); + } + + @Put(':id') + update( + @CurrentUser() user: CurrentUserData, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateTransactionDto + ) { + return this.transactionService.update(user.userId, id, dto); + } + + @Delete(':id') + delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.transactionService.delete(user.userId, id); + } +} diff --git a/apps/finance/apps/backend/src/transaction/transaction.module.ts b/apps/finance/apps/backend/src/transaction/transaction.module.ts new file mode 100644 index 000000000..0d3d7ed61 --- /dev/null +++ b/apps/finance/apps/backend/src/transaction/transaction.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TransactionController } from './transaction.controller'; +import { TransactionService } from './transaction.service'; +import { AccountModule } from '../account/account.module'; + +@Module({ + imports: [AccountModule], + controllers: [TransactionController], + providers: [TransactionService], + exports: [TransactionService], +}) +export class TransactionModule {} diff --git a/apps/finance/apps/backend/src/transaction/transaction.service.ts b/apps/finance/apps/backend/src/transaction/transaction.service.ts new file mode 100644 index 000000000..19dbc526f --- /dev/null +++ b/apps/finance/apps/backend/src/transaction/transaction.service.ts @@ -0,0 +1,301 @@ +import { Injectable, Inject, NotFoundException } from '@nestjs/common'; +import { eq, and, desc, gte, lte, like, or, sql } from 'drizzle-orm'; +import { DATABASE_CONNECTION, type Database } from '../db/connection'; +import { transactions, accounts, categories } from '../db/schema'; +import { AccountService } from '../account/account.service'; +import { CreateTransactionDto, UpdateTransactionDto, QueryTransactionDto } from './dto'; + +@Injectable() +export class TransactionService { + constructor( + @Inject(DATABASE_CONNECTION) private db: Database, + private accountService: AccountService + ) {} + + async findAll(userId: string, query: QueryTransactionDto) { + const conditions = [eq(transactions.userId, userId)]; + + if (query.accountId) { + conditions.push(eq(transactions.accountId, query.accountId)); + } + + if (query.categoryId) { + conditions.push(eq(transactions.categoryId, query.categoryId)); + } + + if (query.type) { + conditions.push(eq(transactions.type, query.type)); + } + + if (query.startDate) { + conditions.push(gte(transactions.date, query.startDate)); + } + + if (query.endDate) { + conditions.push(lte(transactions.date, query.endDate)); + } + + if (query.minAmount !== undefined) { + conditions.push(gte(transactions.amount, query.minAmount.toString())); + } + + if (query.maxAmount !== undefined) { + conditions.push(lte(transactions.amount, query.maxAmount.toString())); + } + + if (query.search) { + const searchTerm = `%${query.search}%`; + conditions.push( + or(like(transactions.description, searchTerm), like(transactions.payee, searchTerm))! + ); + } + + if (query.isPending !== undefined) { + conditions.push(eq(transactions.isPending, query.isPending)); + } + + if (query.isRecurring !== undefined) { + conditions.push(eq(transactions.isRecurring, query.isRecurring)); + } + + const limit = query.limit ?? 50; + const offset = query.offset ?? 0; + + const result = await this.db + .select({ + transaction: transactions, + account: { + id: accounts.id, + name: accounts.name, + type: accounts.type, + currency: accounts.currency, + color: accounts.color, + }, + category: { + id: categories.id, + name: categories.name, + type: categories.type, + color: categories.color, + icon: categories.icon, + }, + }) + .from(transactions) + .leftJoin(accounts, eq(transactions.accountId, accounts.id)) + .leftJoin(categories, eq(transactions.categoryId, categories.id)) + .where(and(...conditions)) + .orderBy(desc(transactions.date), desc(transactions.createdAt)) + .limit(limit) + .offset(offset); + + // Get total count for pagination + const [{ count }] = await this.db + .select({ count: sql`COUNT(*)` }) + .from(transactions) + .where(and(...conditions)); + + return { + data: result.map((r) => ({ + ...r.transaction, + account: r.account, + category: r.category, + })), + total: Number(count), + limit, + offset, + }; + } + + async findOne(userId: string, id: string) { + const [result] = await this.db + .select({ + transaction: transactions, + account: { + id: accounts.id, + name: accounts.name, + type: accounts.type, + currency: accounts.currency, + color: accounts.color, + }, + category: { + id: categories.id, + name: categories.name, + type: categories.type, + color: categories.color, + icon: categories.icon, + }, + }) + .from(transactions) + .leftJoin(accounts, eq(transactions.accountId, accounts.id)) + .leftJoin(categories, eq(transactions.categoryId, categories.id)) + .where(and(eq(transactions.id, id), eq(transactions.userId, userId))); + + if (!result) { + throw new NotFoundException(`Transaction with ID ${id} not found`); + } + + return { + ...result.transaction, + account: result.account, + category: result.category, + }; + } + + async findRecent(userId: string, limit = 10) { + const result = await this.db + .select({ + transaction: transactions, + account: { + id: accounts.id, + name: accounts.name, + type: accounts.type, + currency: accounts.currency, + color: accounts.color, + }, + category: { + id: categories.id, + name: categories.name, + type: categories.type, + color: categories.color, + icon: categories.icon, + }, + }) + .from(transactions) + .leftJoin(accounts, eq(transactions.accountId, accounts.id)) + .leftJoin(categories, eq(transactions.categoryId, categories.id)) + .where(eq(transactions.userId, userId)) + .orderBy(desc(transactions.date), desc(transactions.createdAt)) + .limit(limit); + + return result.map((r) => ({ + ...r.transaction, + account: r.account, + category: r.category, + })); + } + + async create(userId: string, dto: CreateTransactionDto) { + // Verify account ownership + const account = await this.accountService.findOne(userId, dto.accountId); + + const [transaction] = await this.db + .insert(transactions) + .values({ + userId, + accountId: dto.accountId, + categoryId: dto.categoryId, + type: dto.type, + amount: dto.amount.toString(), + currency: dto.currency ?? account.currency, + date: dto.date, + description: dto.description, + payee: dto.payee, + isRecurring: dto.isRecurring ?? false, + recurrenceRule: dto.recurrenceRule, + isPending: dto.isPending ?? false, + tags: dto.tags ?? [], + }) + .returning(); + + // Update account balance + const balanceChange = dto.type === 'income' ? dto.amount : -dto.amount; + await this.accountService.updateBalance(userId, dto.accountId, balanceChange); + + return this.findOne(userId, transaction.id); + } + + async update(userId: string, id: string, dto: UpdateTransactionDto) { + // Get original transaction + const original = await this.findOne(userId, id); + + // If amount or type changed, we need to adjust account balance + const oldBalanceEffect = + original.type === 'income' ? parseFloat(original.amount) : -parseFloat(original.amount); + + const [transaction] = await this.db + .update(transactions) + .set({ + ...(dto.accountId !== undefined && { accountId: dto.accountId }), + ...(dto.categoryId !== undefined && { categoryId: dto.categoryId }), + ...(dto.type !== undefined && { type: dto.type }), + ...(dto.amount !== undefined && { amount: dto.amount.toString() }), + ...(dto.currency !== undefined && { currency: dto.currency }), + ...(dto.date !== undefined && { date: dto.date }), + ...(dto.description !== undefined && { description: dto.description }), + ...(dto.payee !== undefined && { payee: dto.payee }), + ...(dto.isRecurring !== undefined && { isRecurring: dto.isRecurring }), + ...(dto.recurrenceRule !== undefined && { recurrenceRule: dto.recurrenceRule }), + ...(dto.isPending !== undefined && { isPending: dto.isPending }), + ...(dto.tags !== undefined && { tags: dto.tags }), + updatedAt: new Date(), + }) + .where(and(eq(transactions.id, id), eq(transactions.userId, userId))) + .returning(); + + // Calculate new balance effect + const newType = dto.type ?? original.type; + const newAmount = dto.amount ?? parseFloat(original.amount); + const newBalanceEffect = newType === 'income' ? newAmount : -newAmount; + const newAccountId = dto.accountId ?? original.accountId!; + + // If account changed, adjust both accounts + if (dto.accountId && dto.accountId !== original.accountId) { + // Reverse on old account + await this.accountService.updateBalance(userId, original.accountId!, -oldBalanceEffect); + // Apply to new account + await this.accountService.updateBalance(userId, dto.accountId, newBalanceEffect); + } else if (dto.amount !== undefined || dto.type !== undefined) { + // Same account, but amount or type changed + const balanceDiff = newBalanceEffect - oldBalanceEffect; + await this.accountService.updateBalance(userId, newAccountId, balanceDiff); + } + + return this.findOne(userId, transaction.id); + } + + async delete(userId: string, id: string) { + // Get transaction to reverse balance + const transaction = await this.findOne(userId, id); + const balanceEffect = + transaction.type === 'income' + ? parseFloat(transaction.amount) + : -parseFloat(transaction.amount); + + await this.db + .delete(transactions) + .where(and(eq(transactions.id, id), eq(transactions.userId, userId))); + + // Reverse balance effect + await this.accountService.updateBalance(userId, transaction.accountId!, -balanceEffect); + + return { success: true }; + } + + async getSummary(userId: string, startDate: string, endDate: string) { + const result = await this.db + .select({ + type: transactions.type, + total: sql`SUM(${transactions.amount})`, + count: sql`COUNT(*)`, + }) + .from(transactions) + .where( + and( + eq(transactions.userId, userId), + gte(transactions.date, startDate), + lte(transactions.date, endDate) + ) + ) + .groupBy(transactions.type); + + const income = result.find((r) => r.type === 'income'); + const expense = result.find((r) => r.type === 'expense'); + + return { + income: parseFloat(income?.total ?? '0'), + expense: parseFloat(expense?.total ?? '0'), + net: parseFloat(income?.total ?? '0') - parseFloat(expense?.total ?? '0'), + incomeCount: Number(income?.count ?? 0), + expenseCount: Number(expense?.count ?? 0), + }; + } +} diff --git a/apps/finance/apps/backend/src/transfer/dto/create-transfer.dto.ts b/apps/finance/apps/backend/src/transfer/dto/create-transfer.dto.ts new file mode 100644 index 000000000..5c5c549de --- /dev/null +++ b/apps/finance/apps/backend/src/transfer/dto/create-transfer.dto.ts @@ -0,0 +1,29 @@ +import { + IsString, + IsOptional, + IsNumber, + IsUUID, + IsDateString, + MaxLength, + Min, +} from 'class-validator'; + +export class CreateTransferDto { + @IsUUID() + fromAccountId: string; + + @IsUUID() + toAccountId: string; + + @IsNumber() + @Min(0.01) + amount: number; + + @IsDateString() + date: string; + + @IsOptional() + @IsString() + @MaxLength(500) + description?: string; +} diff --git a/apps/finance/apps/backend/src/transfer/dto/index.ts b/apps/finance/apps/backend/src/transfer/dto/index.ts new file mode 100644 index 000000000..949b31419 --- /dev/null +++ b/apps/finance/apps/backend/src/transfer/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-transfer.dto'; +export * from './update-transfer.dto'; diff --git a/apps/finance/apps/backend/src/transfer/dto/update-transfer.dto.ts b/apps/finance/apps/backend/src/transfer/dto/update-transfer.dto.ts new file mode 100644 index 000000000..f706e3bb2 --- /dev/null +++ b/apps/finance/apps/backend/src/transfer/dto/update-transfer.dto.ts @@ -0,0 +1,33 @@ +import { + IsString, + IsOptional, + IsNumber, + IsUUID, + IsDateString, + MaxLength, + Min, +} from 'class-validator'; + +export class UpdateTransferDto { + @IsOptional() + @IsUUID() + fromAccountId?: string; + + @IsOptional() + @IsUUID() + toAccountId?: string; + + @IsOptional() + @IsNumber() + @Min(0.01) + amount?: number; + + @IsOptional() + @IsDateString() + date?: string; + + @IsOptional() + @IsString() + @MaxLength(500) + description?: string; +} diff --git a/apps/finance/apps/backend/src/transfer/transfer.controller.ts b/apps/finance/apps/backend/src/transfer/transfer.controller.ts new file mode 100644 index 000000000..18a874d08 --- /dev/null +++ b/apps/finance/apps/backend/src/transfer/transfer.controller.ts @@ -0,0 +1,49 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + UseGuards, + ParseUUIDPipe, +} from '@nestjs/common'; +import { JwtAuthGuard, CurrentUser, type CurrentUserData } from '@manacore/shared-nestjs-auth'; +import { TransferService } from './transfer.service'; +import { CreateTransferDto, UpdateTransferDto } from './dto'; + +@Controller('transfers') +@UseGuards(JwtAuthGuard) +export class TransferController { + constructor(private readonly transferService: TransferService) {} + + @Get() + findAll(@CurrentUser() user: CurrentUserData) { + return this.transferService.findAll(user.userId); + } + + @Get(':id') + findOne(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.transferService.findOne(user.userId, id); + } + + @Post() + create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateTransferDto) { + return this.transferService.create(user.userId, dto); + } + + @Put(':id') + update( + @CurrentUser() user: CurrentUserData, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateTransferDto + ) { + return this.transferService.update(user.userId, id, dto); + } + + @Delete(':id') + delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { + return this.transferService.delete(user.userId, id); + } +} diff --git a/apps/finance/apps/backend/src/transfer/transfer.module.ts b/apps/finance/apps/backend/src/transfer/transfer.module.ts new file mode 100644 index 000000000..f7ef60588 --- /dev/null +++ b/apps/finance/apps/backend/src/transfer/transfer.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TransferController } from './transfer.controller'; +import { TransferService } from './transfer.service'; +import { AccountModule } from '../account/account.module'; + +@Module({ + imports: [AccountModule], + controllers: [TransferController], + providers: [TransferService], + exports: [TransferService], +}) +export class TransferModule {} diff --git a/apps/finance/apps/backend/src/transfer/transfer.service.ts b/apps/finance/apps/backend/src/transfer/transfer.service.ts new file mode 100644 index 000000000..aa71fef8d --- /dev/null +++ b/apps/finance/apps/backend/src/transfer/transfer.service.ts @@ -0,0 +1,162 @@ +import { Injectable, Inject, NotFoundException, BadRequestException } from '@nestjs/common'; +import { eq, and, desc, sql } from 'drizzle-orm'; +import { DATABASE_CONNECTION, type Database } from '../db/connection'; +import { transfers, accounts } from '../db/schema'; +import { AccountService } from '../account/account.service'; +import { CreateTransferDto, UpdateTransferDto } from './dto'; + +@Injectable() +export class TransferService { + constructor( + @Inject(DATABASE_CONNECTION) private db: Database, + private accountService: AccountService + ) {} + + async findAll(userId: string) { + const result = await this.db + .select({ + transfer: transfers, + fromAccount: { + id: sql`from_acc.id`, + name: sql`from_acc.name`, + currency: sql`from_acc.currency`, + color: sql`from_acc.color`, + }, + toAccount: { + id: sql`to_acc.id`, + name: sql`to_acc.name`, + currency: sql`to_acc.currency`, + color: sql`to_acc.color`, + }, + }) + .from(transfers) + .innerJoin(sql`${accounts} as from_acc`, sql`${transfers.fromAccountId} = from_acc.id`) + .innerJoin(sql`${accounts} as to_acc`, sql`${transfers.toAccountId} = to_acc.id`) + .where(eq(transfers.userId, userId)) + .orderBy(desc(transfers.date), desc(transfers.createdAt)); + + return result.map((r) => ({ + ...r.transfer, + fromAccount: r.fromAccount, + toAccount: r.toAccount, + })); + } + + async findOne(userId: string, id: string) { + const [result] = await this.db + .select({ + transfer: transfers, + fromAccount: { + id: sql`from_acc.id`, + name: sql`from_acc.name`, + currency: sql`from_acc.currency`, + color: sql`from_acc.color`, + }, + toAccount: { + id: sql`to_acc.id`, + name: sql`to_acc.name`, + currency: sql`to_acc.currency`, + color: sql`to_acc.color`, + }, + }) + .from(transfers) + .innerJoin(sql`${accounts} as from_acc`, sql`${transfers.fromAccountId} = from_acc.id`) + .innerJoin(sql`${accounts} as to_acc`, sql`${transfers.toAccountId} = to_acc.id`) + .where(and(eq(transfers.id, id), eq(transfers.userId, userId))); + + if (!result) { + throw new NotFoundException(`Transfer with ID ${id} not found`); + } + + return { + ...result.transfer, + fromAccount: result.fromAccount, + toAccount: result.toAccount, + }; + } + + async create(userId: string, dto: CreateTransferDto) { + if (dto.fromAccountId === dto.toAccountId) { + throw new BadRequestException('Cannot transfer to the same account'); + } + + // Verify both accounts belong to user + await this.accountService.findOne(userId, dto.fromAccountId); + await this.accountService.findOne(userId, dto.toAccountId); + + const [transfer] = await this.db + .insert(transfers) + .values({ + userId, + fromAccountId: dto.fromAccountId, + toAccountId: dto.toAccountId, + amount: dto.amount.toString(), + date: dto.date, + description: dto.description, + }) + .returning(); + + // Update account balances + await this.accountService.updateBalance(userId, dto.fromAccountId, -dto.amount); + await this.accountService.updateBalance(userId, dto.toAccountId, dto.amount); + + return this.findOne(userId, transfer.id); + } + + async update(userId: string, id: string, dto: UpdateTransferDto) { + const original = await this.findOne(userId, id); + const originalAmount = parseFloat(original.amount); + + // Verify new accounts if provided + if (dto.fromAccountId) { + await this.accountService.findOne(userId, dto.fromAccountId); + } + if (dto.toAccountId) { + await this.accountService.findOne(userId, dto.toAccountId); + } + + const newFromAccountId = dto.fromAccountId ?? original.fromAccountId; + const newToAccountId = dto.toAccountId ?? original.toAccountId; + + if (newFromAccountId === newToAccountId) { + throw new BadRequestException('Cannot transfer to the same account'); + } + + const [transfer] = await this.db + .update(transfers) + .set({ + ...(dto.fromAccountId !== undefined && { fromAccountId: dto.fromAccountId }), + ...(dto.toAccountId !== undefined && { toAccountId: dto.toAccountId }), + ...(dto.amount !== undefined && { amount: dto.amount.toString() }), + ...(dto.date !== undefined && { date: dto.date }), + ...(dto.description !== undefined && { description: dto.description }), + updatedAt: new Date(), + }) + .where(and(eq(transfers.id, id), eq(transfers.userId, userId))) + .returning(); + + // Reverse original transfer + await this.accountService.updateBalance(userId, original.fromAccountId, originalAmount); + await this.accountService.updateBalance(userId, original.toAccountId, -originalAmount); + + // Apply new transfer + const newAmount = dto.amount ?? originalAmount; + await this.accountService.updateBalance(userId, newFromAccountId, -newAmount); + await this.accountService.updateBalance(userId, newToAccountId, newAmount); + + return this.findOne(userId, transfer.id); + } + + async delete(userId: string, id: string) { + const transfer = await this.findOne(userId, id); + const amount = parseFloat(transfer.amount); + + await this.db.delete(transfers).where(and(eq(transfers.id, id), eq(transfers.userId, userId))); + + // Reverse the transfer + await this.accountService.updateBalance(userId, transfer.fromAccountId, amount); + await this.accountService.updateBalance(userId, transfer.toAccountId, -amount); + + return { success: true }; + } +} diff --git a/apps/finance/apps/backend/tsconfig.json b/apps/finance/apps/backend/tsconfig.json new file mode 100644 index 000000000..323c27ece --- /dev/null +++ b/apps/finance/apps/backend/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/finance/apps/web/package.json b/apps/finance/apps/web/package.json new file mode 100644 index 000000000..76a98a46e --- /dev/null +++ b/apps/finance/apps/web/package.json @@ -0,0 +1,51 @@ +{ + "name": "@finance/web", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite dev --port 5189", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "eslint .", + "format": "prettier --write .", + "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/vite": "^4.1.7", + "@types/node": "^20.0.0", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.1.7", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^6.0.0" + }, + "dependencies": { + "@finance/shared": "workspace:*", + "@manacore/shared-auth": "workspace:*", + "@manacore/shared-auth-ui": "workspace:*", + "@manacore/shared-branding": "workspace:*", + "@manacore/shared-feedback-service": "workspace:*", + "@manacore/shared-feedback-ui": "workspace:*", + "@manacore/shared-i18n": "workspace:*", + "@manacore/shared-icons": "workspace:*", + "@manacore/shared-profile-ui": "workspace:*", + "@manacore/shared-subscription-ui": "workspace:*", + "@manacore/shared-tailwind": "workspace:*", + "@manacore/shared-theme": "workspace:*", + "@manacore/shared-theme-ui": "workspace:*", + "@manacore/shared-ui": "workspace:*", + "chart.js": "^4.4.7", + "date-fns": "^4.1.0", + "svelte-chartjs": "^3.1.5", + "svelte-i18n": "^4.0.1" + }, + "type": "module" +} diff --git a/apps/finance/apps/web/src/app.css b/apps/finance/apps/web/src/app.css new file mode 100644 index 000000000..65365e0d5 --- /dev/null +++ b/apps/finance/apps/web/src/app.css @@ -0,0 +1,180 @@ +@import 'tailwindcss'; +@import '@manacore/shared-tailwind/themes.css'; + +/* Scan shared packages for Tailwind classes */ +@source "../../../packages/shared/src"; +@source "../../../../../packages/shared-ui/src"; +@source "../../../../../packages/shared-theme-ui/src"; + +:root { + /* Finance App - Green/Emerald Theme */ + --color-primary: #10b981; + --color-primary-hover: #059669; + --color-primary-light: #34d399; + --color-primary-dark: #047857; + + --color-secondary: #ecfdf5; + --color-secondary-hover: #d1fae5; + + --color-accent: #6ee7b7; + --color-accent-hover: #34d399; + + /* Transaction types */ + --color-income: #22c55e; + --color-income-bg: #dcfce7; + --color-expense: #ef4444; + --color-expense-bg: #fee2e2; + --color-transfer: #3b82f6; + --color-transfer-bg: #dbeafe; + + /* Budget status */ + --color-budget-ok: #22c55e; + --color-budget-warning: #eab308; + --color-budget-danger: #ef4444; + --color-budget-over: #dc2626; + + /* Account types */ + --color-checking: #3b82f6; + --color-savings: #22c55e; + --color-credit-card: #f97316; + --color-cash: #8b5cf6; + --color-investment: #06b6d4; + --color-loan: #ef4444; +} + +/* Dark mode overrides */ +:root.dark { + --color-secondary: #064e3b; + --color-secondary-hover: #065f46; + --color-income-bg: #14532d; + --color-expense-bg: #7f1d1d; + --color-transfer-bg: #1e3a8a; +} + +/* Transaction item styling */ +.transaction-item { + transition: + transform 0.15s ease, + box-shadow 0.15s ease; +} + +.transaction-item:hover { + transform: translateY(-1px); +} + +/* Amount styling */ +.amount-income { + color: var(--color-income); +} + +.amount-expense { + color: var(--color-expense); +} + +.amount-transfer { + color: var(--color-transfer); +} + +/* Budget progress bar */ +.budget-progress { + height: 8px; + border-radius: 4px; + background-color: var(--color-secondary); + overflow: hidden; +} + +.budget-progress-bar { + height: 100%; + border-radius: 4px; + transition: width 0.3s ease; +} + +.budget-ok .budget-progress-bar { + background-color: var(--color-budget-ok); +} + +.budget-warning .budget-progress-bar { + background-color: var(--color-budget-warning); +} + +.budget-danger .budget-progress-bar { + background-color: var(--color-budget-danger); +} + +.budget-over .budget-progress-bar { + background-color: var(--color-budget-over); +} + +/* Account card */ +.account-card { + transition: + transform 0.15s ease, + box-shadow 0.15s ease; +} + +.account-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +/* Category chip */ +.category-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; +} + +/* Chart container */ +.chart-container { + position: relative; + width: 100%; + height: 300px; +} + +/* Currency input */ +.currency-input { + font-variant-numeric: tabular-nums; +} + +/* Date range picker */ +.date-range-picker { + display: flex; + gap: 8px; + align-items: center; +} + +/* Quick stats */ +.stat-card { + transition: + transform 0.15s ease, + box-shadow 0.15s ease; +} + +.stat-card:hover { + transform: translateY(-1px); +} + +/* Filter chips */ +.filter-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + border-radius: 9999px; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.filter-chip:hover { + background-color: var(--color-secondary-hover); +} + +.filter-chip.active { + background-color: var(--color-primary); + color: white; +} diff --git a/apps/finance/apps/web/src/app.html b/apps/finance/apps/web/src/app.html new file mode 100644 index 000000000..77a5ff52c --- /dev/null +++ b/apps/finance/apps/web/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/finance/apps/web/src/lib/api/accounts.ts b/apps/finance/apps/web/src/lib/api/accounts.ts new file mode 100644 index 000000000..aaf886fe7 --- /dev/null +++ b/apps/finance/apps/web/src/lib/api/accounts.ts @@ -0,0 +1,25 @@ +import { apiClient } from './client'; +import type { Account, CreateAccountInput, UpdateAccountInput } from '@finance/shared'; + +export const accountsApi = { + getAll: () => apiClient.get('/accounts'), + + getAllIncludingArchived: () => apiClient.get('/accounts/all'), + + getOne: (id: string) => apiClient.get(`/accounts/${id}`), + + getTotals: () => + apiClient.get<{ currency: string; total: number; count: number }[]>('/accounts/totals'), + + create: (data: CreateAccountInput) => apiClient.post('/accounts', data), + + update: (id: string, data: UpdateAccountInput) => apiClient.put(`/accounts/${id}`, data), + + delete: (id: string) => apiClient.delete<{ success: boolean }>(`/accounts/${id}`), + + archive: (id: string) => apiClient.post(`/accounts/${id}/archive`), + + unarchive: (id: string) => apiClient.post(`/accounts/${id}/unarchive`), + + reorder: (accountIds: string[]) => apiClient.put('/accounts/reorder', { accountIds }), +}; diff --git a/apps/finance/apps/web/src/lib/api/budgets.ts b/apps/finance/apps/web/src/lib/api/budgets.ts new file mode 100644 index 000000000..9ec841652 --- /dev/null +++ b/apps/finance/apps/web/src/lib/api/budgets.ts @@ -0,0 +1,43 @@ +import { apiClient } from './client'; +import type { Budget, CreateBudgetInput, UpdateBudgetInput } from '@finance/shared'; + +// Budget with computed spending fields from API +export interface BudgetWithSpending { + id: string; + userId: string; + categoryId: string | null; + month: number; + year: number; + amount: string; + alertThreshold: string; + rolloverEnabled: boolean; + createdAt: Date; + updatedAt: Date; + spent: number; + remaining: number; + percentage: number; + category?: { + id: string; + name: string; + color: string; + icon?: string; + } | null; +} + +export const budgetsApi = { + getAll: () => apiClient.get('/budgets'), + + getByMonth: (year: number, month: number) => + apiClient.get(`/budgets/month/${year}/${month}`), + + getOne: (id: string) => apiClient.get(`/budgets/${id}`), + + create: (data: CreateBudgetInput) => apiClient.post('/budgets', data), + + update: (id: string, data: UpdateBudgetInput) => apiClient.put(`/budgets/${id}`, data), + + delete: (id: string) => apiClient.delete<{ success: boolean }>(`/budgets/${id}`), + + copyFromPreviousMonth: (year: number, month: number) => + apiClient.post<{ message: string; copied: number }>('/budgets/copy', { year, month }), +}; diff --git a/apps/finance/apps/web/src/lib/api/categories.ts b/apps/finance/apps/web/src/lib/api/categories.ts new file mode 100644 index 000000000..5cb36a3ad --- /dev/null +++ b/apps/finance/apps/web/src/lib/api/categories.ts @@ -0,0 +1,30 @@ +import { apiClient } from './client'; +import type { + Category, + CreateCategoryInput, + UpdateCategoryInput, + CategoryType, +} from '@finance/shared'; + +export const categoriesApi = { + getAll: (type?: CategoryType) => { + const params = type ? `?type=${type}` : ''; + return apiClient.get(`/categories${params}`); + }, + + getAllIncludingArchived: () => apiClient.get('/categories/all'), + + getTree: () => apiClient.get<(Category & { children: Category[] })[]>('/categories/tree'), + + getOne: (id: string) => apiClient.get(`/categories/${id}`), + + create: (data: CreateCategoryInput) => apiClient.post('/categories', data), + + update: (id: string, data: UpdateCategoryInput) => + apiClient.put(`/categories/${id}`, data), + + delete: (id: string) => apiClient.delete<{ success: boolean }>(`/categories/${id}`), + + seed: () => + apiClient.post<{ message: string; seeded: boolean; count?: number }>('/categories/seed'), +}; diff --git a/apps/finance/apps/web/src/lib/api/client.ts b/apps/finance/apps/web/src/lib/api/client.ts new file mode 100644 index 000000000..ce722730c --- /dev/null +++ b/apps/finance/apps/web/src/lib/api/client.ts @@ -0,0 +1,61 @@ +import { PUBLIC_BACKEND_URL } from '$env/static/public'; + +class ApiClient { + private baseUrl: string; + private token: string | null = null; + + constructor() { + this.baseUrl = PUBLIC_BACKEND_URL || 'http://localhost:3019'; + } + + setToken(token: string | null) { + this.token = token; + } + + private async request( + method: string, + path: string, + body?: unknown, + options?: RequestInit + ): Promise { + const url = `${this.baseUrl}/api/v1${path}`; + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...(this.token && { Authorization: `Bearer ${this.token}` }), + ...options?.headers, + }; + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + ...options, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Request failed' })); + throw new Error(error.message || `HTTP ${response.status}`); + } + + return response.json(); + } + + get(path: string, options?: RequestInit): Promise { + return this.request('GET', path, undefined, options); + } + + post(path: string, body?: unknown, options?: RequestInit): Promise { + return this.request('POST', path, body, options); + } + + put(path: string, body?: unknown, options?: RequestInit): Promise { + return this.request('PUT', path, body, options); + } + + delete(path: string, options?: RequestInit): Promise { + return this.request('DELETE', path, undefined, options); + } +} + +export const apiClient = new ApiClient(); diff --git a/apps/finance/apps/web/src/lib/api/exchange-rates.ts b/apps/finance/apps/web/src/lib/api/exchange-rates.ts new file mode 100644 index 000000000..15c92e9c2 --- /dev/null +++ b/apps/finance/apps/web/src/lib/api/exchange-rates.ts @@ -0,0 +1,25 @@ +import { apiClient } from './client'; + +interface ExchangeRate { + fromCurrency: string; + toCurrency: string; + rate: number; + date: string; +} + +export const exchangeRatesApi = { + getAll: (baseCurrency = 'EUR') => + apiClient.get(`/exchange-rates?base=${baseCurrency}`), + + getRate: (fromCurrency: string, toCurrency: string) => + apiClient.get(`/exchange-rates/rate?from=${fromCurrency}&to=${toCurrency}`), + + convert: (amount: number, fromCurrency: string, toCurrency: string) => + apiClient.get( + `/exchange-rates/convert?amount=${amount}&from=${fromCurrency}&to=${toCurrency}` + ), + + seed: () => apiClient.post<{ message: string; seeded: boolean }>('/exchange-rates/seed'), + + fetch: () => apiClient.post('/exchange-rates/fetch'), +}; diff --git a/apps/finance/apps/web/src/lib/api/index.ts b/apps/finance/apps/web/src/lib/api/index.ts new file mode 100644 index 000000000..dfe7871cc --- /dev/null +++ b/apps/finance/apps/web/src/lib/api/index.ts @@ -0,0 +1,9 @@ +export { apiClient } from './client'; +export { accountsApi } from './accounts'; +export { categoriesApi } from './categories'; +export { transactionsApi } from './transactions'; +export { budgetsApi } from './budgets'; +export { transfersApi } from './transfers'; +export { reportsApi } from './reports'; +export { settingsApi } from './settings'; +export { exchangeRatesApi } from './exchange-rates'; diff --git a/apps/finance/apps/web/src/lib/api/reports.ts b/apps/finance/apps/web/src/lib/api/reports.ts new file mode 100644 index 000000000..7245122c2 --- /dev/null +++ b/apps/finance/apps/web/src/lib/api/reports.ts @@ -0,0 +1,89 @@ +import { apiClient } from './client'; +import type { DashboardData, MonthlySummary, CategoryBreakdown, TrendData } from '@finance/shared'; + +interface Dashboard { + totals: { currency: string; amount: number }[]; + currentMonth: { + year: number; + month: number; + income: number; + expense: number; + net: number; + }; + budgets: { + id: string; + category: { id: string; name: string; color: string } | null; + amount: number; + spent: number; + percentage: number; + }[]; + recentTransactions: unknown[]; +} + +interface CategoryBreakdownResponse { + startDate: string; + endDate: string; + type: string; + total: number; + categories: { + categoryId: string | null; + name: string; + color: string | null; + icon: string | null; + amount: number; + count: number; + percentage: number; + }[]; +} + +interface TrendsResponse { + months: number; + data: { + year: number; + month: number; + income: number; + expense: number; + net: number; + }[]; + averages: { + income: number; + expense: number; + net: number; + }; +} + +interface CashFlowResponse { + startDate: string; + endDate: string; + startingBalance: number; + endingBalance: number; + data: { + date: string; + balance: number; + income: number; + expense: number; + }[]; +} + +export const reportsApi = { + getDashboard: () => apiClient.get('/reports/dashboard'), + + getMonthlySummary: (year?: number, month?: number) => { + const params = new URLSearchParams(); + if (year) params.append('year', String(year)); + if (month) params.append('month', String(month)); + const query = params.toString(); + return apiClient.get(`/reports/monthly-summary${query ? `?${query}` : ''}`); + }, + + getCategoryBreakdown: (startDate: string, endDate: string, type?: 'income' | 'expense') => { + const params = new URLSearchParams({ startDate, endDate }); + if (type) params.append('type', type); + return apiClient.get(`/reports/category-breakdown?${params}`); + }, + + getTrends: (months = 6) => apiClient.get(`/reports/trends?months=${months}`), + + getCashFlow: (startDate: string, endDate: string) => + apiClient.get(`/reports/cash-flow?startDate=${startDate}&endDate=${endDate}`), +}; diff --git a/apps/finance/apps/web/src/lib/api/settings.ts b/apps/finance/apps/web/src/lib/api/settings.ts new file mode 100644 index 000000000..92c74340c --- /dev/null +++ b/apps/finance/apps/web/src/lib/api/settings.ts @@ -0,0 +1,8 @@ +import { apiClient } from './client'; +import type { UserSettings, UpdateUserSettingsInput } from '@finance/shared'; + +export const settingsApi = { + get: () => apiClient.get('/settings'), + + update: (data: UpdateUserSettingsInput) => apiClient.put('/settings', data), +}; diff --git a/apps/finance/apps/web/src/lib/api/transactions.ts b/apps/finance/apps/web/src/lib/api/transactions.ts new file mode 100644 index 000000000..bca03f904 --- /dev/null +++ b/apps/finance/apps/web/src/lib/api/transactions.ts @@ -0,0 +1,49 @@ +import { apiClient } from './client'; +import type { + Transaction, + CreateTransactionInput, + UpdateTransactionInput, + TransactionFilters, +} from '@finance/shared'; + +interface PaginatedTransactions { + data: Transaction[]; + total: number; + limit: number; + offset: number; +} + +export const transactionsApi = { + getAll: (filters?: TransactionFilters) => { + const params = new URLSearchParams(); + if (filters) { + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + params.append(key, String(value)); + } + }); + } + const query = params.toString(); + return apiClient.get(`/transactions${query ? `?${query}` : ''}`); + }, + + getRecent: (limit = 10) => apiClient.get(`/transactions/recent?limit=${limit}`), + + getSummary: (startDate: string, endDate: string) => + apiClient.get<{ + income: number; + expense: number; + net: number; + incomeCount: number; + expenseCount: number; + }>(`/transactions/summary?startDate=${startDate}&endDate=${endDate}`), + + getOne: (id: string) => apiClient.get(`/transactions/${id}`), + + create: (data: CreateTransactionInput) => apiClient.post('/transactions', data), + + update: (id: string, data: UpdateTransactionInput) => + apiClient.put(`/transactions/${id}`, data), + + delete: (id: string) => apiClient.delete<{ success: boolean }>(`/transactions/${id}`), +}; diff --git a/apps/finance/apps/web/src/lib/api/transfers.ts b/apps/finance/apps/web/src/lib/api/transfers.ts new file mode 100644 index 000000000..b4f2b825a --- /dev/null +++ b/apps/finance/apps/web/src/lib/api/transfers.ts @@ -0,0 +1,15 @@ +import { apiClient } from './client'; +import type { Transfer, CreateTransferInput, UpdateTransferInput } from '@finance/shared'; + +export const transfersApi = { + getAll: () => apiClient.get('/transfers'), + + getOne: (id: string) => apiClient.get(`/transfers/${id}`), + + create: (data: CreateTransferInput) => apiClient.post('/transfers', data), + + update: (id: string, data: UpdateTransferInput) => + apiClient.put(`/transfers/${id}`, data), + + delete: (id: string) => apiClient.delete<{ success: boolean }>(`/transfers/${id}`), +}; diff --git a/apps/finance/apps/web/src/lib/components/AppSlider.svelte b/apps/finance/apps/web/src/lib/components/AppSlider.svelte new file mode 100644 index 000000000..6de040fe6 --- /dev/null +++ b/apps/finance/apps/web/src/lib/components/AppSlider.svelte @@ -0,0 +1,76 @@ + + + + +{#if isOpen} + + + + +
+
+
+

ManaCore Apps

+ +
+ +
+ {#each apps as app} + +
+ {app.name} +
+ {app.name} +
+ {/each} +
+
+
+{/if} diff --git a/apps/finance/apps/web/src/lib/components/LanguageSelector.svelte b/apps/finance/apps/web/src/lib/components/LanguageSelector.svelte new file mode 100644 index 000000000..9247dcc7c --- /dev/null +++ b/apps/finance/apps/web/src/lib/components/LanguageSelector.svelte @@ -0,0 +1,53 @@ + + + + +
+ + + {#if isOpen} +
+ {#each languages as lang} + + {/each} +
+ {/if} +
diff --git a/apps/finance/apps/web/src/lib/stores/accounts.svelte.ts b/apps/finance/apps/web/src/lib/stores/accounts.svelte.ts new file mode 100644 index 000000000..73f773509 --- /dev/null +++ b/apps/finance/apps/web/src/lib/stores/accounts.svelte.ts @@ -0,0 +1,113 @@ +import { accountsApi } from '$lib/api'; +import type { Account, CreateAccountInput, UpdateAccountInput } from '@finance/shared'; + +let accounts = $state([]); +let isLoading = $state(false); +let error = $state(null); + +export const accountsStore = { + get accounts() { + return accounts; + }, + get isLoading() { + return isLoading; + }, + get error() { + return error; + }, + + get activeAccounts() { + return accounts.filter((a) => !a.isArchived); + }, + + get totalByCurrency() { + const totals: Record = {}; + for (const account of accounts.filter((a) => !a.isArchived && a.includeInTotal)) { + const balance = parseFloat(account.balance); + const adjustedBalance = + account.type === 'credit_card' || account.type === 'loan' ? -Math.abs(balance) : balance; + totals[account.currency] = (totals[account.currency] || 0) + adjustedBalance; + } + return totals; + }, + + async fetchAccounts() { + isLoading = true; + error = null; + try { + accounts = await accountsApi.getAll(); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to fetch accounts'; + } finally { + isLoading = false; + } + }, + + async createAccount(data: CreateAccountInput) { + isLoading = true; + error = null; + try { + const newAccount = await accountsApi.create(data); + accounts = [...accounts, newAccount]; + return newAccount; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to create account'; + throw e; + } finally { + isLoading = false; + } + }, + + async updateAccount(id: string, data: UpdateAccountInput) { + isLoading = true; + error = null; + try { + const updated = await accountsApi.update(id, data); + accounts = accounts.map((a) => (a.id === id ? updated : a)); + return updated; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to update account'; + throw e; + } finally { + isLoading = false; + } + }, + + async deleteAccount(id: string) { + isLoading = true; + error = null; + try { + await accountsApi.delete(id); + accounts = accounts.filter((a) => a.id !== id); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to delete account'; + throw e; + } finally { + isLoading = false; + } + }, + + async archiveAccount(id: string) { + try { + const updated = await accountsApi.archive(id); + accounts = accounts.map((a) => (a.id === id ? updated : a)); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to archive account'; + throw e; + } + }, + + async unarchiveAccount(id: string) { + try { + const updated = await accountsApi.unarchive(id); + accounts = accounts.map((a) => (a.id === id ? updated : a)); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to unarchive account'; + throw e; + } + }, + + getAccountById(id: string) { + return accounts.find((a) => a.id === id); + }, +}; diff --git a/apps/finance/apps/web/src/lib/stores/auth.svelte.ts b/apps/finance/apps/web/src/lib/stores/auth.svelte.ts new file mode 100644 index 000000000..6e12c1a84 --- /dev/null +++ b/apps/finance/apps/web/src/lib/stores/auth.svelte.ts @@ -0,0 +1,61 @@ +import { apiClient } from '$lib/api'; + +interface User { + id: string; + email: string; + name?: string; +} + +let user = $state(null); +let token = $state(null); +let isLoading = $state(true); + +export const authStore = { + get user() { + return user; + }, + get token() { + return token; + }, + get isLoading() { + return isLoading; + }, + get isAuthenticated() { + return !!user && !!token; + }, + + setToken(newToken: string | null) { + token = newToken; + apiClient.setToken(newToken); + + if (newToken && typeof window !== 'undefined') { + localStorage.setItem('finance_token', newToken); + } else if (typeof window !== 'undefined') { + localStorage.removeItem('finance_token'); + } + }, + + setUser(newUser: User | null) { + user = newUser; + }, + + async init() { + if (typeof window === 'undefined') { + isLoading = false; + return; + } + + const savedToken = localStorage.getItem('finance_token'); + if (savedToken) { + this.setToken(savedToken); + // TODO: Validate token with backend + } + + isLoading = false; + }, + + logout() { + this.setToken(null); + this.setUser(null); + }, +}; diff --git a/apps/finance/apps/web/src/lib/stores/budgets.svelte.ts b/apps/finance/apps/web/src/lib/stores/budgets.svelte.ts new file mode 100644 index 000000000..cba4892c9 --- /dev/null +++ b/apps/finance/apps/web/src/lib/stores/budgets.svelte.ts @@ -0,0 +1,110 @@ +import { budgetsApi, type BudgetWithSpending } from '$lib/api/budgets'; +import type { CreateBudgetInput, UpdateBudgetInput } from '@finance/shared'; + +let budgets = $state([]); +let isLoading = $state(false); +let error = $state(null); +let selectedMonth = $state(new Date().getMonth() + 1); +let selectedYear = $state(new Date().getFullYear()); + +export const budgetsStore = { + get budgets() { + return budgets; + }, + get isLoading() { + return isLoading; + }, + get error() { + return error; + }, + get selectedMonth() { + return selectedMonth; + }, + get selectedYear() { + return selectedYear; + }, + + get totalBudgeted() { + return budgets.reduce((sum, b) => sum + parseFloat(b.amount), 0); + }, + + get totalSpent() { + return budgets.reduce((sum, b) => sum + b.spent, 0); + }, + + get overBudgetCount() { + return budgets.filter((b) => b.percentage >= 1).length; + }, + + setMonth(month: number, year: number) { + selectedMonth = month; + selectedYear = year; + }, + + async fetchBudgets() { + isLoading = true; + error = null; + try { + budgets = await budgetsApi.getByMonth(selectedYear, selectedMonth); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to fetch budgets'; + } finally { + isLoading = false; + } + }, + + async createBudget(data: CreateBudgetInput) { + isLoading = true; + error = null; + try { + await budgetsApi.create(data); + await this.fetchBudgets(); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to create budget'; + throw e; + } finally { + isLoading = false; + } + }, + + async updateBudget(id: string, data: UpdateBudgetInput) { + isLoading = true; + error = null; + try { + await budgetsApi.update(id, data); + await this.fetchBudgets(); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to update budget'; + throw e; + } finally { + isLoading = false; + } + }, + + async deleteBudget(id: string) { + isLoading = true; + error = null; + try { + await budgetsApi.delete(id); + budgets = budgets.filter((b) => b.id !== id); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to delete budget'; + throw e; + } finally { + isLoading = false; + } + }, + + async copyFromPreviousMonth() { + try { + const result = await budgetsApi.copyFromPreviousMonth(selectedYear, selectedMonth); + if (result.copied > 0) { + await this.fetchBudgets(); + } + return result; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to copy budgets'; + throw e; + } + }, +}; diff --git a/apps/finance/apps/web/src/lib/stores/categories.svelte.ts b/apps/finance/apps/web/src/lib/stores/categories.svelte.ts new file mode 100644 index 000000000..df2569bdb --- /dev/null +++ b/apps/finance/apps/web/src/lib/stores/categories.svelte.ts @@ -0,0 +1,104 @@ +import { categoriesApi } from '$lib/api'; +import type { + Category, + CreateCategoryInput, + UpdateCategoryInput, + CategoryType, +} from '@finance/shared'; + +let categories = $state([]); +let isLoading = $state(false); +let error = $state(null); + +export const categoriesStore = { + get categories() { + return categories; + }, + get isLoading() { + return isLoading; + }, + get error() { + return error; + }, + + get expenseCategories() { + return categories.filter((c) => c.type === 'expense' && !c.isArchived); + }, + + get incomeCategories() { + return categories.filter((c) => c.type === 'income' && !c.isArchived); + }, + + async fetchCategories(type?: CategoryType) { + isLoading = true; + error = null; + try { + categories = await categoriesApi.getAll(type); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to fetch categories'; + } finally { + isLoading = false; + } + }, + + async createCategory(data: CreateCategoryInput) { + isLoading = true; + error = null; + try { + const newCategory = await categoriesApi.create(data); + categories = [...categories, newCategory]; + return newCategory; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to create category'; + throw e; + } finally { + isLoading = false; + } + }, + + async updateCategory(id: string, data: UpdateCategoryInput) { + isLoading = true; + error = null; + try { + const updated = await categoriesApi.update(id, data); + categories = categories.map((c) => (c.id === id ? updated : c)); + return updated; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to update category'; + throw e; + } finally { + isLoading = false; + } + }, + + async deleteCategory(id: string) { + isLoading = true; + error = null; + try { + await categoriesApi.delete(id); + categories = categories.filter((c) => c.id !== id); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to delete category'; + throw e; + } finally { + isLoading = false; + } + }, + + async seedCategories() { + try { + const result = await categoriesApi.seed(); + if (result.seeded) { + await this.fetchCategories(); + } + return result; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to seed categories'; + throw e; + } + }, + + getCategoryById(id: string) { + return categories.find((c) => c.id === id); + }, +}; diff --git a/apps/finance/apps/web/src/lib/stores/dashboard.svelte.ts b/apps/finance/apps/web/src/lib/stores/dashboard.svelte.ts new file mode 100644 index 000000000..3573f3ef4 --- /dev/null +++ b/apps/finance/apps/web/src/lib/stores/dashboard.svelte.ts @@ -0,0 +1,67 @@ +import { reportsApi } from '$lib/api'; + +interface DashboardData { + totals: { currency: string; amount: number }[]; + currentMonth: { + year: number; + month: number; + income: number; + expense: number; + net: number; + }; + budgets: { + id: string; + category: { id: string; name: string; color: string } | null; + amount: number; + spent: number; + percentage: number; + }[]; + recentTransactions: unknown[]; +} + +let data = $state(null); +let isLoading = $state(false); +let error = $state(null); + +export const dashboardStore = { + get data() { + return data; + }, + get isLoading() { + return isLoading; + }, + get error() { + return error; + }, + + get primaryTotal() { + if (!data?.totals?.length) return 0; + // Return EUR total if available, otherwise first currency + const eurTotal = data.totals.find((t) => t.currency === 'EUR'); + return eurTotal?.amount ?? data.totals[0]?.amount ?? 0; + }, + + get monthlyNet() { + return data?.currentMonth?.net ?? 0; + }, + + get budgetProgress() { + return data?.budgets ?? []; + }, + + async fetchDashboard() { + isLoading = true; + error = null; + try { + data = await reportsApi.getDashboard(); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to fetch dashboard'; + } finally { + isLoading = false; + } + }, + + async refresh() { + await this.fetchDashboard(); + }, +}; diff --git a/apps/finance/apps/web/src/lib/stores/index.ts b/apps/finance/apps/web/src/lib/stores/index.ts new file mode 100644 index 000000000..82757bdea --- /dev/null +++ b/apps/finance/apps/web/src/lib/stores/index.ts @@ -0,0 +1,7 @@ +export { authStore } from './auth.svelte'; +export { accountsStore } from './accounts.svelte'; +export { categoriesStore } from './categories.svelte'; +export { transactionsStore } from './transactions.svelte'; +export { budgetsStore } from './budgets.svelte'; +export { dashboardStore } from './dashboard.svelte'; +export { settingsStore } from './settings.svelte'; diff --git a/apps/finance/apps/web/src/lib/stores/settings.svelte.ts b/apps/finance/apps/web/src/lib/stores/settings.svelte.ts new file mode 100644 index 000000000..976beb85f --- /dev/null +++ b/apps/finance/apps/web/src/lib/stores/settings.svelte.ts @@ -0,0 +1,67 @@ +import { settingsApi } from '$lib/api'; +import type { UserSettings, UpdateUserSettingsInput } from '@finance/shared'; + +const DEFAULT_SETTINGS: UserSettings = { + id: '', + userId: '', + defaultCurrency: 'EUR', + locale: 'de-DE', + dateFormat: 'dd.MM.yyyy', + weekStartsOn: 1, + createdAt: new Date(), + updatedAt: new Date(), +}; + +let settings = $state(DEFAULT_SETTINGS); +let isLoading = $state(false); +let error = $state(null); + +export const settingsStore = { + get settings() { + return settings; + }, + get isLoading() { + return isLoading; + }, + get error() { + return error; + }, + + get currency() { + return settings.defaultCurrency; + }, + + get locale() { + return settings.locale; + }, + + get dateFormat() { + return settings.dateFormat; + }, + + async fetchSettings() { + isLoading = true; + error = null; + try { + settings = await settingsApi.get(); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to fetch settings'; + } finally { + isLoading = false; + } + }, + + async updateSettings(data: UpdateUserSettingsInput) { + isLoading = true; + error = null; + try { + settings = await settingsApi.update(data); + return settings; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to update settings'; + throw e; + } finally { + isLoading = false; + } + }, +}; diff --git a/apps/finance/apps/web/src/lib/stores/transactions.svelte.ts b/apps/finance/apps/web/src/lib/stores/transactions.svelte.ts new file mode 100644 index 000000000..c794cc6af --- /dev/null +++ b/apps/finance/apps/web/src/lib/stores/transactions.svelte.ts @@ -0,0 +1,112 @@ +import { transactionsApi } from '$lib/api'; +import type { + Transaction, + CreateTransactionInput, + UpdateTransactionInput, + TransactionFilters, +} from '@finance/shared'; + +let transactions = $state([]); +let total = $state(0); +let isLoading = $state(false); +let error = $state(null); +let filters = $state({}); + +export const transactionsStore = { + get transactions() { + return transactions; + }, + get total() { + return total; + }, + get isLoading() { + return isLoading; + }, + get error() { + return error; + }, + get filters() { + return filters; + }, + + setFilters(newFilters: TransactionFilters) { + filters = { ...filters, ...newFilters }; + }, + + clearFilters() { + filters = {}; + }, + + async fetchTransactions(customFilters?: TransactionFilters) { + isLoading = true; + error = null; + try { + const result = await transactionsApi.getAll(customFilters ?? filters); + transactions = result.data; + total = result.total; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to fetch transactions'; + } finally { + isLoading = false; + } + }, + + async fetchRecent(limit = 10) { + try { + return await transactionsApi.getRecent(limit); + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to fetch recent transactions'; + throw e; + } + }, + + async createTransaction(data: CreateTransactionInput) { + isLoading = true; + error = null; + try { + const newTransaction = await transactionsApi.create(data); + transactions = [newTransaction, ...transactions]; + total += 1; + return newTransaction; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to create transaction'; + throw e; + } finally { + isLoading = false; + } + }, + + async updateTransaction(id: string, data: UpdateTransactionInput) { + isLoading = true; + error = null; + try { + const updated = await transactionsApi.update(id, data); + transactions = transactions.map((t) => (t.id === id ? updated : t)); + return updated; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to update transaction'; + throw e; + } finally { + isLoading = false; + } + }, + + async deleteTransaction(id: string) { + isLoading = true; + error = null; + try { + await transactionsApi.delete(id); + transactions = transactions.filter((t) => t.id !== id); + total -= 1; + } catch (e) { + error = e instanceof Error ? e.message : 'Failed to delete transaction'; + throw e; + } finally { + isLoading = false; + } + }, + + getTransactionById(id: string) { + return transactions.find((t) => t.id === id); + }, +}; diff --git a/apps/finance/apps/web/src/routes/(auth)/forgot-password/+page.svelte b/apps/finance/apps/web/src/routes/(auth)/forgot-password/+page.svelte new file mode 100644 index 000000000..ecaacc557 --- /dev/null +++ b/apps/finance/apps/web/src/routes/(auth)/forgot-password/+page.svelte @@ -0,0 +1,75 @@ + + + + Passwort vergessen | Finance + + +
+
+
+

Passwort vergessen

+

+ Geben Sie Ihre E-Mail-Adresse ein, um Ihr Passwort zurückzusetzen +

+
+ + {#if success} +
+

E-Mail gesendet!

+

Überprüfen Sie Ihren Posteingang für weitere Anweisungen.

+
+ {:else} +
+
+ + +
+ + {#if error} +
{error}
+ {/if} + + +
+ {/if} + + +
+
diff --git a/apps/finance/apps/web/src/routes/(auth)/login/+page.svelte b/apps/finance/apps/web/src/routes/(auth)/login/+page.svelte new file mode 100644 index 000000000..5151d3f40 --- /dev/null +++ b/apps/finance/apps/web/src/routes/(auth)/login/+page.svelte @@ -0,0 +1,85 @@ + + + + Login | Finance + + +
+
+
+

Anmelden

+

Melden Sie sich bei Finance an

+
+ +
+
+ + +
+ +
+ + +
+ + {#if error} +
{error}
+ {/if} + + +
+ + + +
+ Noch kein Konto? Registrieren +
+
+
diff --git a/apps/finance/apps/web/src/routes/(auth)/register/+page.svelte b/apps/finance/apps/web/src/routes/(auth)/register/+page.svelte new file mode 100644 index 000000000..18be54c15 --- /dev/null +++ b/apps/finance/apps/web/src/routes/(auth)/register/+page.svelte @@ -0,0 +1,113 @@ + + + + Registrieren | Finance + + +
+
+
+

Registrieren

+

Erstellen Sie ein neues Konto

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + {#if error} +
{error}
+ {/if} + + +
+ +
+ Bereits ein Konto? Anmelden +
+
+
diff --git a/apps/finance/apps/web/src/routes/+layout.svelte b/apps/finance/apps/web/src/routes/+layout.svelte new file mode 100644 index 000000000..00ab8dabb --- /dev/null +++ b/apps/finance/apps/web/src/routes/+layout.svelte @@ -0,0 +1,186 @@ + + +
+ +
+
+
+ + + + + + + + Finance + +
+ + + +
+ + +
+
+
+ + + + + +
+ {@render children()} +
+
+ + diff --git a/apps/finance/apps/web/src/routes/+page.svelte b/apps/finance/apps/web/src/routes/+page.svelte new file mode 100644 index 000000000..b90f3e0b4 --- /dev/null +++ b/apps/finance/apps/web/src/routes/+page.svelte @@ -0,0 +1,204 @@ + + + + Dashboard | Finance + + +
+

Dashboard

+ + {#if dashboardStore.isLoading} +
+
+
+ {:else if dashboardStore.error} +
{dashboardStore.error}
+ {:else if dashboardStore.data} + +
+ +
+

Gesamtvermögen

+

{formatCurrency(dashboardStore.primaryTotal)}

+ {#if dashboardStore.data.totals.length > 1} +
+ {#each dashboardStore.data.totals as total} + {#if total.currency !== 'EUR'} + {formatCurrency(total.amount, total.currency)} + {/if} + {/each} +
+ {/if} +
+ + +
+

+ Einnahmen ({dashboardStore.data.currentMonth.month}/{dashboardStore.data.currentMonth + .year}) +

+

+ {formatCurrency(dashboardStore.data.currentMonth.income)} +

+
+ + +
+

+ Ausgaben ({dashboardStore.data.currentMonth.month}/{dashboardStore.data.currentMonth + .year}) +

+

+ {formatCurrency(dashboardStore.data.currentMonth.expense)} +

+
+ + +
+

+ Netto ({dashboardStore.data.currentMonth.month}/{dashboardStore.data.currentMonth.year}) +

+

+ {formatCurrency(dashboardStore.monthlyNet)} +

+
+
+ +
+ +
+
+

Konten

+ Alle anzeigen +
+ {#if accountsStore.activeAccounts.length === 0} +

Noch keine Konten vorhanden.

+ Konto erstellen + {:else} +
+ {#each accountsStore.activeAccounts.slice(0, 5) as account} +
+
+
+ {account.name.charAt(0)} +
+
+

{account.name}

+

{account.type}

+
+
+

+ {formatCurrency(account.balance, account.currency)} +

+
+ {/each} +
+ {/if} +
+ + +
+
+

Budget-Fortschritt

+ Alle anzeigen +
+ {#if dashboardStore.budgetProgress.length === 0} +

Noch keine Budgets definiert.

+ Budget erstellen + {:else} +
+ {#each dashboardStore.budgetProgress.slice(0, 4) as budget} +
+
+ {budget.category?.name ?? 'Gesamt'} + {formatCurrency(budget.spent)} / {formatCurrency(budget.amount)} +
+
+
+
+
+ {/each} +
+ {/if} +
+
+ + +
+
+

Letzte Transaktionen

+ Alle anzeigen +
+ {#if dashboardStore.data.recentTransactions.length === 0} +

Noch keine Transaktionen vorhanden.

+ Transaktion erstellen + {:else} +
+ {#each dashboardStore.data.recentTransactions as transaction} +
+
+
+ {(transaction as any).category?.name?.charAt(0) ?? '?'} +
+
+

+ {(transaction as any).description || + (transaction as any).payee || + 'Keine Beschreibung'} +

+

{(transaction as any).date}

+
+
+

+ {(transaction as any).type === 'income' ? '+' : '-'}{formatCurrency( + (transaction as any).amount + )} +

+
+ {/each} +
+ {/if} +
+ {/if} +
diff --git a/apps/finance/apps/web/src/routes/accounts/+page.svelte b/apps/finance/apps/web/src/routes/accounts/+page.svelte new file mode 100644 index 000000000..0f82ecc4f --- /dev/null +++ b/apps/finance/apps/web/src/routes/accounts/+page.svelte @@ -0,0 +1,124 @@ + + + + Konten | Finance + + +
+
+

Konten

+
+ + + + Neues Konto + +
+
+ + +
+ {#each Object.entries(accountsStore.totalByCurrency) as [currency, total]} +
+

Gesamtsaldo ({currency})

+

+ {formatCurrency(total, currency)} +

+
+ {/each} +
+ + + {#if accountsStore.isLoading} +
+
+
+ {:else if accountsStore.error} +
{accountsStore.error}
+ {:else if displayedAccounts.length === 0} +
+

Noch keine Konten vorhanden.

+ + Erstes Konto erstellen + +
+ {:else} + {#each Object.entries(accountsByType()) as [type, accounts]} +
+

+ {ACCOUNT_TYPE_LABELS[type as keyof typeof ACCOUNT_TYPE_LABELS]?.de ?? type} +

+ +
+ {/each} + {/if} +
diff --git a/apps/finance/apps/web/src/routes/budgets/+page.svelte b/apps/finance/apps/web/src/routes/budgets/+page.svelte new file mode 100644 index 000000000..0e301e142 --- /dev/null +++ b/apps/finance/apps/web/src/routes/budgets/+page.svelte @@ -0,0 +1,184 @@ + + + + Budgets | Finance + + +
+
+

Budgets

+ + + Neues Budget + +
+ + +
+ + + {months[budgetsStore.selectedMonth - 1]} + {budgetsStore.selectedYear} + + +
+ + +
+
+

Budgetiert

+

{formatCurrency(budgetsStore.totalBudgeted)}

+
+
+

Ausgegeben

+

{formatCurrency(budgetsStore.totalSpent)}

+
+
+

Verbleibend

+

+ {formatCurrency(budgetsStore.totalBudgeted - budgetsStore.totalSpent)} +

+
+
+ + {#if budgetsStore.isLoading} +
+
+
+ {:else if budgetsStore.error} +
{budgetsStore.error}
+ {:else if budgetsStore.budgets.length === 0} +
+

Keine Budgets für diesen Monat definiert.

+
+ + + Budget erstellen + +
+
+ {:else} +
+ {#each budgetsStore.budgets as budget} +
+
+
+ {#if budget.category} +
+ {budget.category.name.charAt(0)} +
+ {budget.category.name} + {:else} +
+ G +
+ Gesamtbudget + {/if} +
+
+

+ {formatCurrency(budget.spent)} / {formatCurrency(budget.amount)} +

+

+ {budget.remaining >= 0 + ? `${formatCurrency(budget.remaining)} übrig` + : `${formatCurrency(Math.abs(budget.remaining))} über Budget`} +

+
+
+
+
+
+

+ {Math.round(budget.percentage * 100)}% +

+
+ {/each} +
+ {/if} +
diff --git a/apps/finance/apps/web/src/routes/categories/+page.svelte b/apps/finance/apps/web/src/routes/categories/+page.svelte new file mode 100644 index 000000000..dd6e99295 --- /dev/null +++ b/apps/finance/apps/web/src/routes/categories/+page.svelte @@ -0,0 +1,110 @@ + + + + Kategorien | Finance + + +
+
+

Kategorien

+
+ {#if categoriesStore.categories.length === 0} + + {/if} + + + Neue Kategorie + +
+
+ + {#if categoriesStore.isLoading} +
+
+
+ {:else if categoriesStore.error} +
{categoriesStore.error}
+ {:else} +
+ +
+

Ausgaben

+ {#if categoriesStore.expenseCategories.length === 0} +

Keine Ausgaben-Kategorien vorhanden.

+ {:else} +
+ {#each categoriesStore.expenseCategories as category} + +
+ {category.name.charAt(0)} +
+
+

{category.name}

+ {#if category.isSystem} + Standard + {/if} +
+
+ {/each} +
+ {/if} +
+ + +
+

Einnahmen

+ {#if categoriesStore.incomeCategories.length === 0} +

Keine Einnahmen-Kategorien vorhanden.

+ {:else} +
+ {#each categoriesStore.incomeCategories as category} + +
+ {category.name.charAt(0)} +
+
+

{category.name}

+ {#if category.isSystem} + Standard + {/if} +
+
+ {/each} +
+ {/if} +
+
+ {/if} +
diff --git a/apps/finance/apps/web/src/routes/feedback/+page.svelte b/apps/finance/apps/web/src/routes/feedback/+page.svelte new file mode 100644 index 000000000..30054e9ed --- /dev/null +++ b/apps/finance/apps/web/src/routes/feedback/+page.svelte @@ -0,0 +1,124 @@ + + + + Feedback | Finance + + +
+

Feedback

+ + {#if success} +
+
+

Vielen Dank für Ihr Feedback!

+

Wir werden uns Ihre Nachricht ansehen.

+ + Zurück zur Startseite + +
+ {:else} +
+
+

Was möchten Sie uns mitteilen?

+ +
+ + + +
+ +
+
+ + +
+ +
+ + +

+ Falls wir Rückfragen haben oder Sie über Updates informieren möchten +

+
+
+
+ + {#if error} +
{error}
+ {/if} + +
+ +
+
+ {/if} +
diff --git a/apps/finance/apps/web/src/routes/reports/+page.svelte b/apps/finance/apps/web/src/routes/reports/+page.svelte new file mode 100644 index 000000000..11d3c297a --- /dev/null +++ b/apps/finance/apps/web/src/routes/reports/+page.svelte @@ -0,0 +1,129 @@ + + + + Berichte | Finance + + +
+

Berichte

+ + + + + + {#if isLoading} +
+
+
+ {:else if error} +
{error}
+ {:else} +
+

Letzte 6 Monate

+ + +
+ {#each trends as month} +
+
+ {months[month.month - 1]} {month.year} + Netto: = 0 ? 'text-green-500' : 'text-red-500'} + >{formatCurrency(month.net)} +
+
+
+
+
+
+ {formatCurrency(month.income)} +
+
+
+
+
+ {formatCurrency(month.expense)} +
+
+
+ {/each} +
+ +
+ Einnahmen + Ausgaben +
+
+ {/if} +
diff --git a/apps/finance/apps/web/src/routes/settings/+page.svelte b/apps/finance/apps/web/src/routes/settings/+page.svelte new file mode 100644 index 000000000..cb4c83e34 --- /dev/null +++ b/apps/finance/apps/web/src/routes/settings/+page.svelte @@ -0,0 +1,146 @@ + + + + Einstellungen | Finance + + +
+

Einstellungen

+ + {#if settingsStore.isLoading} +
+
+
+ {:else} +
+
+

Regionale Einstellungen

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + {#if settingsStore.error} +
{settingsStore.error}
+ {/if} + + {#if successMessage} +
{successMessage}
+ {/if} + +
+ +
+
+ + +
+

Gefahrenzone

+

+ Diese Aktionen können nicht rückgängig gemacht werden. +

+ +
+ {/if} +
diff --git a/apps/finance/apps/web/src/routes/transactions/+page.svelte b/apps/finance/apps/web/src/routes/transactions/+page.svelte new file mode 100644 index 000000000..9b5672301 --- /dev/null +++ b/apps/finance/apps/web/src/routes/transactions/+page.svelte @@ -0,0 +1,212 @@ + + + + Transaktionen | Finance + + +
+
+

Transaktionen

+
+ + + + Neu + +
+
+ + + {#if showFilters} +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ {/if} + + + {#if transactionsStore.isLoading} +
+
+
+ {:else if transactionsStore.error} +
{transactionsStore.error}
+ {:else if transactionsStore.transactions.length === 0} +
+

Keine Transaktionen gefunden.

+ + Erste Transaktion erstellen + +
+ {:else} + + + +
+ {transactionsStore.transactions.length} von {transactionsStore.total} Transaktionen +
+ {/if} +
diff --git a/apps/finance/apps/web/svelte.config.js b/apps/finance/apps/web/svelte.config.js new file mode 100644 index 000000000..c8b303bb6 --- /dev/null +++ b/apps/finance/apps/web/svelte.config.js @@ -0,0 +1,12 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + }, +}; + +export default config; diff --git a/apps/finance/apps/web/tsconfig.json b/apps/finance/apps/web/tsconfig.json new file mode 100644 index 000000000..a8f10c8e3 --- /dev/null +++ b/apps/finance/apps/web/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/apps/finance/apps/web/vite.config.ts b/apps/finance/apps/web/vite.config.ts new file mode 100644 index 000000000..6931f73fb --- /dev/null +++ b/apps/finance/apps/web/vite.config.ts @@ -0,0 +1,47 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()], + server: { + port: 5189, + strictPort: true, + }, + ssr: { + noExternal: [ + '@finance/shared', + '@manacore/shared-icons', + '@manacore/shared-ui', + '@manacore/shared-tailwind', + '@manacore/shared-theme', + '@manacore/shared-theme-ui', + '@manacore/shared-feedback-ui', + '@manacore/shared-feedback-service', + '@manacore/shared-feedback-types', + '@manacore/shared-auth', + '@manacore/shared-auth-ui', + '@manacore/shared-branding', + '@manacore/shared-subscription-ui', + 'chart.js', + 'svelte-chartjs', + ], + }, + optimizeDeps: { + exclude: [ + '@finance/shared', + '@manacore/shared-icons', + '@manacore/shared-ui', + '@manacore/shared-tailwind', + '@manacore/shared-theme', + '@manacore/shared-theme-ui', + '@manacore/shared-feedback-ui', + '@manacore/shared-feedback-service', + '@manacore/shared-feedback-types', + '@manacore/shared-auth', + '@manacore/shared-auth-ui', + '@manacore/shared-branding', + '@manacore/shared-subscription-ui', + ], + }, +}); diff --git a/apps/finance/package.json b/apps/finance/package.json new file mode 100644 index 000000000..db51b7c0e --- /dev/null +++ b/apps/finance/package.json @@ -0,0 +1,23 @@ +{ + "name": "finance", + "version": "1.0.0", + "private": true, + "description": "Finance App - Budget & Expense Tracking for ManaCore Ecosystem", + "scripts": { + "dev": "turbo run dev", + "dev:backend": "pnpm --filter @finance/backend dev", + "dev:web": "pnpm --filter @finance/web dev", + "dev:mobile": "pnpm --filter @finance/mobile dev", + "dev:landing": "pnpm --filter @finance/landing dev", + "build": "turbo run build", + "lint": "turbo run lint", + "clean": "turbo run clean", + "db:push": "pnpm --filter @finance/backend db:push", + "db:studio": "pnpm --filter @finance/backend db:studio", + "db:seed": "pnpm --filter @finance/backend db:seed" + }, + "devDependencies": { + "typescript": "^5.9.3" + }, + "packageManager": "pnpm@9.15.0" +} diff --git a/apps/finance/packages/shared/package.json b/apps/finance/packages/shared/package.json new file mode 100644 index 000000000..8c0811ff7 --- /dev/null +++ b/apps/finance/packages/shared/package.json @@ -0,0 +1,20 @@ +{ + "name": "@finance/shared", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./types": "./src/types/index.ts", + "./utils": "./src/utils/index.ts", + "./constants": "./src/constants/index.ts" + }, + "scripts": { + "type-check": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/apps/finance/packages/shared/src/constants/index.ts b/apps/finance/packages/shared/src/constants/index.ts new file mode 100644 index 000000000..3f4a8f014 --- /dev/null +++ b/apps/finance/packages/shared/src/constants/index.ts @@ -0,0 +1,249 @@ +import type { AccountType, CategoryType } from '../types'; + +// Account Type Labels +export const ACCOUNT_TYPE_LABELS: Record = { + checking: { de: 'Girokonto', en: 'Checking Account' }, + savings: { de: 'Sparkonto', en: 'Savings Account' }, + credit_card: { de: 'Kreditkarte', en: 'Credit Card' }, + cash: { de: 'Bargeld', en: 'Cash' }, + investment: { de: 'Investment', en: 'Investment' }, + loan: { de: 'Kredit', en: 'Loan' }, + other: { de: 'Sonstiges', en: 'Other' }, +}; + +// Account Type Icons +export const ACCOUNT_TYPE_ICONS: Record = { + checking: 'bank', + savings: 'piggy-bank', + credit_card: 'credit-card', + cash: 'wallet', + investment: 'chart-line-up', + loan: 'hand-coins', + other: 'dots-three', +}; + +// Account Type Colors +export const ACCOUNT_TYPE_COLORS: Record = { + checking: '#3b82f6', + savings: '#22c55e', + credit_card: '#f97316', + cash: '#8b5cf6', + investment: '#06b6d4', + loan: '#ef4444', + other: '#6b7280', +}; + +// Default Categories +export interface DefaultCategory { + name: { de: string; en: string }; + type: CategoryType; + color: string; + icon: string; +} + +export const DEFAULT_CATEGORIES: DefaultCategory[] = [ + // Expense Categories + { + name: { de: 'Lebensmittel', en: 'Groceries' }, + type: 'expense', + color: '#22c55e', + icon: 'shopping-cart', + }, + { + name: { de: 'Restaurant', en: 'Dining' }, + type: 'expense', + color: '#f97316', + icon: 'fork-knife', + }, + { name: { de: 'Transport', en: 'Transport' }, type: 'expense', color: '#3b82f6', icon: 'car' }, + { name: { de: 'Wohnen', en: 'Housing' }, type: 'expense', color: '#8b5cf6', icon: 'house' }, + { + name: { de: 'Versicherungen', en: 'Insurance' }, + type: 'expense', + color: '#6b7280', + icon: 'shield-check', + }, + { name: { de: 'Gesundheit', en: 'Health' }, type: 'expense', color: '#ef4444', icon: 'heart' }, + { + name: { de: 'Unterhaltung', en: 'Entertainment' }, + type: 'expense', + color: '#ec4899', + icon: 'game-controller', + }, + { + name: { de: 'Shopping', en: 'Shopping' }, + type: 'expense', + color: '#eab308', + icon: 'shopping-bag', + }, + { + name: { de: 'Bildung', en: 'Education' }, + type: 'expense', + color: '#6366f1', + icon: 'graduation-cap', + }, + { name: { de: 'Reisen', en: 'Travel' }, type: 'expense', color: '#06b6d4', icon: 'airplane' }, + { + name: { de: 'Abonnements', en: 'Subscriptions' }, + type: 'expense', + color: '#a855f7', + icon: 'repeat', + }, + { + name: { de: 'Sonstiges', en: 'Other Expense' }, + type: 'expense', + color: '#9ca3af', + icon: 'dots-three', + }, + + // Income Categories + { name: { de: 'Gehalt', en: 'Salary' }, type: 'income', color: '#22c55e', icon: 'money' }, + { + name: { de: 'Nebeneinkommen', en: 'Side Income' }, + type: 'income', + color: '#3b82f6', + icon: 'briefcase', + }, + { + name: { de: 'Investitionen', en: 'Investments' }, + type: 'income', + color: '#8b5cf6', + icon: 'chart-line-up', + }, + { name: { de: 'Geschenke', en: 'Gifts' }, type: 'income', color: '#ec4899', icon: 'gift' }, + { + name: { de: 'Sonstiges', en: 'Other Income' }, + type: 'income', + color: '#9ca3af', + icon: 'dots-three', + }, +]; + +// Supported Currencies +export interface Currency { + code: string; + name: { de: string; en: string }; + symbol: string; + decimalDigits: number; +} + +export const CURRENCIES: Currency[] = [ + { code: 'EUR', name: { de: 'Euro', en: 'Euro' }, symbol: '€', decimalDigits: 2 }, + { code: 'USD', name: { de: 'US-Dollar', en: 'US Dollar' }, symbol: '$', decimalDigits: 2 }, + { + code: 'GBP', + name: { de: 'Britisches Pfund', en: 'British Pound' }, + symbol: '£', + decimalDigits: 2, + }, + { + code: 'CHF', + name: { de: 'Schweizer Franken', en: 'Swiss Franc' }, + symbol: 'CHF', + decimalDigits: 2, + }, + { + code: 'JPY', + name: { de: 'Japanischer Yen', en: 'Japanese Yen' }, + symbol: '¥', + decimalDigits: 0, + }, + { + code: 'CAD', + name: { de: 'Kanadischer Dollar', en: 'Canadian Dollar' }, + symbol: 'C$', + decimalDigits: 2, + }, + { + code: 'AUD', + name: { de: 'Australischer Dollar', en: 'Australian Dollar' }, + symbol: 'A$', + decimalDigits: 2, + }, + { + code: 'CNY', + name: { de: 'Chinesischer Yuan', en: 'Chinese Yuan' }, + symbol: '¥', + decimalDigits: 2, + }, + { + code: 'INR', + name: { de: 'Indische Rupie', en: 'Indian Rupee' }, + symbol: '₹', + decimalDigits: 2, + }, + { + code: 'PLN', + name: { de: 'Polnischer Zloty', en: 'Polish Zloty' }, + symbol: 'zł', + decimalDigits: 2, + }, + { + code: 'SEK', + name: { de: 'Schwedische Krone', en: 'Swedish Krona' }, + symbol: 'kr', + decimalDigits: 2, + }, + { + code: 'NOK', + name: { de: 'Norwegische Krone', en: 'Norwegian Krone' }, + symbol: 'kr', + decimalDigits: 2, + }, + { + code: 'DKK', + name: { de: 'Dänische Krone', en: 'Danish Krone' }, + symbol: 'kr', + decimalDigits: 2, + }, +]; + +export const DEFAULT_CURRENCY = 'EUR'; + +// Recurrence Frequencies +export const RECURRENCE_FREQUENCIES = [ + { value: 'daily', label: { de: 'Täglich', en: 'Daily' } }, + { value: 'weekly', label: { de: 'Wöchentlich', en: 'Weekly' } }, + { value: 'biweekly', label: { de: 'Zweiwöchentlich', en: 'Biweekly' } }, + { value: 'monthly', label: { de: 'Monatlich', en: 'Monthly' } }, + { value: 'yearly', label: { de: 'Jährlich', en: 'Yearly' } }, +] as const; + +// Date Formats +export const DATE_FORMATS = [ + { value: 'dd.MM.yyyy', label: 'DD.MM.YYYY (31.12.2024)' }, + { value: 'MM/dd/yyyy', label: 'MM/DD/YYYY (12/31/2024)' }, + { value: 'yyyy-MM-dd', label: 'YYYY-MM-DD (2024-12-31)' }, + { value: 'dd/MM/yyyy', label: 'DD/MM/YYYY (31/12/2024)' }, +] as const; + +// Week Start Options +export const WEEK_START_OPTIONS = [ + { value: 0, label: { de: 'Sonntag', en: 'Sunday' } }, + { value: 1, label: { de: 'Montag', en: 'Monday' } }, +] as const; + +// Budget Alert Thresholds +export const BUDGET_ALERT_THRESHOLDS = [ + { value: '0.50', label: '50%' }, + { value: '0.75', label: '75%' }, + { value: '0.80', label: '80%' }, + { value: '0.90', label: '90%' }, + { value: '0.95', label: '95%' }, +] as const; + +// Chart Colors +export const CHART_COLORS = [ + '#3b82f6', // blue + '#22c55e', // green + '#f97316', // orange + '#8b5cf6', // purple + '#ef4444', // red + '#06b6d4', // cyan + '#ec4899', // pink + '#eab308', // yellow + '#6366f1', // indigo + '#14b8a6', // teal + '#f43f5e', // rose + '#84cc16', // lime +]; diff --git a/apps/finance/packages/shared/src/index.ts b/apps/finance/packages/shared/src/index.ts new file mode 100644 index 000000000..316fb8ba3 --- /dev/null +++ b/apps/finance/packages/shared/src/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * from './constants'; +export * from './utils'; diff --git a/apps/finance/packages/shared/src/types/index.ts b/apps/finance/packages/shared/src/types/index.ts new file mode 100644 index 000000000..3fe324c18 --- /dev/null +++ b/apps/finance/packages/shared/src/types/index.ts @@ -0,0 +1,311 @@ +// Account Types +export type AccountType = + | 'checking' + | 'savings' + | 'credit_card' + | 'cash' + | 'investment' + | 'loan' + | 'other'; + +export interface Account { + id: string; + userId: string; + name: string; + type: AccountType; + balance: string; + currency: string; + color: string; + icon: string; + isArchived: boolean; + includeInTotal: boolean; + order: number; + description?: string; + institutionName?: string; + accountNumber?: string; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateAccountInput { + name: string; + type: AccountType; + balance?: string; + currency?: string; + color?: string; + icon?: string; + description?: string; + institutionName?: string; + accountNumber?: string; + includeInTotal?: boolean; +} + +export interface UpdateAccountInput extends Partial { + isArchived?: boolean; + order?: number; +} + +// Category Types +export type CategoryType = 'income' | 'expense'; + +export interface Category { + id: string; + userId: string; + name: string; + type: CategoryType; + parentId?: string; + color: string; + icon: string; + order: number; + isSystem: boolean; + isArchived: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateCategoryInput { + name: string; + type: CategoryType; + parentId?: string; + color?: string; + icon?: string; +} + +export interface UpdateCategoryInput extends Partial { + isArchived?: boolean; + order?: number; +} + +// Transaction Types +export type TransactionType = 'income' | 'expense'; + +export interface RecurrenceRule { + frequency: 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'yearly'; + interval: number; + endDate?: string; + dayOfMonth?: number; + dayOfWeek?: number; +} + +export interface Transaction { + id: string; + userId: string; + accountId: string; + categoryId?: string; + type: TransactionType; + amount: string; + currency: string; + date: string; + description?: string; + notes?: string; + payee?: string; + isRecurring: boolean; + recurrenceRule?: RecurrenceRule; + parentTransactionId?: string; + isPending: boolean; + isReconciled: boolean; + tags: string[]; + attachments: string[]; + createdAt: Date; + updatedAt: Date; + // Joined data + account?: Account; + category?: Category; +} + +export interface CreateTransactionInput { + accountId: string; + categoryId?: string; + type: TransactionType; + amount: string; + currency?: string; + date: string; + description?: string; + notes?: string; + payee?: string; + isRecurring?: boolean; + recurrenceRule?: RecurrenceRule; + isPending?: boolean; + tags?: string[]; +} + +export interface UpdateTransactionInput extends Partial { + isReconciled?: boolean; +} + +export interface TransactionFilters { + accountId?: string; + categoryId?: string; + type?: TransactionType; + startDate?: string; + endDate?: string; + minAmount?: string; + maxAmount?: string; + search?: string; + isPending?: boolean; + isRecurring?: boolean; + limit?: number; + offset?: number; +} + +// Budget Types +export interface Budget { + id: string; + userId: string; + categoryId?: string; + month: number; + year: number; + amount: string; + currency: string; + alertThreshold: string; + alertEnabled: boolean; + rolloverEnabled: boolean; + rolloverAmount: string; + createdAt: Date; + updatedAt: Date; + // Computed + spent?: string; + remaining?: string; + percentage?: number; + category?: Category; +} + +export interface CreateBudgetInput { + categoryId?: string; + month: number; + year: number; + amount: string; + currency?: string; + alertThreshold?: string; + alertEnabled?: boolean; + rolloverEnabled?: boolean; +} + +export interface UpdateBudgetInput extends Partial {} + +// Transfer Types +export interface Transfer { + id: string; + userId: string; + fromAccountId: string; + toAccountId: string; + amount: string; + date: string; + description?: string; + createdAt: Date; + updatedAt: Date; + // Joined data + fromAccount?: Account; + toAccount?: Account; +} + +export interface CreateTransferInput { + fromAccountId: string; + toAccountId: string; + amount: string; + date: string; + description?: string; +} + +export interface UpdateTransferInput extends Partial {} + +// Exchange Rate Types +export interface ExchangeRate { + id: string; + fromCurrency: string; + toCurrency: string; + rate: string; + date: string; +} + +// User Settings Types +export interface UserSettings { + id: string; + userId: string; + defaultCurrency: string; + locale: string; + dateFormat: string; + weekStartsOn: number; + createdAt: Date; + updatedAt: Date; +} + +export interface UpdateUserSettingsInput { + defaultCurrency?: string; + locale?: string; + dateFormat?: string; + weekStartsOn?: number; +} + +// Report Types +export interface DashboardData { + totalBalance: number; + totalBalanceByCurrency: Record; + monthlyIncome: number; + monthlyExpenses: number; + monthlyNet: number; + budgetProgress: BudgetProgress[]; + recentTransactions: Transaction[]; + accountBalances: AccountBalance[]; +} + +export interface BudgetProgress { + categoryId: string; + categoryName: string; + categoryColor: string; + budgeted: number; + spent: number; + remaining: number; + percentage: number; +} + +export interface AccountBalance { + accountId: string; + accountName: string; + accountType: AccountType; + accountColor: string; + balance: number; + currency: string; +} + +export interface MonthlySummary { + month: number; + year: number; + income: number; + expenses: number; + net: number; + byCategory: CategoryBreakdown[]; +} + +export interface CategoryBreakdown { + categoryId: string; + categoryName: string; + categoryColor: string; + categoryIcon: string; + amount: number; + percentage: number; + transactionCount: number; +} + +export interface TrendData { + date: string; + income: number; + expenses: number; + net: number; +} + +// Connected Account Types (Bank Sync Preparation) +export type ConnectionStatus = 'active' | 'disconnected' | 'error'; + +export interface ConnectedAccount { + id: string; + userId: string; + accountId: string; + provider: string; + externalId: string; + status: ConnectionStatus; + lastSyncAt?: Date; + metadata?: Record; + createdAt: Date; + updatedAt: Date; +} diff --git a/apps/finance/packages/shared/src/utils/index.ts b/apps/finance/packages/shared/src/utils/index.ts new file mode 100644 index 000000000..ac52e5e71 --- /dev/null +++ b/apps/finance/packages/shared/src/utils/index.ts @@ -0,0 +1,214 @@ +import { CURRENCIES, DEFAULT_CURRENCY } from '../constants'; + +/** + * Format a number as currency + */ +export function formatCurrency( + amount: number | string, + currency: string = DEFAULT_CURRENCY, + locale: string = 'de-DE' +): string { + const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount; + const currencyInfo = CURRENCIES.find((c) => c.code === currency); + + return new Intl.NumberFormat(locale, { + style: 'currency', + currency: currency, + minimumFractionDigits: currencyInfo?.decimalDigits ?? 2, + maximumFractionDigits: currencyInfo?.decimalDigits ?? 2, + }).format(numAmount); +} + +/** + * Format a number with thousand separators + */ +export function formatNumber(amount: number | string, locale: string = 'de-DE'): string { + const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount; + return new Intl.NumberFormat(locale).format(numAmount); +} + +/** + * Parse a currency string to number + */ +export function parseCurrency(value: string): number { + // Remove currency symbols and thousand separators + const cleaned = value.replace(/[^0-9,.-]/g, '').replace(',', '.'); + return parseFloat(cleaned) || 0; +} + +/** + * Format a date string + */ +export function formatDate( + date: string | Date, + format: string = 'dd.MM.yyyy', + locale: string = 'de-DE' +): string { + const d = typeof date === 'string' ? new Date(date) : date; + + const day = d.getDate().toString().padStart(2, '0'); + const month = (d.getMonth() + 1).toString().padStart(2, '0'); + const year = d.getFullYear().toString(); + + switch (format) { + case 'dd.MM.yyyy': + return `${day}.${month}.${year}`; + case 'MM/dd/yyyy': + return `${month}/${day}/${year}`; + case 'yyyy-MM-dd': + return `${year}-${month}-${day}`; + case 'dd/MM/yyyy': + return `${day}/${month}/${year}`; + default: + return d.toLocaleDateString(locale); + } +} + +/** + * Get the current month and year + */ +export function getCurrentMonthYear(): { month: number; year: number } { + const now = new Date(); + return { + month: now.getMonth() + 1, + year: now.getFullYear(), + }; +} + +/** + * Get date range for a month + */ +export function getMonthDateRange( + month: number, + year: number +): { startDate: string; endDate: string } { + const startDate = new Date(year, month - 1, 1); + const endDate = new Date(year, month, 0); // Last day of month + + return { + startDate: startDate.toISOString().split('T')[0], + endDate: endDate.toISOString().split('T')[0], + }; +} + +/** + * Calculate budget percentage + */ +export function calculateBudgetPercentage(spent: number, budgeted: number): number { + if (budgeted <= 0) return 0; + return Math.round((spent / budgeted) * 100); +} + +/** + * Get budget status based on percentage + */ +export function getBudgetStatus(percentage: number): 'ok' | 'warning' | 'danger' | 'over' { + if (percentage >= 100) return 'over'; + if (percentage >= 90) return 'danger'; + if (percentage >= 75) return 'warning'; + return 'ok'; +} + +/** + * Generate a color from a string (for consistent category colors) + */ +export function stringToColor(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + + const hue = hash % 360; + return `hsl(${hue}, 65%, 50%)`; +} + +/** + * Calculate net worth from accounts + */ +export function calculateNetWorth( + accounts: { balance: string; type: string; includeInTotal: boolean }[] +): number { + return accounts + .filter((a) => a.includeInTotal) + .reduce((sum, account) => { + const balance = parseFloat(account.balance); + // Credit cards and loans are liabilities (negative) + if (account.type === 'credit_card' || account.type === 'loan') { + return sum - Math.abs(balance); + } + return sum + balance; + }, 0); +} + +/** + * Group transactions by date + */ +export function groupByDate(items: T[]): Record { + return items.reduce( + (groups, item) => { + const date = item.date; + if (!groups[date]) { + groups[date] = []; + } + groups[date].push(item); + return groups; + }, + {} as Record + ); +} + +/** + * Group transactions by category + */ +export function groupByCategory( + items: T[] +): Record { + return items.reduce( + (groups, item) => { + const categoryId = item.categoryId || 'uncategorized'; + if (!groups[categoryId]) { + groups[categoryId] = []; + } + groups[categoryId].push(item); + return groups; + }, + {} as Record + ); +} + +/** + * Sort by date (newest first) + */ +export function sortByDateDesc(items: T[]): T[] { + return [...items].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); +} + +/** + * Sort by date (oldest first) + */ +export function sortByDateAsc(items: T[]): T[] { + return [...items].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); +} + +/** + * Calculate sum of amounts + */ +export function sumAmounts(items: { amount: string }[]): number { + return items.reduce((sum, item) => sum + parseFloat(item.amount), 0); +} + +/** + * Validate IBAN (basic check) + */ +export function isValidIBAN(iban: string): boolean { + const cleaned = iban.replace(/\s/g, '').toUpperCase(); + return /^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/.test(cleaned); +} + +/** + * Format IBAN with spaces + */ +export function formatIBAN(iban: string): string { + const cleaned = iban.replace(/\s/g, '').toUpperCase(); + return cleaned.match(/.{1,4}/g)?.join(' ') || cleaned; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6964dad4f..e3a94fa76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -822,12 +822,18 @@ importers: '@manacore/shared-ui': specifier: workspace:* version: link:../../../../packages/shared-ui + d3: + specifier: ^7.9.0 + version: 7.9.0 svelte-dnd-action: specifier: ^0.9.68 version: 0.9.68(svelte@5.44.0) svelte-i18n: specifier: ^4.0.1 version: 4.0.1(svelte@5.44.0) + topojson-client: + specifier: ^3.1.0 + version: 3.1.0 devDependencies: '@sveltejs/adapter-auto': specifier: ^3.0.0 @@ -841,9 +847,18 @@ importers: '@tailwindcss/vite': specifier: ^4.1.7 version: 4.1.17(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 '@types/node': specifier: ^20.0.0 version: 20.19.25 + '@types/topojson-client': + specifier: ^3.1.5 + version: 3.1.5 + '@types/topojson-specification': + specifier: ^1.0.5 + version: 1.0.5 prettier: specifier: ^3.1.1 version: 3.6.2 @@ -1075,6 +1090,182 @@ importers: specifier: ^6.0.0 version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + apps/finance: + devDependencies: + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + apps/finance/apps/backend: + dependencies: + '@finance/shared': + specifier: workspace:* + version: link:../../packages/shared + '@manacore/shared-nestjs-auth': + specifier: workspace:* + version: link:../../../../packages/shared-nestjs-auth + '@nestjs/common': + specifier: ^10.4.9 + version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^3.3.0 + version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^10.4.9 + version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^10.4.9 + version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/schedule': + specifier: ^4.1.2 + version: 4.1.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.3 + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + drizzle-orm: + specifier: ^0.38.3 + version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.7)(expo-sqlite@15.2.14(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(kysely@0.28.8)(postgres@3.4.7)(react@19.1.0) + postgres: + specifier: ^3.4.5 + version: 3.4.7 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + devDependencies: + '@nestjs/cli': + specifier: ^10.4.9 + version: 10.4.9(esbuild@0.27.0) + '@nestjs/schematics': + specifier: ^10.2.3 + version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) + '@types/express': + specifier: ^5.0.1 + version: 5.0.5 + '@types/node': + specifier: ^22.15.21 + version: 22.19.1 + drizzle-kit: + specifier: ^0.30.2 + version: 0.30.6 + tsx: + specifier: ^4.19.4 + version: 4.20.6 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + apps/finance/apps/web: + dependencies: + '@finance/shared': + specifier: workspace:* + version: link:../../packages/shared + '@manacore/shared-auth': + specifier: workspace:* + version: link:../../../../packages/shared-auth + '@manacore/shared-auth-ui': + specifier: workspace:* + version: link:../../../../packages/shared-auth-ui + '@manacore/shared-branding': + specifier: workspace:* + version: link:../../../../packages/shared-branding + '@manacore/shared-feedback-service': + specifier: workspace:* + version: link:../../../../packages/shared-feedback-service + '@manacore/shared-feedback-ui': + specifier: workspace:* + version: link:../../../../packages/shared-feedback-ui + '@manacore/shared-i18n': + specifier: workspace:* + version: link:../../../../packages/shared-i18n + '@manacore/shared-icons': + specifier: workspace:* + version: link:../../../../packages/shared-icons + '@manacore/shared-profile-ui': + specifier: workspace:* + version: link:../../../../packages/shared-profile-ui + '@manacore/shared-subscription-ui': + specifier: workspace:* + version: link:../../../../packages/shared-subscription-ui + '@manacore/shared-tailwind': + specifier: workspace:* + version: link:../../../../packages/shared-tailwind + '@manacore/shared-theme': + specifier: workspace:* + version: link:../../../../packages/shared-theme + '@manacore/shared-theme-ui': + specifier: workspace:* + version: link:../../../../packages/shared-theme-ui + '@manacore/shared-ui': + specifier: workspace:* + version: link:../../../../packages/shared-ui + chart.js: + specifier: ^4.4.7 + version: 4.5.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + svelte-chartjs: + specifier: ^3.1.5 + version: 3.1.5(chart.js@4.5.1)(svelte@5.44.0) + svelte-i18n: + specifier: ^4.0.1 + version: 4.0.1(svelte@5.44.0) + devDependencies: + '@sveltejs/adapter-auto': + specifier: ^3.0.0 + version: 3.3.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))) + '@sveltejs/kit': + specifier: ^2.0.0 + version: 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': + specifier: ^5.0.0 + version: 5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + '@tailwindcss/vite': + specifier: ^4.1.7 + version: 4.1.17(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + '@types/node': + specifier: ^20.0.0 + version: 20.19.25 + prettier: + specifier: ^3.1.1 + version: 3.6.2 + prettier-plugin-svelte: + specifier: ^3.1.2 + version: 3.4.0(prettier@3.6.2)(svelte@5.44.0) + svelte: + specifier: ^5.0.0 + version: 5.44.0 + svelte-check: + specifier: ^4.0.0 + version: 4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3) + tailwindcss: + specifier: ^4.1.7 + version: 4.1.17 + tslib: + specifier: ^2.4.1 + version: 2.8.1 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + vite: + specifier: ^6.0.0 + version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + + apps/finance/packages/shared: + devDependencies: + typescript: + specifier: ^5.9.3 + version: 5.9.3 + apps/mail: devDependencies: typescript: @@ -1710,6 +1901,9 @@ importers: '@supabase/supabase-js': specifier: ^2.81.1 version: 2.84.0 + svelte-dnd-action: + specifier: ^0.9.68 + version: 0.9.68(svelte@5.44.0) svelte-i18n: specifier: ^4.0.0 version: 4.0.1(svelte@5.44.0) @@ -2190,6 +2384,369 @@ importers: specifier: ^7.1.10 version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + apps/moodlit: {} + + apps/moodlit/apps/backend: + dependencies: + '@manacore/shared-nestjs-auth': + specifier: workspace:* + version: link:../../../../packages/shared-nestjs-auth + '@nestjs/common': + specifier: ^10.4.15 + version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^3.3.0 + version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^10.4.15 + version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^10.4.15 + version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.3 + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + drizzle-kit: + specifier: ^0.30.2 + version: 0.30.6 + drizzle-orm: + specifier: ^0.38.3 + version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.7)(expo-sqlite@15.2.14(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(kysely@0.28.8)(postgres@3.4.7)(react@19.1.0) + postgres: + specifier: ^3.4.5 + version: 3.4.7 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + devDependencies: + '@nestjs/cli': + specifier: ^10.4.9 + version: 10.4.9(esbuild@0.27.0) + '@nestjs/schematics': + specifier: ^10.2.3 + version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) + '@types/express': + specifier: ^5.0.0 + version: 5.0.5 + '@types/node': + specifier: ^22.10.2 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^8.18.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.18.1 + version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: + specifier: ^9.17.0 + version: 9.39.1(jiti@2.6.1) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.2(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.1 + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) + prettier: + specifier: ^3.4.2 + version: 3.6.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + ts-loader: + specifier: ^9.5.1 + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + + apps/moodlit/apps/landing: + dependencies: + '@astrojs/check': + specifier: ^0.9.0 + version: 0.9.5(prettier-plugin-astro@0.14.1)(prettier@3.6.2)(typescript@5.9.3) + '@manacore/shared-landing-ui': + specifier: workspace:* + version: link:../../../../packages/shared-landing-ui + astro: + specifier: ^5.16.0 + version: 5.16.0(@azure/identity@4.13.0)(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + devDependencies: + '@astrojs/tailwind': + specifier: ^6.0.2 + version: 6.0.2(astro@5.16.0(@azure/identity@4.13.0)(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + '@tailwindcss/typography': + specifier: ^0.5.18 + version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) + '@types/node': + specifier: ^20.0.0 + version: 20.19.25 + eslint: + specifier: ^9.0.0 + version: 9.39.1(jiti@2.6.1) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.2(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-astro: + specifier: ^1.0.0 + version: 1.5.0(eslint@9.39.1(jiti@2.6.1)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-astro: + specifier: ^0.14.1 + version: 0.14.1 + prettier-plugin-tailwindcss: + specifier: ^0.6.14 + version: 0.6.14(prettier-plugin-astro@0.14.1)(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.0))(prettier@3.6.2) + tailwindcss: + specifier: ^3.4.0 + version: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + + apps/moodlit/apps/mobile: + dependencies: + '@expo/vector-icons': + specifier: ^15.0.2 + version: 15.0.3(expo-font@14.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@react-native-async-storage/async-storage': + specifier: 2.2.0 + version: 2.2.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + '@react-native-community/slider': + specifier: ^5.1.0 + version: 5.1.1 + '@react-navigation/native': + specifier: ^7.1.6 + version: 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@supabase/supabase-js': + specifier: ^2.38.4 + version: 2.84.0 + expo: + specifier: ^54.0.0 + version: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-av: + specifier: ^16.0.7 + version: 16.0.7(expo@54.0.25)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-brightness: + specifier: ^14.0.7 + version: 14.0.7(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + expo-camera: + specifier: ^17.0.9 + version: 17.0.9(expo@54.0.25)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-constants: + specifier: ~18.0.9 + version: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + expo-dev-client: + specifier: ~6.0.13 + version: 6.0.18(expo@54.0.25) + expo-device: + specifier: ~8.0.9 + version: 8.0.9(expo@54.0.25) + expo-haptics: + specifier: ^15.0.7 + version: 15.0.7(expo@54.0.25) + expo-keep-awake: + specifier: ^15.0.7 + version: 15.0.7(expo@54.0.25)(react@19.1.0) + expo-linear-gradient: + specifier: ^15.0.7 + version: 15.0.7(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-linking: + specifier: ~8.0.8 + version: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-router: + specifier: ~6.0.10 + version: 6.0.15(5vziatpev6vbpelxbu5bibycr4) + expo-splash-screen: + specifier: ^0.21.1 + version: 0.21.1(expo-modules-autolinking@3.0.22)(expo@54.0.25) + expo-status-bar: + specifier: ~3.0.8 + version: 3.0.8(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-symbols: + specifier: ^1.0.7 + version: 1.0.7(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + expo-system-ui: + specifier: ~6.0.7 + version: 6.0.8(expo@54.0.25)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + expo-web-browser: + specifier: ~15.0.7 + version: 15.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + nativewind: + specifier: latest + version: 4.2.1(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + react-native: + specifier: 0.81.5 + version: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-draggable-flatlist: + specifier: ^4.0.3 + version: 4.0.3(@babel/core@7.28.5)(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + react-native-gesture-handler: + specifier: ~2.28.0 + version: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-nitro-modules: + specifier: ^0.31.4 + version: 0.31.10(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-reanimated: + specifier: ~4.1.1 + version: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: + specifier: ~5.6.0 + version: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-screens: + specifier: ~4.16.0 + version: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-torch-nitro: + specifier: ^0.0.1 + version: 0.0.1(react-native-nitro-modules@0.31.10(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-web: + specifier: ^0.21.0 + version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-native-worklets: + specifier: 0.5.1 + version: 0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + zustand: + specifier: ^4.5.1 + version: 4.5.7(@types/react@19.1.17)(react@19.1.0) + devDependencies: + '@babel/core': + specifier: ^7.20.0 + version: 7.28.5 + '@types/react': + specifier: ~19.1.10 + version: 19.1.17 + eslint: + specifier: ^9.25.1 + version: 9.39.1(jiti@2.6.1) + eslint-config-expo: + specifier: ~10.0.0 + version: 10.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-config-prettier: + specifier: ^10.1.2 + version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) + prettier: + specifier: ^3.2.5 + version: 3.6.2 + prettier-plugin-tailwindcss: + specifier: ^0.5.11 + version: 0.5.14(prettier-plugin-astro@0.14.1)(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.0))(prettier@3.6.2) + tailwindcss: + specifier: ^3.4.0 + version: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + typescript: + specifier: ~5.9.2 + version: 5.9.3 + + apps/moodlit/apps/web: + dependencies: + '@manacore/shared-auth': + specifier: workspace:* + version: link:../../../../packages/shared-auth + '@manacore/shared-auth-ui': + specifier: workspace:* + version: link:../../../../packages/shared-auth-ui + '@manacore/shared-branding': + specifier: workspace:* + version: link:../../../../packages/shared-branding + '@manacore/shared-feedback-service': + specifier: workspace:* + version: link:../../../../packages/shared-feedback-service + '@manacore/shared-feedback-ui': + specifier: workspace:* + version: link:../../../../packages/shared-feedback-ui + '@manacore/shared-i18n': + specifier: workspace:* + version: link:../../../../packages/shared-i18n + '@manacore/shared-icons': + specifier: workspace:* + version: link:../../../../packages/shared-icons + '@manacore/shared-profile-ui': + specifier: workspace:* + version: link:../../../../packages/shared-profile-ui + '@manacore/shared-subscription-ui': + specifier: workspace:* + version: link:../../../../packages/shared-subscription-ui + '@manacore/shared-tailwind': + specifier: workspace:* + version: link:../../../../packages/shared-tailwind + '@manacore/shared-theme': + specifier: workspace:* + version: link:../../../../packages/shared-theme + '@manacore/shared-theme-ui': + specifier: workspace:* + version: link:../../../../packages/shared-theme-ui + '@manacore/shared-ui': + specifier: workspace:* + version: link:../../../../packages/shared-ui + svelte-i18n: + specifier: ^4.0.1 + version: 4.0.1(svelte@5.44.0) + devDependencies: + '@sveltejs/adapter-auto': + specifier: ^3.0.0 + version: 3.3.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))) + '@sveltejs/kit': + specifier: ^2.0.0 + version: 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': + specifier: ^5.0.0 + version: 5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + '@tailwindcss/vite': + specifier: ^4.1.7 + version: 4.1.17(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + '@types/node': + specifier: ^20.0.0 + version: 20.19.25 + prettier: + specifier: ^3.1.1 + version: 3.6.2 + prettier-plugin-svelte: + specifier: ^3.1.2 + version: 3.4.0(prettier@3.6.2)(svelte@5.44.0) + svelte: + specifier: ^5.0.0 + version: 5.44.0 + svelte-check: + specifier: ^4.0.0 + version: 4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3) + tailwindcss: + specifier: ^4.1.7 + version: 4.1.17 + tslib: + specifier: ^2.4.1 + version: 2.8.1 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + vite: + specifier: ^6.0.0 + version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + apps/picture: dependencies: expo: @@ -3815,6 +4372,118 @@ importers: specifier: ^5.0.0 version: 5.9.3 + games/figgos: + dependencies: + '@expo/vector-icons': + specifier: ^14.0.0 + version: 14.1.0(expo-font@14.0.9(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@react-native-async-storage/async-storage': + specifier: ^1.23.1 + version: 1.23.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + '@react-navigation/native': + specifier: ^7.0.3 + version: 7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@supabase/supabase-js': + specifier: ^2.49.4 + version: 2.84.0 + expo: + specifier: ^52.0.46 + version: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-blur: + specifier: ~14.0.3 + version: 14.0.3(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-constants: + specifier: ~17.0.8 + version: 17.0.8(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + expo-dev-client: + specifier: ~5.0.4 + version: 5.0.20(expo@52.0.47) + expo-dev-launcher: + specifier: ^5.0.17 + version: 5.1.17(expo@52.0.47) + expo-image-picker: + specifier: ^16.0.6 + version: 16.0.6(expo@52.0.47) + expo-linking: + specifier: ~7.0.5 + version: 7.0.5(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-router: + specifier: ~4.0.6 + version: 4.0.21(uybeljqs5zibgesoubkrrbh7bq) + expo-status-bar: + specifier: ~2.0.1 + version: 2.0.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-system-ui: + specifier: ~4.0.9 + version: 4.0.9(expo@52.0.47)(react-native-web@0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + expo-web-browser: + specifier: ~14.0.2 + version: 14.0.2(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + nativewind: + specifier: latest + version: 4.2.1(react-native-reanimated@3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-svg@15.12.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-native: + specifier: 0.76.9 + version: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-gesture-handler: + specifier: ~2.20.2 + version: 2.20.2(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-keyboard-aware-scroll-view: + specifier: ^0.9.5 + version: 0.9.5(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + react-native-reanimated: + specifier: 3.16.2 + version: 3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-safe-area-context: + specifier: 4.12.0 + version: 4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-screens: + specifier: ~4.4.0 + version: 4.4.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-svg: + specifier: ^15.11.2 + version: 15.12.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-web: + specifier: ~0.19.10 + version: 0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@babel/core': + specifier: ^7.20.0 + version: 7.28.5 + '@types/react': + specifier: ~18.3.12 + version: 18.3.27 + '@typescript-eslint/eslint-plugin': + specifier: ^7.7.0 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint@8.57.1)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: ^7.7.0 + version: 7.18.0(eslint@8.57.1)(typescript@5.3.3) + eslint: + specifier: ^8.57.0 + version: 8.57.1 + eslint-config-universe: + specifier: ^12.0.1 + version: 12.1.0(@types/eslint@9.6.1)(eslint@8.57.1)(prettier@3.6.2)(typescript@5.3.3) + prettier: + specifier: ^3.2.5 + version: 3.6.2 + prettier-plugin-tailwindcss: + specifier: ^0.5.11 + version: 0.5.14(prettier-plugin-astro@0.14.1)(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.0))(prettier@3.6.2) + tailwindcss: + specifier: ^3.4.0 + version: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + typescript: + specifier: ~5.3.3 + version: 5.3.3 + games/mana-games: {} games/mana-games/apps/backend: @@ -6994,9 +7663,15 @@ packages: '@expo/config-plugins@54.0.2': resolution: {integrity: sha512-jD4qxFcURQUVsUFGMcbo63a/AnviK8WUGard+yrdQE3ZrB/aurn68SlApjirQQLEizhjI5Ar2ufqflOBlNpyPg==} + '@expo/config-plugins@7.3.1': + resolution: {integrity: sha512-TkDtAP3P/rrjhr7GBQtyYH/l1SQUGAO/gByBCwHjfRa4RIPFs+iiq7hocytAl+oSmVsB28ipZCC3O1IPg1OZ7g==} + '@expo/config-plugins@9.0.17': resolution: {integrity: sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==} + '@expo/config-types@49.0.0': + resolution: {integrity: sha512-8eyREVi+K2acnMBe/rTIu1dOfyR2+AMnTLHlut+YpMV9OZPdeKV0Bs9BxAewGqBA2slslbQ9N39IS2CuTKpXkA==} + '@expo/config-types@52.0.5': resolution: {integrity: sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA==} @@ -7015,6 +7690,9 @@ packages: '@expo/config@12.0.10': resolution: {integrity: sha512-lJMof5Nqakq1DxGYlghYB/ogSBjmv4Fxn1ovyDmcjlRsQdFCXgu06gEUogkhPtc9wBt9WlTTfqENln5HHyLW6w==} + '@expo/config@8.2.1': + resolution: {integrity: sha512-15XjV0WrSb5hChRM5pAEK5uyh55njfgFOEZpov3YKYBeMd9D6QT/azWWqnaFuuCWKzgzyxD9HaYgGo5VbnOU1g==} + '@expo/devcert@1.2.0': resolution: {integrity: sha512-Uilcv3xGELD5t/b0eM4cxBFEKQRIivB3v7i+VhWLV/gL98aw810unLKKJbGAxAIhY6Ipyz8ChWibFsKFXYwstA==} @@ -7047,6 +7725,9 @@ packages: resolution: {integrity: sha512-8YPJpEYlmV171fi+t+cSLMX1nC5ngY9j2FiN70dHldLpd6Ct6ouGhk96svJ4BQZwsqwII2pokwzrDAwqo4Z0FQ==} hasBin: true + '@expo/image-utils@0.3.22': + resolution: {integrity: sha512-uzq+RERAtkWypOFOLssFnXXqEqKjNj9eXN7e97d/EXUAojNcLDoXc0sL+F5B1I4qtlsnhX01kcpoIBBZD8wZNQ==} + '@expo/image-utils@0.6.5': resolution: {integrity: sha512-RsS/1CwJYzccvlprYktD42KjyfWZECH6PPIEowvoSmXfGLfdViwcUEI4RvBfKX5Jli6P67H+6YmHvPTbGOboew==} @@ -7056,6 +7737,12 @@ packages: '@expo/json-file@10.0.7': resolution: {integrity: sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw==} + '@expo/json-file@8.2.37': + resolution: {integrity: sha512-YaH6rVg11JoTS2P6LsW7ybS2CULjf40AbnAHw2F1eDPuheprNjARZMnyHFPkKv7GuxCy+B9GPcbOKgc4cgA80Q==} + + '@expo/json-file@8.3.3': + resolution: {integrity: sha512-eZ5dld9AD0PrVRiIWpRkm5aIoWBw3kAyd8VkuWEy92sEthBKDDDHAnK2a0dw0Eil6j7rK7lS/Qaq/Zzngv2h5A==} + '@expo/json-file@9.0.2': resolution: {integrity: sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw==} @@ -7129,6 +7816,9 @@ packages: '@expo/plist@0.0.18': resolution: {integrity: sha512-+48gRqUiz65R21CZ/IXa7RNBXgAI/uPSdvJqoN9x1hfL44DNbUoWHgHiEXTx7XelcATpDwNTz6sHLfy0iNqf+w==} + '@expo/plist@0.0.20': + resolution: {integrity: sha512-UXQ4LXCfTZ580LDHGJ5q62jSTwJFFJ1GqBu8duQMThiHKWbMJ+gajJh6rsB6EJ3aLUr9wcauxneL5LVRFxwBEA==} + '@expo/plist@0.2.2': resolution: {integrity: sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g==} @@ -7143,6 +7833,11 @@ packages: peerDependencies: expo: '*' + '@expo/prebuild-config@6.3.0': + resolution: {integrity: sha512-lB3kfrP8L5zeSDMrSo8hblapr+yKkSGIceONtUx12RVuybF65Zed0m6oU/S0uyJphoS8+F7v6Wwhh/rDTz+raA==} + peerDependencies: + expo-modules-autolinking: '>=0.8.1' + '@expo/prebuild-config@8.2.0': resolution: {integrity: sha512-CxiPpd980s0jyxi7eyN3i/7YKu3XL+8qPjBZUCYtc0+axpGweqIkq2CslyLSKHyqVyH/zlPkbVgWdyiYavFS5Q==} @@ -7159,6 +7854,10 @@ packages: '@expo/server@0.5.3': resolution: {integrity: sha512-WXsWzeBs5v/h0PUfHyNLLz07rwwO5myQ1A5DGYewyyGLmsyl61yVCe8AgAlp1wkiMsqhj2hZqI2u3K10QnCMrQ==} + '@expo/spawn-async@1.5.0': + resolution: {integrity: sha512-LB7jWkqrHo+5fJHNrLAFdimuSXQ2MQ4lA7SQW5bf/HbsXuV2VrT/jN/M8f/KoWt0uJMGN4k/j7Opx4AvOOxSew==} + engines: {node: '>=4'} + '@expo/spawn-async@1.7.2': resolution: {integrity: sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==} engines: {node: '>=12'} @@ -7936,6 +8635,9 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@ljharb/through@2.3.14': resolution: {integrity: sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==} engines: {node: '>= 0.4'} @@ -8786,6 +9488,9 @@ packages: peerDependencies: react-native: '>=0.59' + '@react-native-community/slider@5.1.1': + resolution: {integrity: sha512-W98If/LnTaziU3/0h5+G1LvJaRhMc6iLQBte6UWa4WBIHDMaDPglNBIFKcCXc9Dxp83W+f+5Wv22Olq9M2HJYA==} + '@react-native-google-signin/google-signin@14.0.2': resolution: {integrity: sha512-L23SBl30VGVjxdX3jb9PZJHuuitsSU/aMGWCNQqSx69PrUqGcP2gkcUeOsK6VPFadPbjh+4ml+0UoAEzZGA3dQ==} peerDependencies: @@ -8819,6 +9524,10 @@ packages: resolution: {integrity: sha512-o79whsqL5fbPTUQO9w1FptRd4cw1TaeOrXtQSLQeDrMVAenw/wmsjyPK10VKtvqxa1KNMtWEyfgxcM8CVZVFmg==} engines: {node: '>=18'} + '@react-native/assets-registry@0.76.9': + resolution: {integrity: sha512-pN0Ws5xsjWOZ8P37efh0jqHHQmq+oNGKT4AyAoKRpxBDDDmlAmpaYjer9Qz7PpDKF+IUyRjF/+rBsM50a8JcUg==} + engines: {node: '>=18'} + '@react-native/assets-registry@0.81.4': resolution: {integrity: sha512-AMcDadefBIjD10BRqkWw+W/VdvXEomR6aEZ0fhQRAv7igrBzb4PTn4vHKYg+sUK0e3wa74kcMy2DLc/HtnGcMA==} engines: {node: '>= 20.19.4'} @@ -8915,6 +9624,15 @@ packages: '@react-native-community/cli-server-api': optional: true + '@react-native/community-cli-plugin@0.76.9': + resolution: {integrity: sha512-08jx8ixCjjd4jNQwNpP8yqrjrDctN2qvPPlf6ebz1OJQk8e1sbUl3wVn1zhhMvWrYcaraDnatPb5uCPq+dn3NQ==} + engines: {node: '>=18'} + peerDependencies: + '@react-native-community/cli': '*' + peerDependenciesMeta: + '@react-native-community/cli': + optional: true + '@react-native/community-cli-plugin@0.81.4': resolution: {integrity: sha512-8mpnvfcLcnVh+t1ok6V9eozWo8Ut+TZhz8ylJ6gF9d6q9EGDQX6s8jenan5Yv/pzN4vQEKI4ib2pTf/FELw+SA==} engines: {node: '>= 20.19.4'} @@ -8987,6 +9705,10 @@ packages: resolution: {integrity: sha512-gQI6RcrJbigU8xk7F960C5xQIgvbBj20TUvGecD+N2PHfbLpqR+92cj7hz3UcbrCONmTP40WHnbMMJ8P+kLsrA==} engines: {node: '>=18'} + '@react-native/gradle-plugin@0.76.9': + resolution: {integrity: sha512-uGzp3dL4GfNDz+jOb8Nik1Vrfq1LHm0zESizrGhHACFiFlUSflVAnWuUAjlZlz5XfLhzGVvunG4Vdrpw8CD2ng==} + engines: {node: '>=18'} + '@react-native/gradle-plugin@0.81.4': resolution: {integrity: sha512-T7fPcQvDDCSusZFVSg6H1oVDKb/NnVYLnsqkcHsAF2C2KGXyo3J7slH/tJAwNfj/7EOA2OgcWxfC1frgn9TQvw==} engines: {node: '>= 20.19.4'} @@ -9003,6 +9725,10 @@ packages: resolution: {integrity: sha512-+iEikj6c6Zvrg1c3cYMeiPB+5nS8EaIC3jCtP6Muk3qc7c386IymEPM2xycIlfg04DPZvO3D4P2/vaO9/TCnUg==} engines: {node: '>=18'} + '@react-native/js-polyfills@0.76.9': + resolution: {integrity: sha512-s6z6m8cK4SMjIX1hm8LT187aQ6//ujLrjzDBogqDCYXRbfjbAYovw5as/v2a2rhUIyJbS3UjokZm3W0H+Oh/RQ==} + engines: {node: '>=18'} + '@react-native/js-polyfills@0.81.4': resolution: {integrity: sha512-sr42FaypKXJHMVHhgSbu2f/ZJfrLzgaoQ+HdpRvKEiEh2mhFf6XzZwecyLBvWqf2pMPZa+CpPfNPiejXjKEy8w==} engines: {node: '>= 20.19.4'} @@ -9023,6 +9749,15 @@ packages: peerDependencies: '@babel/core': '*' + '@react-native/metro-babel-transformer@0.76.9': + resolution: {integrity: sha512-HGq11347UHNiO/NvVbAO35hQCmH8YZRs7in7nVq7SL99pnpZK4WXwLdAXmSuwz5uYqOuwnKYDlpadz8fkE94Mg==} + engines: {node: '>=18'} + peerDependencies: + '@babel/core': '*' + + '@react-native/normalize-color@2.1.0': + resolution: {integrity: sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA==} + '@react-native/normalize-colors@0.74.89': resolution: {integrity: sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==} @@ -9069,6 +9804,17 @@ packages: '@types/react': optional: true + '@react-native/virtualized-lists@0.76.9': + resolution: {integrity: sha512-2neUfZKuqMK2LzfS8NyOWOyWUJOWgDym5fUph6fN9qF+LNPjAvnc4Zr9+o+59qjNu/yXwQgVMWNU4+8WJuPVWw==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': ^18.2.6 + react: '*' + react-native: '*' + peerDependenciesMeta: + '@types/react': + optional: true + '@react-native/virtualized-lists@0.81.4': resolution: {integrity: sha512-hBM+rMyL6Wm1Q4f/WpqGsaCojKSNUBqAXLABNGoWm1vabZ7cSnARMxBvA/2vo3hLcoR4v7zDK8tkKm9+O0LjVA==} engines: {node: '>= 20.19.4'} @@ -9898,6 +10644,99 @@ packages: '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -9922,6 +10761,9 @@ packages: '@types/fontkit@2.0.8': resolution: {integrity: sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -10034,6 +10876,9 @@ packages: '@types/react@18.3.27': resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + '@types/react@19.1.17': + resolution: {integrity: sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==} + '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} @@ -10067,6 +10912,12 @@ packages: '@types/three@0.176.0': resolution: {integrity: sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==} + '@types/topojson-client@3.1.5': + resolution: {integrity: sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw==} + + '@types/topojson-specification@1.0.5': + resolution: {integrity: sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==} + '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -11367,6 +12218,10 @@ packages: charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + check-disk-space@3.4.0: resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} engines: {node: '>=16'} @@ -11767,6 +12622,10 @@ packages: crypt@0.0.2: resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + crypto-random-string@1.0.0: + resolution: {integrity: sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==} + engines: {node: '>=4'} + crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -11833,6 +12692,133 @@ packages: csv-parse@6.1.0: resolution: {integrity: sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} @@ -11972,6 +12958,9 @@ packages: resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} engines: {node: '>=10'} + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -12978,6 +13967,17 @@ packages: react: '*' react-native: '*' + expo-av@16.0.7: + resolution: {integrity: sha512-QReef6/RYuZ4GekTcZZw5zY26pcPOmHqK6LMgFlPhnsT0ga97HJrgMc63pvIiojDP+q4oIv24g+QPD8ljZu4XQ==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + react-native-web: '*' + peerDependenciesMeta: + react-native-web: + optional: true + expo-blur@14.0.3: resolution: {integrity: sha512-BL3xnqBJbYm3Hg9t/HjNjdeY7N/q8eK5tsLYxswWG1yElISWZmMvrXYekl7XaVCPfyFyz8vQeaxd7q74ZY3Wrw==} peerDependencies: @@ -12992,11 +13992,28 @@ packages: react: '*' react-native: '*' + expo-brightness@14.0.7: + resolution: {integrity: sha512-wccb/NdQEd45UF0lgNEksZt3E8uzlIcxIx1ZqZYWbHyNvcS3LUj5wxB6+ZgKTLeWu4vLQ+oHe+F0QrkC9ojrig==} + peerDependencies: + expo: '*' + react-native: '*' + expo-build-properties@1.0.9: resolution: {integrity: sha512-2icttCy3OPTk/GWIFt+vwA+0hup53jnmYb7JKRbvNvrrOrz+WblzpeoiaOleI2dYG/vjwpNO8to8qVyKhYJtrQ==} peerDependencies: expo: '*' + expo-camera@17.0.9: + resolution: {integrity: sha512-KgticPGurqEsaPBIwbG0T6mzAVnqZasDdM/6OoJt5zPh6tWB09+th6cBF1WafIBMPy8AWbfyUQSqQXqOrNJClg==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + react-native-web: '*' + peerDependenciesMeta: + react-native-web: + optional: true + expo-clipboard@8.0.7: resolution: {integrity: sha512-zvlfFV+wB2QQrQnHWlo0EKHAkdi2tycLtE+EXFUWTPZYkgu1XcH+aiKfd4ul7Z0SDF+1IuwoiW9AA9eO35aj3Q==} peerDependencies: @@ -13334,6 +14351,11 @@ packages: peerDependencies: expo: '*' + expo-splash-screen@0.21.1: + resolution: {integrity: sha512-FAbXTF8d5/6ZYCFuqqKDtY8O5pYFhBue7C6SzQ0F2kOnlMi2hym68udmHVVOfT0ZTnSCn/H1SjQ90TyR4MNETw==} + peerDependencies: + expo: '*' + expo-splash-screen@0.29.24: resolution: {integrity: sha512-k2rdjbb3Qeg4g104Sdz6+qXXYba8QgiuZRSxHX8IpsSYiiTU48BmCCGy12sN+O1B+sD1/+WPL4duCa1Fy6+Y4g==} peerDependencies: @@ -13937,6 +14959,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -14330,6 +15356,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + intl-messageformat@10.7.18: resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} @@ -15401,6 +16431,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} @@ -17186,18 +18220,34 @@ packages: react: '*' react-native: '*' + react-native-iphone-x-helper@1.3.1: + resolution: {integrity: sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==} + peerDependencies: + react-native: '>=0.42.0' + react-native-is-edge-to-edge@1.2.1: resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==} peerDependencies: react: '*' react-native: '*' + react-native-keyboard-aware-scroll-view@0.9.5: + resolution: {integrity: sha512-XwfRn+T/qBH9WjTWIBiJD2hPWg0yJvtaEw6RtPCa5/PYHabzBaWxYBOl0usXN/368BL1XktnZPh8C2lmTpOREA==} + peerDependencies: + react-native: '>=0.48.4' + react-native-markdown-display@7.0.2: resolution: {integrity: sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ==} peerDependencies: react: '>=16.2.0' react-native: '>=0.50.4' + react-native-nitro-modules@0.31.10: + resolution: {integrity: sha512-hcvjTu9YJE9fMmnAUvhG8CxvYLpOuMQ/2eyi/S6GyrecezF6Rmk/uRQEL6v09BRFWA/xRVZNQVulQPS+2HS3mQ==} + peerDependencies: + react: '*' + react-native: '*' + react-native-pager-view@6.9.1: resolution: {integrity: sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw==} peerDependencies: @@ -17287,6 +18337,13 @@ packages: react-native-swipe-gestures@1.0.5: resolution: {integrity: sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw==} + react-native-torch-nitro@0.0.1: + resolution: {integrity: sha512-I9wXCR6RplVzlOAXGtUTiPHXo2hW+ywU0X2wPhXkRYemrXAd7IWNReNQpZKBkbsiLmUVmHgp7Mi20awoUWyzdQ==} + peerDependencies: + react: '*' + react-native: '*' + react-native-nitro-modules: '*' + react-native-web@0.19.13: resolution: {integrity: sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A==} peerDependencies: @@ -17356,6 +18413,17 @@ packages: '@types/react': optional: true + react-native@0.76.9: + resolution: {integrity: sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@types/react': ^18.2.6 + react: ^18.2.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-native@0.81.4: resolution: {integrity: sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==} engines: {node: '>= 20.19.4'} @@ -17693,6 +18761,9 @@ packages: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rollup@4.53.3: resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -17723,6 +18794,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} @@ -17807,6 +18881,16 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.3.2: + resolution: {integrity: sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==} + engines: {node: '>=10'} + hasBin: true + + semver@7.5.3: + resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} + engines: {node: '>=10'} + hasBin: true + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -18291,6 +19375,12 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte-chartjs@3.1.5: + resolution: {integrity: sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA==} + peerDependencies: + chart.js: ^3.5.0 || ^4.0.0 + svelte: ^4.0.0 + svelte-check@4.3.4: resolution: {integrity: sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==} engines: {node: '>= 18.0.0'} @@ -18365,6 +19455,10 @@ packages: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} + temp-dir@1.0.0: + resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} + engines: {node: '>=4'} + temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -18373,6 +19467,10 @@ packages: resolution: {integrity: sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==} engines: {node: '>=6.0.0'} + tempy@0.3.0: + resolution: {integrity: sha512-WrH/pui8YCwmeiAoxV+lpRH9HpRtgBhSR2ViBPgpGb/wnYDzp21R4MN45fsCGvLROvY67o3byhJRYRONJyImVQ==} + engines: {node: '>=8'} + tempy@0.7.1: resolution: {integrity: sha512-vXPxwOyaNVi9nyczO16mxmHGpl6ASC5/TVhRRHpqeYHvKQm58EaWNvZXxAhR0lYYnBOQFjXjhzeLsaXdjxLjRg==} engines: {node: '>=10'} @@ -18508,6 +19606,10 @@ packages: resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} engines: {node: '>=14.16'} + topojson-client@3.1.0: + resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==} + hasBin: true + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -18725,6 +19827,10 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@0.3.1: + resolution: {integrity: sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==} + engines: {node: '>=6'} + type-fest@0.7.1: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} @@ -18893,6 +19999,10 @@ packages: resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + unique-string@1.0.0: + resolution: {integrity: sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==} + engines: {node: '>=4'} + unique-string@2.0.0: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} @@ -22958,6 +24068,83 @@ snapshots: - supports-color - utf-8-validate + '@expo/cli@54.0.16(expo-router@6.0.15)(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))': + dependencies: + '@0no-co/graphql.web': 1.2.0 + '@expo/code-signing-certificates': 0.0.5 + '@expo/config': 12.0.10 + '@expo/config-plugins': 54.0.2 + '@expo/devcert': 1.2.0 + '@expo/env': 2.0.7 + '@expo/image-utils': 0.8.7 + '@expo/json-file': 10.0.7 + '@expo/mcp-tunnel': 0.1.0 + '@expo/metro': 54.1.0 + '@expo/metro-config': 54.0.9(expo@54.0.25) + '@expo/osascript': 2.3.7 + '@expo/package-manager': 1.9.8 + '@expo/plist': 0.4.7 + '@expo/prebuild-config': 54.0.6(expo@54.0.25) + '@expo/schema-utils': 0.1.7 + '@expo/spawn-async': 1.7.2 + '@expo/ws-tunnel': 1.0.6 + '@expo/xcpretty': 4.3.2 + '@react-native/dev-middleware': 0.81.5 + '@urql/core': 5.2.0 + '@urql/exchange-retry': 1.3.2(@urql/core@5.2.0) + accepts: 1.3.8 + arg: 5.0.2 + better-opn: 3.0.2 + bplist-creator: 0.1.0 + bplist-parser: 0.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + compression: 1.8.1 + connect: 3.7.0 + debug: 4.4.3 + env-editor: 0.4.2 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + freeport-async: 2.0.0 + getenv: 2.0.0 + glob: 10.5.0 + lan-network: 0.1.7 + minimatch: 9.0.5 + node-forge: 1.3.2 + npm-package-arg: 11.0.3 + ora: 3.4.0 + picomatch: 3.0.1 + pretty-bytes: 5.6.0 + pretty-format: 29.7.0 + progress: 2.0.3 + prompts: 2.4.2 + qrcode-terminal: 0.11.0 + require-from-string: 2.0.2 + requireg: 0.2.2 + resolve: 1.22.11 + resolve-from: 5.0.0 + resolve.exports: 2.0.3 + semver: 7.7.3 + send: 0.19.1 + slugify: 1.6.6 + source-map-support: 0.5.21 + stacktrace-parser: 0.1.11 + structured-headers: 0.4.1 + tar: 7.5.2 + terminal-link: 2.1.1 + undici: 6.22.0 + wrap-ansi: 7.0.0 + ws: 8.18.3 + optionalDependencies: + expo-router: 6.0.15(5vziatpev6vbpelxbu5bibycr4) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - graphql + - supports-color + - utf-8-validate + '@expo/cli@54.0.16(expo-router@6.0.15)(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))': dependencies: '@0no-co/graphql.web': 1.2.0 @@ -23078,6 +24265,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/config-plugins@7.3.1': + dependencies: + '@expo/config-types': 49.0.0 + '@expo/json-file': 8.2.37 + '@expo/plist': 0.0.20 + '@expo/sdk-runtime-versions': 1.0.0 + '@react-native/normalize-color': 2.1.0 + chalk: 4.1.2 + debug: 4.4.3 + find-up: 5.0.0 + getenv: 1.0.0 + glob: 7.1.6 + resolve-from: 5.0.0 + semver: 7.7.3 + slash: 3.0.0 + xcode: 3.0.1 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + '@expo/config-plugins@9.0.17': dependencies: '@expo/config-types': 52.0.5 @@ -23097,6 +24304,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/config-types@49.0.0': {} + '@expo/config-types@52.0.5': {} '@expo/config-types@53.0.5': {} @@ -23157,6 +24366,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/config@8.2.1': + dependencies: + '@babel/code-frame': 7.10.4 + '@expo/config-plugins': 7.3.1 + '@expo/config-types': 49.0.0 + '@expo/json-file': 8.3.3 + getenv: 1.0.0 + glob: 7.1.6 + require-from-string: 2.0.2 + resolve-from: 5.0.0 + semver: 7.5.3 + slugify: 1.6.6 + sucrase: 3.35.1 + transitivePeerDependencies: + - supports-color + '@expo/devcert@1.2.0': dependencies: '@expo/sudo-prompt': 9.3.2 @@ -23172,6 +24397,13 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + '@expo/devtools@0.1.7(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + chalk: 4.1.2 + optionalDependencies: + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + '@expo/devtools@0.1.7(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: chalk: 4.1.2 @@ -23246,6 +24478,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/image-utils@0.3.22': + dependencies: + '@expo/spawn-async': 1.5.0 + chalk: 4.1.2 + fs-extra: 9.0.0 + getenv: 1.0.0 + jimp-compact: 0.16.1 + mime: 2.6.0 + node-fetch: 2.7.0 + parse-png: 2.1.0 + resolve-from: 5.0.0 + semver: 7.3.2 + tempy: 0.3.0 + transitivePeerDependencies: + - encoding + '@expo/image-utils@0.6.5': dependencies: '@expo/spawn-async': 1.7.2 @@ -23277,6 +24525,18 @@ snapshots: '@babel/code-frame': 7.10.4 json5: 2.2.3 + '@expo/json-file@8.2.37': + dependencies: + '@babel/code-frame': 7.10.4 + json5: 2.2.3 + write-file-atomic: 2.4.3 + + '@expo/json-file@8.3.3': + dependencies: + '@babel/code-frame': 7.10.4 + json5: 2.2.3 + write-file-atomic: 2.4.3 + '@expo/json-file@9.0.2': dependencies: '@babel/code-frame': 7.10.4 @@ -23427,6 +24687,10 @@ snapshots: dependencies: react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + '@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))': + dependencies: + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + '@expo/metro-runtime@6.1.2(expo@52.0.47)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': dependencies: anser: 1.4.10 @@ -23453,6 +24717,19 @@ snapshots: react-dom: 18.3.1(react@18.3.1) optional: true + '@expo/metro-runtime@6.1.2(expo@52.0.47)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + anser: 1.4.10 + expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + pretty-format: 29.7.0 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + optional: true + '@expo/metro-runtime@6.1.2(expo@54.0.12)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: anser: 1.4.10 @@ -23489,6 +24766,18 @@ snapshots: optionalDependencies: react-dom: 19.1.0(react@19.1.0) + '@expo/metro-runtime@6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + anser: 1.4.10 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + pretty-format: 29.7.0 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + '@expo/metro-runtime@6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: anser: 1.4.10 @@ -23559,6 +24848,12 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 14.0.0 + '@expo/plist@0.0.20': + dependencies: + '@xmldom/xmldom': 0.7.13 + base64-js: 1.5.1 + xmlbuilder: 14.0.0 + '@expo/plist@0.2.2': dependencies: '@xmldom/xmldom': 0.7.13 @@ -23625,6 +24920,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/prebuild-config@6.3.0(expo-modules-autolinking@3.0.22)': + dependencies: + '@expo/config': 8.2.1 + '@expo/config-plugins': 7.3.1 + '@expo/config-types': 49.0.0 + '@expo/image-utils': 0.3.22 + '@expo/json-file': 8.3.3 + debug: 4.4.3 + expo-modules-autolinking: 3.0.22 + fs-extra: 9.1.0 + resolve-from: 5.0.0 + semver: 7.5.3 + xml2js: 0.6.0 + transitivePeerDependencies: + - encoding + - supports-color + '@expo/prebuild-config@8.2.0': dependencies: '@expo/config': 10.0.11 @@ -23666,6 +24978,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/spawn-async@1.5.0': + dependencies: + cross-spawn: 6.0.6 + '@expo/spawn-async@1.7.2': dependencies: cross-spawn: 7.0.6 @@ -23702,6 +25018,12 @@ snapshots: react: 18.3.1 react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + '@expo/vector-icons@14.1.0(expo-font@14.0.9(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + expo-font: 14.0.9(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + '@expo/vector-icons@15.0.3(expo-font@14.0.9(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: expo-font: 14.0.9(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -23720,6 +25042,12 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + '@expo/vector-icons@15.0.3(expo-font@14.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + expo-font: 14.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + '@expo/vector-icons@15.0.3(expo-font@14.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: expo-font: 14.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -24782,6 +26110,8 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': optional: true + '@kurkle/color@0.3.4': {} + '@ljharb/through@2.3.14': dependencies: call-bind: 1.0.8 @@ -25449,6 +26779,18 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + '@types/react-dom': 19.2.3(@types/react@19.1.17) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.1.0) @@ -25472,6 +26814,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.17)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.1.0)': dependencies: react: 19.1.0 @@ -25498,12 +26846,40 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-context@1.1.2(@types/react@19.1.17)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-context@1.1.2(@types/react@19.2.7)(react@19.1.0)': dependencies: react: 19.1.0 optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.17)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.1(@types/react@19.1.17)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + '@types/react-dom': 19.2.3(@types/react@19.1.17) + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -25532,6 +26908,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-direction@1.1.1(@types/react@19.1.17)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-direction@1.1.1(@types/react@19.2.7)(react@19.1.0)': dependencies: react: 19.1.0 @@ -25551,6 +26933,19 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + '@types/react-dom': 19.2.3(@types/react@19.1.17) + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -25585,6 +26980,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.1.17)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.7)(react@19.1.0)': dependencies: react: 19.1.0 @@ -25602,6 +27003,17 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + '@types/react-dom': 19.2.3(@types/react@19.1.17) + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.1.0) @@ -25620,6 +27032,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-id@1.1.1(@types/react@19.1.17)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-id@1.1.1(@types/react@19.2.7)(react@19.1.0)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.1.0) @@ -25681,6 +27100,16 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + '@types/react-dom': 19.2.3(@types/react@19.1.17) + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -25701,6 +27130,16 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + '@types/react-dom': 19.2.3(@types/react@19.1.17) + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.1.0) @@ -25720,6 +27159,15 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + '@types/react-dom': 19.2.3(@types/react@19.1.17) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.1.0) @@ -25746,6 +27194,23 @@ snapshots: '@types/react': 18.3.27 '@types/react-dom': 19.2.3(@types/react@18.3.27) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + '@types/react-dom': 19.2.3(@types/react@19.1.17) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -25769,6 +27234,13 @@ snapshots: '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) react: 18.3.1 + '@radix-ui/react-slot@1.2.0(@types/react@19.1.17)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-slot@1.2.0(@types/react@19.2.7)(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.1.0) @@ -25783,6 +27255,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-slot@1.2.3(@types/react@19.1.17)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-slot@1.2.3(@types/react@19.2.7)(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.1.0) @@ -25790,6 +27269,22 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + '@types/react-dom': 19.2.3(@types/react@19.1.17) + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -25812,6 +27307,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.17)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.1.0)': dependencies: react: 19.1.0 @@ -25826,6 +27327,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.17)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.7)(react@19.1.0)': dependencies: '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.7)(react@19.1.0) @@ -25841,6 +27350,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.17)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.7)(react@19.1.0)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.1.0) @@ -25855,6 +27371,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.17)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.17)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.7)(react@19.1.0)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.1.0) @@ -25868,6 +27391,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.17)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.17 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.7)(react@19.1.0)': dependencies: react: 19.1.0 @@ -25895,11 +27424,21 @@ snapshots: merge-options: 3.0.4 react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + '@react-native-async-storage/async-storage@1.23.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))': + dependencies: + merge-options: 3.0.4 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + '@react-native-async-storage/async-storage@2.2.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))': dependencies: merge-options: 3.0.4 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + '@react-native-async-storage/async-storage@2.2.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))': + dependencies: + merge-options: 3.0.4 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + '@react-native-async-storage/async-storage@2.2.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))': dependencies: merge-options: 3.0.4 @@ -25909,6 +27448,8 @@ snapshots: dependencies: react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + '@react-native-community/slider@5.1.1': {} + '@react-native-google-signin/google-signin@14.0.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: react: 19.1.0 @@ -25931,6 +27472,8 @@ snapshots: '@react-native/assets-registry@0.76.7': {} + '@react-native/assets-registry@0.76.9': {} + '@react-native/assets-registry@0.81.4': {} '@react-native/assets-registry@0.81.5': {} @@ -26271,6 +27814,27 @@ snapshots: - supports-color - utf-8-validate + '@react-native/community-cli-plugin@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))': + dependencies: + '@react-native/dev-middleware': 0.76.9 + '@react-native/metro-babel-transformer': 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5)) + chalk: 4.1.2 + execa: 5.1.1 + invariant: 2.2.4 + metro: 0.81.5 + metro-config: 0.81.5 + metro-core: 0.81.5 + node-fetch: 2.7.0 + readline: 1.3.0 + semver: 7.7.3 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - bufferutil + - encoding + - supports-color + - utf-8-validate + '@react-native/community-cli-plugin@0.81.4': dependencies: '@react-native/dev-middleware': 0.81.4 @@ -26405,6 +27969,8 @@ snapshots: '@react-native/gradle-plugin@0.76.7': {} + '@react-native/gradle-plugin@0.76.9': {} + '@react-native/gradle-plugin@0.81.4': {} '@react-native/gradle-plugin@0.81.5': {} @@ -26413,6 +27979,8 @@ snapshots: '@react-native/js-polyfills@0.76.7': {} + '@react-native/js-polyfills@0.76.9': {} + '@react-native/js-polyfills@0.81.4': {} '@react-native/js-polyfills@0.81.5': {} @@ -26437,6 +28005,18 @@ snapshots: - '@babel/preset-env' - supports-color + '@react-native/metro-babel-transformer@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))': + dependencies: + '@babel/core': 7.28.5 + '@react-native/babel-preset': 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5)) + hermes-parser: 0.23.1 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + + '@react-native/normalize-color@2.1.0': {} + '@react-native/normalize-colors@0.74.89': {} '@react-native/normalize-colors@0.76.3': {} @@ -26471,6 +28051,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + '@react-native/virtualized-lists@0.76.9(@types/react@18.3.27)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@react-native/virtualized-lists@0.81.4(@types/react@19.2.7)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: invariant: 2.2.4 @@ -26480,6 +28069,15 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@react-native/virtualized-lists@0.81.5(@types/react@19.1.17)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + '@react-native/virtualized-lists@0.81.5(@types/react@19.2.7)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@18.3.1))(react@18.3.1)': dependencies: invariant: 2.2.4 @@ -26524,6 +28122,19 @@ snapshots: transitivePeerDependencies: - '@react-native-masked-view/masked-view' + '@react-navigation/bottom-tabs@7.8.6(@react-navigation/native@7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.4.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@react-navigation/native': 7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + color: 4.2.3 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-safe-area-context: 4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-screens: 4.4.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + sf-symbols-typescript: 2.1.0 + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + '@react-navigation/bottom-tabs@7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -26537,6 +28148,19 @@ snapshots: transitivePeerDependencies: - '@react-native-masked-view/masked-view' + '@react-navigation/bottom-tabs@7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + color: 4.2.3 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + sf-symbols-typescript: 2.1.0 + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + '@react-navigation/bottom-tabs@7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -26574,6 +28198,23 @@ snapshots: use-latest-callback: 0.2.6(react@19.1.0) use-sync-external-store: 1.6.0(react@19.1.0) + '@react-navigation/drawer@7.7.4(7nnntr3f2j3s2v7n65dx3d6u7a)': + dependencies: + '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@react-navigation/native': 7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + color: 4.2.3 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-drawer-layout: 4.2.0(react-native-gesture-handler@2.20.2(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-gesture-handler: 2.20.2(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-reanimated: 3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-safe-area-context: 4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-screens: 4.4.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + use-latest-callback: 0.2.6(react@18.3.1) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + optional: true + '@react-navigation/drawer@7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -26641,6 +28282,23 @@ snapshots: - '@react-native-masked-view/masked-view' optional: true + '@react-navigation/drawer@7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + color: 4.2.3 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-drawer-layout: 4.2.0(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + use-latest-callback: 0.2.6(react@19.1.0) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + optional: true + '@react-navigation/drawer@7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -26710,6 +28368,16 @@ snapshots: use-latest-callback: 0.2.6(react@18.3.1) use-sync-external-store: 1.6.0(react@18.3.1) + '@react-navigation/elements@2.8.3(@react-navigation/native@7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-navigation/native': 7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + color: 4.2.3 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-safe-area-context: 4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + use-latest-callback: 0.2.6(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + '@react-navigation/elements@2.8.3(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: '@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -26720,6 +28388,16 @@ snapshots: use-latest-callback: 0.2.6(react@19.1.0) use-sync-external-store: 1.6.0(react@19.1.0) + '@react-navigation/elements@2.8.3(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + color: 4.2.3 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + use-latest-callback: 0.2.6(react@19.1.0) + use-sync-external-store: 1.6.0(react@19.1.0) + '@react-navigation/elements@2.8.3(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -26758,6 +28436,20 @@ snapshots: transitivePeerDependencies: - '@react-native-masked-view/masked-view' + '@react-navigation/native-stack@7.8.0(@react-navigation/native@7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.4.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@react-navigation/native': 7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + color: 4.2.3 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-safe-area-context: 4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-screens: 4.4.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + sf-symbols-typescript: 2.1.0 + warn-once: 0.1.1 + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + '@react-navigation/native-stack@7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -26772,6 +28464,20 @@ snapshots: transitivePeerDependencies: - '@react-native-masked-view/masked-view' + '@react-navigation/native-stack@7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + color: 4.2.3 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + sf-symbols-typescript: 2.1.0 + warn-once: 0.1.1 + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + '@react-navigation/native-stack@7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: '@react-navigation/elements': 2.8.3(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -26806,6 +28512,16 @@ snapshots: react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) use-latest-callback: 0.2.6(react@18.3.1) + '@react-navigation/native@7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-navigation/core': 7.13.2(react@18.3.1) + escape-string-regexp: 4.0.0 + fast-deep-equal: 3.1.3 + nanoid: 3.3.11 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + use-latest-callback: 0.2.6(react@18.3.1) + '@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: '@react-navigation/core': 7.13.2(react@19.1.0) @@ -26816,6 +28532,16 @@ snapshots: react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) use-latest-callback: 0.2.6(react@19.1.0) + '@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-navigation/core': 7.13.2(react@19.1.0) + escape-string-regexp: 4.0.0 + fast-deep-equal: 3.1.3 + nanoid: 3.3.11 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + use-latest-callback: 0.2.6(react@19.1.0) + '@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: '@react-navigation/core': 7.13.2(react@19.1.0) @@ -27856,6 +29582,19 @@ snapshots: optionalDependencies: jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + jest-matcher-utils: 30.2.0 + picocolors: 1.1.1 + pretty-format: 30.2.0 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-test-renderer: 19.1.0(react@19.1.0) + redent: 3.0.0 + optionalDependencies: + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + optional: true + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 @@ -27966,6 +29705,123 @@ snapshots: '@types/node': 22.19.1 optional: true + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -28001,6 +29857,8 @@ snapshots: dependencies: '@types/node': 22.19.1 + '@types/geojson@7946.0.16': {} + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 22.19.1 @@ -28122,6 +29980,11 @@ snapshots: '@types/react': 18.3.27 optional: true + '@types/react-dom@19.2.3(@types/react@19.1.17)': + dependencies: + '@types/react': 19.1.17 + optional: true + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -28131,6 +29994,10 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/react@19.1.17': + dependencies: + csstype: 3.2.3 + '@types/react@19.2.7': dependencies: csstype: 3.2.3 @@ -28182,6 +30049,15 @@ snapshots: fflate: 0.8.2 meshoptimizer: 0.18.1 + '@types/topojson-client@3.1.5': + dependencies: + '@types/geojson': 7946.0.16 + '@types/topojson-specification': 1.0.5 + + '@types/topojson-specification@1.0.5': + dependencies: + '@types/geojson': 7946.0.16 + '@types/tough-cookie@4.0.5': {} '@types/triple-beam@1.3.5': {} @@ -30385,6 +32261,10 @@ snapshots: charenc@0.0.2: {} + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + check-disk-space@3.4.0: {} check-error@2.1.1: {} @@ -30834,6 +32714,8 @@ snapshots: crypt@0.0.2: {} + crypto-random-string@1.0.0: {} + crypto-random-string@2.0.0: {} css-color-keywords@1.0.0: {} @@ -30903,6 +32785,158 @@ snapshots: csv-parse@6.1.0: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + d@1.0.2: dependencies: es5-ext: 0.10.64 @@ -31027,6 +33061,10 @@ snapshots: rimraf: 3.0.2 slash: 3.0.0 + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + delayed-stream@1.0.0: {} delegates@1.0.0: {} @@ -32454,6 +34492,18 @@ snapshots: transitivePeerDependencies: - supports-color + expo-asset@11.0.5(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + '@expo/image-utils': 0.6.5 + expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-constants: 17.0.8(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + invariant: 2.2.4 + md5-file: 3.2.3 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - supports-color + expo-asset@12.0.10(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: '@expo/image-utils': 0.8.7 @@ -32484,6 +34534,16 @@ snapshots: transitivePeerDependencies: - supports-color + expo-asset@12.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + '@expo/image-utils': 0.8.7 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + transitivePeerDependencies: + - supports-color + expo-asset@12.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: '@expo/image-utils': 0.8.7 @@ -32494,12 +34554,26 @@ snapshots: transitivePeerDependencies: - supports-color + expo-av@16.0.7(expo@54.0.25)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + optionalDependencies: + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + expo-blur@14.0.3(expo@52.0.47)(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): dependencies: expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.12.2(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) react: 18.3.1 react-native: 0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + expo-blur@14.0.3(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + expo-blur@15.0.7(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -32518,12 +34592,26 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + expo-brightness@14.0.7(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)): + dependencies: + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + expo-build-properties@1.0.9(expo@54.0.13): dependencies: ajv: 8.17.1 expo: 54.0.13(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) semver: 7.7.3 + expo-camera@17.0.9(expo@54.0.25)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + invariant: 2.2.4 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + optionalDependencies: + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + expo-clipboard@8.0.7(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -32554,6 +34642,15 @@ snapshots: transitivePeerDependencies: - supports-color + expo-constants@17.0.8(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)): + dependencies: + '@expo/config': 10.0.11 + '@expo/env': 0.4.2 + expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - supports-color + expo-constants@18.0.10(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: '@expo/config': 12.0.10 @@ -32581,6 +34678,15 @@ snapshots: transitivePeerDependencies: - supports-color + expo-constants@18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)): + dependencies: + '@expo/config': 12.0.10 + '@expo/env': 2.0.7 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + transitivePeerDependencies: + - supports-color + expo-constants@18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: '@expo/config': 12.0.10 @@ -32719,6 +34825,11 @@ snapshots: expo: 54.0.13(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) ua-parser-js: 0.7.41 + expo-device@8.0.9(expo@54.0.25): + dependencies: + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + ua-parser-js: 0.7.41 + expo-document-picker@14.0.7(expo@54.0.25): dependencies: expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -32737,6 +34848,12 @@ snapshots: react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) web-streams-polyfill: 3.3.3 + expo-file-system@18.0.12(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)): + dependencies: + expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + web-streams-polyfill: 3.3.3 + expo-file-system@19.0.19(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -32752,6 +34869,11 @@ snapshots: expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + expo-file-system@19.0.19(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)): + dependencies: + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + expo-file-system@19.0.19(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -32770,6 +34892,13 @@ snapshots: react: 18.3.1 react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + expo-font@14.0.9(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + fontfaceobserver: 2.3.0 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + expo-font@14.0.9(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -32791,6 +34920,13 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + expo-font@14.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + fontfaceobserver: 2.3.0 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + expo-font@14.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -32872,6 +35008,12 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + expo-linear-gradient@15.0.7(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + expo-linking@7.0.5(expo@52.0.47)(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): dependencies: expo-constants: 17.0.8(expo@52.0.47)(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) @@ -32892,6 +35034,16 @@ snapshots: - expo - supports-color + expo-linking@7.0.5(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + expo-constants: 17.0.8(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + invariant: 2.2.4 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - expo + - supports-color + expo-linking@8.0.9(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: expo-constants: 18.0.10(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) @@ -32922,6 +35074,16 @@ snapshots: - expo - supports-color + expo-linking@8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + invariant: 2.2.4 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + transitivePeerDependencies: + - expo + - supports-color + expo-linking@8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) @@ -33046,6 +35208,12 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + expo-modules-core@3.0.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + invariant: 2.2.4 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + expo-modules-core@3.0.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: invariant: 2.2.4 @@ -33112,6 +35280,82 @@ snapshots: - react-native - supports-color + expo-router@4.0.21(uybeljqs5zibgesoubkrrbh7bq): + dependencies: + '@expo/metro-runtime': 4.0.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + '@expo/server': 0.5.3 + '@radix-ui/react-slot': 1.0.1(react@18.3.1) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.4.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@react-navigation/native': 7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.4.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + client-only: 0.0.1 + expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-constants: 17.0.8(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + expo-linking: 7.0.5(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-native-helmet-async: 2.0.4(react@18.3.1) + react-native-is-edge-to-edge: 1.2.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-safe-area-context: 4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-screens: 4.4.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + schema-utils: 4.3.3 + semver: 7.6.3 + server-only: 0.0.1 + optionalDependencies: + '@react-navigation/drawer': 7.7.4(7nnntr3f2j3s2v7n65dx3d6u7a) + react-native-reanimated: 3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - react + - react-dom + - react-native + - supports-color + + expo-router@6.0.15(5vziatpev6vbpelxbu5bibycr4): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.1.17)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + expo-router@6.0.15(dhhg3hnzbvo62ldg5nj2klgrdy): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.12)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -33420,6 +35664,15 @@ snapshots: dependencies: expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-splash-screen@0.21.1(expo-modules-autolinking@3.0.22)(expo@54.0.25): + dependencies: + '@expo/prebuild-config': 6.3.0(expo-modules-autolinking@3.0.22) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + transitivePeerDependencies: + - encoding + - expo-modules-autolinking + - supports-color + expo-splash-screen@0.29.24(expo@52.0.47): dependencies: '@expo/prebuild-config': 8.2.0 @@ -33445,12 +35698,23 @@ snapshots: react: 18.3.1 react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + expo-status-bar@2.0.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + expo-status-bar@3.0.8(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-status-bar@3.0.8(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-status-bar@3.0.8(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -33476,6 +35740,12 @@ snapshots: react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) sf-symbols-typescript: 2.1.0 + expo-symbols@1.0.7(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)): + dependencies: + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + sf-symbols-typescript: 2.1.0 + expo-system-ui@4.0.9(expo@52.0.47)(react-native-web@0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)): dependencies: '@react-native/normalize-colors': 0.76.8 @@ -33498,6 +35768,17 @@ snapshots: transitivePeerDependencies: - supports-color + expo-system-ui@4.0.9(expo@52.0.47)(react-native-web@0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)): + dependencies: + '@react-native/normalize-colors': 0.76.8 + debug: 4.4.3 + expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + react-native-web: 0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - supports-color + expo-system-ui@6.0.8(expo@54.0.12)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: '@react-native/normalize-colors': 0.81.5 @@ -33531,6 +35812,17 @@ snapshots: transitivePeerDependencies: - supports-color + expo-system-ui@6.0.8(expo@54.0.25)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)): + dependencies: + '@react-native/normalize-colors': 0.81.5 + debug: 4.4.3 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + optionalDependencies: + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + transitivePeerDependencies: + - supports-color + expo-system-ui@6.0.8(expo@54.0.25)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: '@react-native/normalize-colors': 0.81.5 @@ -33586,6 +35878,11 @@ snapshots: expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + expo-web-browser@14.0.2(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)): + dependencies: + expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + expo-web-browser@15.0.9(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -33601,6 +35898,11 @@ snapshots: expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + expo-web-browser@15.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)): + dependencies: + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + expo-web-browser@15.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -33678,6 +35980,42 @@ snapshots: - supports-color - utf-8-validate + expo@52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@expo/cli': 0.22.26 + '@expo/config': 10.0.11 + '@expo/config-plugins': 9.0.17 + '@expo/fingerprint': 0.11.11 + '@expo/metro-config': 0.19.12 + '@expo/vector-icons': 14.0.4 + babel-preset-expo: 12.0.11(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5)) + expo-asset: 11.0.5(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + expo-constants: 17.0.8(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + expo-file-system: 18.0.12(expo@52.0.47)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + expo-font: 13.0.4(expo@52.0.47)(react@18.3.1) + expo-keep-awake: 14.0.3(expo@52.0.47)(react@18.3.1) + expo-modules-autolinking: 2.0.8 + expo-modules-core: 2.2.3 + fbemitter: 3.0.0 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + web-streams-polyfill: 3.3.3 + whatwg-url-without-unicode: 8.0.0-3 + optionalDependencies: + '@expo/metro-runtime': 6.1.2(expo@52.0.47)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-webview: 13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - babel-plugin-react-compiler + - bufferutil + - encoding + - graphql + - react-compiler-runtime + - supports-color + - utf-8-validate + expo@54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.28.4 @@ -33789,6 +36127,43 @@ snapshots: - supports-color - utf-8-validate + expo@54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + '@babel/runtime': 7.28.4 + '@expo/cli': 54.0.16(expo-router@6.0.15)(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + '@expo/config': 12.0.10 + '@expo/config-plugins': 54.0.2 + '@expo/devtools': 0.1.7(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@expo/fingerprint': 0.15.3 + '@expo/metro': 54.1.0 + '@expo/metro-config': 54.0.9(expo@54.0.25) + '@expo/vector-icons': 15.0.3(expo-font@14.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@ungap/structured-clone': 1.3.0 + babel-preset-expo: 54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25)(react-refresh@0.14.2) + expo-asset: 12.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + expo-file-system: 19.0.19(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + expo-font: 14.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-keep-awake: 15.0.7(expo@54.0.25)(react@19.1.0) + expo-modules-autolinking: 3.0.22 + expo-modules-core: 3.0.26(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + pretty-format: 29.7.0 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-refresh: 0.14.2 + whatwg-url-without-unicode: 8.0.0-3 + optionalDependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-webview: 13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + transitivePeerDependencies: + - '@babel/core' + - '@modelcontextprotocol/sdk' + - bufferutil + - expo-router + - graphql + - supports-color + - utf-8-validate + expo@54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.28.4 @@ -34497,6 +36872,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.1 + glob@7.1.6: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -35046,6 +37430,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@2.0.3: {} + intl-messageformat@10.7.18: dependencies: '@formatjs/ecma402-abstract': 2.3.6 @@ -36914,6 +39300,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lru-queue@0.1.0: dependencies: es5-ext: 0.10.64 @@ -38269,6 +40659,20 @@ snapshots: - react-native-svg - supports-color + nativewind@4.2.1(react-native-reanimated@3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-svg@15.12.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + comment-json: 4.4.1 + debug: 4.4.3 + react-native-css-interop: 0.2.1(react-native-reanimated@3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-svg@15.12.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - react + - react-native + - react-native-reanimated + - react-native-safe-area-context + - react-native-svg + - supports-color + nativewind@4.2.1(react-native-reanimated@4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)): dependencies: comment-json: 4.4.1 @@ -38311,6 +40715,20 @@ snapshots: - react-native-svg - supports-color + nativewind@4.2.1(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + comment-json: 4.4.1 + debug: 4.4.3 + react-native-css-interop: 0.2.1(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - react + - react-native + - react-native-reanimated + - react-native-safe-area-context + - react-native-svg + - supports-color + nativewind@4.2.1(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)): dependencies: comment-json: 4.4.1 @@ -39296,6 +41714,24 @@ snapshots: transitivePeerDependencies: - supports-color + react-native-css-interop@0.2.1(react-native-reanimated@3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-svg@15.12.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + debug: 4.4.3 + lightningcss: 1.27.0 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-reanimated: 3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + semver: 7.7.3 + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + optionalDependencies: + react-native-safe-area-context: 4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-svg: 15.12.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - supports-color + react-native-css-interop@0.2.1(react-native-reanimated@4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@babel/helper-module-imports': 7.27.1 @@ -39348,6 +41784,23 @@ snapshots: transitivePeerDependencies: - supports-color + react-native-css-interop@0.2.1(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + debug: 4.4.3 + lightningcss: 1.27.0 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + semver: 7.7.3 + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + optionalDependencies: + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + transitivePeerDependencies: + - supports-color + react-native-css-interop@0.2.1(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@babel/helper-module-imports': 7.27.1 @@ -39402,6 +41855,16 @@ snapshots: - '@babel/core' - supports-color + react-native-draggable-flatlist@4.0.3(@babel/core@7.28.5)(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)): + dependencies: + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + react-native-drawer-layout@4.2.0(react-native-gesture-handler@2.20.2(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.16.2(@babel/core@7.28.5)(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): dependencies: color: 4.2.3 @@ -39421,6 +41884,16 @@ snapshots: react-native-reanimated: 3.16.2(@babel/core@7.28.5)(react-native@0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) use-latest-callback: 0.2.6(react@18.3.1) + react-native-drawer-layout@4.2.0(react-native-gesture-handler@2.20.2(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + color: 4.2.3 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-gesture-handler: 2.20.2(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + react-native-reanimated: 3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + use-latest-callback: 0.2.6(react@18.3.1) + optional: true + react-native-drawer-layout@4.2.0(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: color: 4.2.3 @@ -39460,6 +41933,16 @@ snapshots: use-latest-callback: 0.2.6(react@19.1.0) optional: true + react-native-drawer-layout@4.2.0(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + color: 4.2.3 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + use-latest-callback: 0.2.6(react@19.1.0) + optional: true + react-native-drawer-layout@4.2.0(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: color: 4.2.3 @@ -39491,6 +41974,15 @@ snapshots: react: 18.3.1 react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-gesture-handler@2.20.2(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + '@egjs/hammerjs': 2.0.17 + hoist-non-react-statics: 3.3.2 + invariant: 2.2.4 + prop-types: 15.8.1 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: '@egjs/hammerjs': 2.0.17 @@ -39499,6 +41991,14 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + '@egjs/hammerjs': 2.0.17 + hoist-non-react-statics: 3.3.2 + invariant: 2.2.4 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: '@egjs/hammerjs': 2.0.17 @@ -39528,6 +42028,10 @@ snapshots: react: 18.3.1 react-native: 0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-iphone-x-helper@1.3.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)): + dependencies: + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-is-edge-to-edge@1.2.1(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -39538,11 +42042,21 @@ snapshots: react: 18.3.1 react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-is-edge-to-edge@1.2.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-is-edge-to-edge@1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge@1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-is-edge-to-edge@1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -39553,6 +42067,12 @@ snapshots: react: 19.1.0 react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-keyboard-aware-scroll-view@0.9.5(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)): + dependencies: + prop-types: 15.8.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-iphone-x-helper: 1.3.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)) + react-native-markdown-display@7.0.2(react-native@0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): dependencies: css-to-react-native: 3.2.0 @@ -39562,6 +42082,11 @@ snapshots: react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) react-native-fit-image: 1.5.5 + react-native-nitro-modules@0.31.10(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-pager-view@6.9.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -39614,6 +42139,25 @@ snapshots: transitivePeerDependencies: - supports-color + react-native-reanimated@3.16.2(@babel/core@7.28.5)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.5) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.5) + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + convert-source-map: 2.0.0 + invariant: 2.2.4 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + transitivePeerDependencies: + - supports-color + react-native-reanimated@4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: '@babel/core': 7.28.5 @@ -39641,6 +42185,15 @@ snapshots: react-native-worklets: 0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) semver: 7.7.2 + react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + '@babel/core': 7.28.5 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-worklets: 0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + semver: 7.7.2 + react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: '@babel/core': 7.28.5 @@ -39678,6 +42231,11 @@ snapshots: react: 18.3.1 react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + react-native-safe-area-context@4.5.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -39688,6 +42246,11 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -39713,6 +42276,14 @@ snapshots: react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) warn-once: 0.1.1 + react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-freeze: 1.0.4(react@19.1.0) + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + warn-once: 0.1.1 + react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -39728,10 +42299,25 @@ snapshots: react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) warn-once: 0.1.1 + react-native-screens@4.4.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-freeze: 1.0.4(react@18.3.1) + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + warn-once: 0.1.1 + react-native-shared-group-preferences@1.1.24(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)): dependencies: react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-svg@15.12.1(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + css-select: 5.2.2 + css-tree: 1.1.3 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + warn-once: 0.1.1 + react-native-svg@15.12.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: css-select: 5.2.2 @@ -39742,6 +42328,12 @@ snapshots: react-native-swipe-gestures@1.0.5: {} + react-native-torch-nitro@0.0.1(react-native-nitro-modules@0.31.10(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-nitro-modules: 0.31.10(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-web@0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.28.4 @@ -39787,6 +42379,14 @@ snapshots: react-native: 0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) optional: true + react-native-webview@13.16.0(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1): + dependencies: + escape-string-regexp: 4.0.0 + invariant: 2.2.4 + react: 18.3.1 + react-native: 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1) + optional: true + react-native-webview@13.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: escape-string-regexp: 4.0.0 @@ -39794,6 +42394,14 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + escape-string-regexp: 4.0.0 + invariant: 2.2.4 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + optional: true + react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: escape-string-regexp: 4.0.0 @@ -39821,6 +42429,25 @@ snapshots: transitivePeerDependencies: - supports-color + react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.5) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.28.5) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.5) + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + convert-source-map: 2.0.0 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: '@babel/core': 7.28.5 @@ -39989,6 +42616,58 @@ snapshots: - supports-color - utf-8-validate + react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native/assets-registry': 0.76.9 + '@react-native/codegen': 0.76.9(@babel/preset-env@7.28.5(@babel/core@7.28.5)) + '@react-native/community-cli-plugin': 0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5)) + '@react-native/gradle-plugin': 0.76.9 + '@react-native/js-polyfills': 0.76.9 + '@react-native/normalize-colors': 0.76.9 + '@react-native/virtualized-lists': 0.76.9(@types/react@18.3.27)(react-native@0.76.9(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-jest: 29.7.0(@babel/core@7.28.5) + babel-plugin-syntax-hermes-parser: 0.23.1 + base64-js: 1.5.1 + chalk: 4.1.2 + commander: 12.1.0 + event-target-shim: 5.0.1 + flow-enums-runtime: 0.0.6 + glob: 7.2.3 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + jsc-android: 250231.0.0 + memoize-one: 5.2.1 + metro-runtime: 0.81.5 + metro-source-map: 0.81.5 + mkdirp: 0.5.6 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 18.3.1 + react-devtools-core: 5.3.2 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.24.0-canary-efb381bbf-20230505 + semver: 7.7.3 + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + ws: 6.2.3 + yargs: 17.7.2 + optionalDependencies: + '@types/react': 18.3.27 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - '@react-native-community/cli' + - bufferutil + - encoding + - supports-color + - utf-8-validate + react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0): dependencies: '@jest/create-cache-key-function': 29.7.0 @@ -40036,6 +42715,53 @@ snapshots: - supports-color - utf-8-validate + react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native/assets-registry': 0.81.5 + '@react-native/codegen': 0.81.5(@babel/core@7.28.5) + '@react-native/community-cli-plugin': 0.81.5 + '@react-native/gradle-plugin': 0.81.5 + '@react-native/js-polyfills': 0.81.5 + '@react-native/normalize-colors': 0.81.5 + '@react-native/virtualized-lists': 0.81.5(@types/react@19.1.17)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-jest: 29.7.0(@babel/core@7.28.5) + babel-plugin-syntax-hermes-parser: 0.29.1 + base64-js: 1.5.1 + commander: 12.1.0 + flow-enums-runtime: 0.0.6 + glob: 7.2.3 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + memoize-one: 5.2.1 + metro-runtime: 0.83.3 + metro-source-map: 0.83.3 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.1.0 + react-devtools-core: 6.1.5 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.26.0 + semver: 7.7.3 + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + ws: 6.2.3 + yargs: 17.7.2 + optionalDependencies: + '@types/react': 19.1.17 + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil + - supports-color + - utf-8-validate + react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@18.3.1): dependencies: '@jest/create-cache-key-function': 29.7.0 @@ -40142,6 +42868,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + react-remove-scroll-bar@2.3.8(@types/react@19.1.17)(react@19.1.0): + dependencies: + react: 19.1.0 + react-style-singleton: 2.2.3(@types/react@19.1.17)(react@19.1.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.17 + react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.1.0): dependencies: react: 19.1.0 @@ -40161,6 +42895,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + react-remove-scroll@2.7.1(@types/react@19.1.17)(react@19.1.0): + dependencies: + react: 19.1.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.1.17)(react@19.1.0) + react-style-singleton: 2.2.3(@types/react@19.1.17)(react@19.1.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.1.17)(react@19.1.0) + use-sidecar: 1.1.3(@types/react@19.1.17)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + react-remove-scroll@2.7.1(@types/react@19.2.7)(react@19.1.0): dependencies: react: 19.1.0 @@ -40199,6 +42944,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + react-style-singleton@2.2.3(@types/react@19.1.17)(react@19.1.0): + dependencies: + get-nonce: 1.0.1 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.17 + react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.1.0): dependencies: get-nonce: 1.0.1 @@ -40545,6 +43298,8 @@ snapshots: dependencies: glob: 10.5.0 + robust-predicates@3.0.2: {} + rollup@4.53.3: dependencies: '@types/estree': 1.0.8 @@ -40597,6 +43352,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rw@1.3.3: {} + rxjs@7.8.1: dependencies: tslib: 2.8.1 @@ -40689,6 +43446,12 @@ snapshots: semver@6.3.1: {} + semver@7.3.2: {} + + semver@7.5.3: + dependencies: + lru-cache: 6.0.0 + semver@7.6.3: {} semver@7.7.2: {} @@ -41308,6 +44071,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte-chartjs@3.1.5(chart.js@4.5.1)(svelte@5.44.0): + dependencies: + chart.js: 4.5.1 + svelte: 5.44.0 + svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -41441,12 +44209,20 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + temp-dir@1.0.0: {} + temp-dir@2.0.0: {} temp@0.8.4: dependencies: rimraf: 2.6.3 + tempy@0.3.0: + dependencies: + temp-dir: 1.0.0 + type-fest: 0.3.1 + unique-string: 1.0.0 + tempy@0.7.1: dependencies: del: 6.1.1 @@ -41612,6 +44388,10 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + topojson-client@3.1.0: + dependencies: + commander: 2.20.3 + totalist@3.0.1: {} tough-cookie@4.1.4: @@ -41927,6 +44707,8 @@ snapshots: type-fest@0.21.3: {} + type-fest@0.3.1: {} + type-fest@0.7.1: {} type-fest@4.41.0: {} @@ -42093,6 +44875,10 @@ snapshots: dependencies: imurmurhash: 0.1.4 + unique-string@1.0.0: + dependencies: + crypto-random-string: 1.0.0 + unique-string@2.0.0: dependencies: crypto-random-string: 2.0.0 @@ -42215,6 +45001,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + use-callback-ref@1.3.3(@types/react@19.1.17)(react@19.1.0): + dependencies: + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.17 + use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.1.0): dependencies: react: 19.1.0 @@ -42238,6 +45031,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + use-sidecar@1.1.3(@types/react@19.1.17)(react@19.1.0): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.17 + use-sidecar@1.1.3(@types/react@19.2.7)(react@19.1.0): dependencies: detect-node-es: 1.1.0 @@ -42283,6 +45084,15 @@ snapshots: vary@1.1.2: {} + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -43229,6 +46039,13 @@ snapshots: zod@4.1.13: {} + zustand@4.5.7(@types/react@19.1.17)(react@19.1.0): + dependencies: + use-sync-external-store: 1.6.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.17 + react: 19.1.0 + zustand@4.5.7(@types/react@19.2.7)(react@19.1.0): dependencies: use-sync-external-store: 1.6.0(react@19.1.0) diff --git a/scripts/generate-env.mjs b/scripts/generate-env.mjs index 3308a91ac..490a7e578 100644 --- a/scripts/generate-env.mjs +++ b/scripts/generate-env.mjs @@ -536,6 +536,70 @@ const APP_CONFIGS = [ PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, }, }, + + // Moodlit Backend (NestJS) + { + path: 'apps/moodlit/apps/backend/.env', + vars: { + NODE_ENV: () => 'development', + PORT: (env) => env.MOODLIT_BACKEND_PORT || '3012', + DATABASE_URL: (env) => env.MOODLIT_DATABASE_URL, + MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, + DEV_BYPASS_AUTH: () => 'true', + DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000', + CORS_ORIGINS: (env) => env.CORS_ORIGINS, + }, + }, + + // Moodlit Mobile (Expo) + { + path: 'apps/moodlit/apps/mobile/.env', + vars: { + EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.MOODLIT_BACKEND_PORT || '3012'}`, + EXPO_PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, + }, + }, + + // Moodlit Web (SvelteKit) + { + path: 'apps/moodlit/apps/web/.env', + vars: { + PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.MOODLIT_BACKEND_PORT || '3012'}`, + PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, + }, + }, + + // Finance Backend (NestJS) + { + path: 'apps/finance/apps/backend/.env', + vars: { + NODE_ENV: () => 'development', + PORT: (env) => env.FINANCE_BACKEND_PORT || '3019', + DATABASE_URL: (env) => env.FINANCE_DATABASE_URL, + MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, + DEV_BYPASS_AUTH: () => 'true', + DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000', + CORS_ORIGINS: (env) => env.CORS_ORIGINS, + }, + }, + + // Finance Mobile (Expo) + { + path: 'apps/finance/apps/mobile/.env', + vars: { + EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.FINANCE_BACKEND_PORT || '3019'}`, + EXPO_PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, + }, + }, + + // Finance Web (SvelteKit) + { + path: 'apps/finance/apps/web/.env', + vars: { + PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.FINANCE_BACKEND_PORT || '3019'}`, + PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, + }, + }, ]; function main() {