diff --git a/apps/web/.claude/launch.json b/apps/web/.claude/launch.json new file mode 100644 index 0000000..17b92d3 --- /dev/null +++ b/apps/web/.claude/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "beenvoice-dev", + "runtimeExecutable": "bun", + "runtimeArgs": ["dev"], + "port": 3000, + "autoPort": false + } + ] +} diff --git a/apps/web/.dockerignore b/apps/web/.dockerignore new file mode 100644 index 0000000..f9b132d --- /dev/null +++ b/apps/web/.dockerignore @@ -0,0 +1,19 @@ +node_modules +.next +.git +.gitignore +Dockerfile* +docker-compose* +README.md +docs +AGENTS.md +*.log +.DS_Store +.env* +!.env.example +.vscode +.idea +coverage +*.tsbuildinfo +dist +build diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 0000000..1595f35 --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,174 @@ +# ============================================================================= +# beenvoice-web — environment template +# ============================================================================= +# +# Quick start (local dev): +# cp .env.example .env.local +# docker compose -f docker-compose.dev.yml up -d # Postgres + Garage +# bun run db:push # or: bun run db:migrate +# bun run dev +# Garage S3 API: http://localhost:3900 +# +# Quick start (Docker app + Postgres): +# cp .env.example .env +# # edit AUTH_SECRET + public URLs below +# ./scripts/docker-deploy.sh +# +# ----------------------------------------------------------------------------- +# Build-time vs runtime (Docker) +# ----------------------------------------------------------------------------- +# +# Baked into the image at `docker compose build` (rebuild after changes): +# NEXT_PUBLIC_APP_URL +# NEXT_PUBLIC_* branding / theme defaults +# NEXT_PUBLIC_AUTHENTIK_ENABLED +# NEXT_PUBLIC_UMAMI_* +# +# Read from .env when the container starts (restart app after changes): +# AUTH_SECRET, BETTER_AUTH_URL, DATABASE_URL (compose overrides host), +# RESEND_*, DISABLE_SIGNUPS, AUTHENTIK_* secrets, CRON_SECRET +# +# `NEXT_PUBLIC_APP_URL` should still match your public browser URL for SSR, +# emails, and MCP links. In the browser, sign-in uses the current page origin +# automatically so dev works when Next picks another port (e.g. 3002). +# +# Updating production: git pull && ./scripts/docker-deploy.sh +# (or: docker compose up -d --build). Plain `docker compose up -d` does NOT rebuild. +# Migrations run on every app start (idempotent — only pending SQL is applied). + +# ============================================================================= +# Core — required +# ============================================================================= + +# PostgreSQL connection string. +# Local dev (docker-compose.dev.yml): host is localhost +DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres + +# Session signing secret. Required in production. +# Generate: openssl rand -base64 32 +AUTH_SECRET=change-me-generate-a-real-secret + +# Public URL users open in the browser (scheme + host + port if non-standard). +# Must match how you access the app for cookies, OAuth callbacks, and email links. +BETTER_AUTH_URL=http://localhost:3000 + +# Same as BETTER_AUTH_URL in most setups. Embedded in the client bundle at build. +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +# ============================================================================= +# Local development +# ============================================================================= + +NODE_ENV=development + +# Set true when connecting to local Postgres without SSL (default for compose). +DB_DISABLE_SSL=true + +# Dev-only: host ports for `docker compose -f docker-compose.dev.yml`. +POSTGRES_PORT=5432 +GARAGE_API_PORT=3900 + +# Optional: if Next dev picks another port, you do not need to change URLs for +# sign-in — the auth client uses window.location.origin in the browser. + +# ============================================================================= +# Docker Compose (app + database) +# ============================================================================= + +# Host port mapped to container :3000 (WEB_PORT, then PORT, then 3000). +WEB_PORT=3000 + +# App image tag for docker-compose.yml (optional). docker-deploy.sh sets +# beenvoice: automatically; default without it is beenvoice:local. +# BEENVOICE_IMAGE=beenvoice:local + +# Postgres credentials for docker-compose.yml `db` service. +# DATABASE_URL inside the app container is set by compose (host `db`, not localhost). +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=postgres + +# ============================================================================= +# White-label defaults (optional) +# ============================================================================= +# Baked in at Docker build. After first deploy, admins can override many of +# Optional white-label defaults (build-time). Users choose light/dark in Settings. + +NEXT_PUBLIC_BRAND_NAME=beenvoice +NEXT_PUBLIC_BRAND_TAGLINE=Simple and efficient invoicing for freelancers and small businesses +NEXT_PUBLIC_BRAND_LOGO_TEXT=beenvoice +NEXT_PUBLIC_BRAND_ICON=$ + +# ============================================================================= +# Email — Resend (optional) +# ============================================================================= +# Leave blank to disable invoice and password-reset email delivery. + +RESEND_API_KEY= +RESEND_DOMAIN= + +# ============================================================================= +# Analytics — Umami (optional) +# ============================================================================= +# Leave website ID blank to disable. + +NEXT_PUBLIC_UMAMI_WEBSITE_ID= +NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.umami.is/script.js + +# ============================================================================= +# Access control (optional) +# ============================================================================= + +# Block new email/password registrations (default: true / signups off). +# Set DISABLE_SIGNUPS=false to allow new email/password signups. +# DISABLE_SIGNUPS=false + +# Bearer token for POST /api/cron/generate-recurring (recurring invoice cron). +# CRON_SECRET= + +# ============================================================================= +# Receipt storage — S3-compatible (optional) +# ============================================================================= +# When S3_BUCKET + S3_ACCESS_KEY + S3_SECRET_KEY are unset, receipts land in +# .data/receipts/ (dev-friendly). Works with AWS S3, Garage, Cloudflare R2, etc. +# +# S3_ENDPOINT — who can reach Garage? +# • Host dev (bun dev + docker-compose.dev.yml Garage on the host): localhost:3900 +# • App in Docker (docker-compose.yml): http://garage:3900 (Compose service name) +# • Coolify — see docs/COOLIFY.md. Summary: +# - Best: one Compose resource with docker-compose.coolify.yml (app+db+garage). +# - Application + separate Garage: ENOTFOUND garage → set S3_ENDPOINT to +# SERVICE_URL_GARAGE_3900 (public domain) OR http://garage-:3900 +# with Connect to Predefined Network on both resources. Never bare "garage". +# - NEVER use localhost in production — inside the app container that is the app, not Garage. +# +# Local dev with docker-compose.dev.yml Garage (host `bun dev`): +S3_ENDPOINT=http://localhost:3900 +S3_BUCKET=beenvoice-receipts +S3_ACCESS_KEY=GK3515373e4c851ebaad366558 +S3_SECRET_KEY=7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34 +S3_REGION=garage +# S3_FORCE_PATH_STYLE=true # default on when S3_ENDPOINT is set; required for Garage/HTTPS proxy +# +# docker-compose.yml sets S3_ENDPOINT=http://garage:3900 inside the app container +# automatically. S3_ACCESS_KEY / S3_SECRET_KEY must match the garage service env. + +# ============================================================================= +# SSO — Authentik OIDC (optional) +# ============================================================================= +# Set NEXT_PUBLIC_AUTHENTIK_ENABLED=true and rebuild the image to show SSO on +# sign-in. Server secrets are runtime-only (no rebuild needed for secrets). + +NEXT_PUBLIC_AUTHENTIK_ENABLED=false +AUTHENTIK_ISSUER= +AUTHENTIK_CLIENT_ID= +AUTHENTIK_CLIENT_SECRET= +# Optional extra trusted origin for better-auth (defaults derived from issuer). +AUTHENTIK_ORIGIN= + +# ============================================================================= +# Advanced / CI (usually unset) +# ============================================================================= + +# Skip Zod env validation during `next build` (set automatically in Dockerfile). +# SKIP_ENV_VALIDATION=1 diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..c8b57e7 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# database +/prisma/db.sqlite +/prisma/db.sqlite-journal +db.sqlite + +# next.js +/.next/ +/out/ +next-env.d.ts + +# production +/build + +# misc +.DS_Store +*.pem +.data/ + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables +.env +.env.prod +.env*.local +.env*.production + +# typescript +*.tsbuildinfo + +# idea files +.idea diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md new file mode 100644 index 0000000..f2580e3 --- /dev/null +++ b/apps/web/AGENTS.md @@ -0,0 +1,461 @@ +# beenvoice - AI Assistant Rules + +> **Canonical architecture reference:** [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) (stack, routers, schema, auth). This file may lag behind; prefer ARCHITECTURE.md for facts. + +## Project Overview +beenvoice-web is the web app and API for beenvoice — Next.js 16, tRPC 11, Drizzle/PostgreSQL, better-auth, and shadcn/ui. Reliability, security, and professional UX are paramount. + +**Repository:** [git.soconnor.dev/soconnor/beenvoice-web](https://git.soconnor.dev/soconnor/beenvoice-web) + +## Core Development Principles + +### 1. Business-First Approach +- **Priority**: Reliability and security over flashy features +- **User Experience**: Professional, clean, and intuitive interface +- **Data Integrity**: Always validate and sanitize user input +- **Error Handling**: Graceful degradation with clear user feedback + +### 2. Type Safety & Code Quality +- **TypeScript**: Use strict TypeScript for all new code +- **tRPC**: All API calls must go through tRPC for type safety +- **Validation**: Use Zod schemas for all input validation +- **Error Boundaries**: Implement proper error handling at all levels + +## Tech Stack Guidelines + +### Frontend (Next.js 16 + App Router) +- Use App Router patterns consistently +- Implement proper loading states and error boundaries +- Use React Server Components where appropriate + +### Backend (tRPC + Drizzle) +- All business logic goes through tRPC routers +- Use Drizzle ORM for all database operations +- Implement proper transactions for multi-table operations +- Follow existing router patterns in `src/server/api/routers/` + +### Database (PostgreSQL) +- Use Drizzle migrations in `drizzle/`; journal must stay in sync (`drizzle/meta/_journal.json`) +- `db:push` for local iteration; Docker runs `migrate.ts` on startup +- Follow existing schema patterns in `src/server/db/schema.ts` + +### Authentication (better-auth) +- Email/password via better-auth + custom `/api/auth/register` REST +- Optional Authentik OIDC (`AUTHENTIK_*`); Expo plugin for mobile +- Route protection via `src/proxy.ts` (session cookie check) and `protectedProcedure` +- `DISABLE_SIGNUPS=true` blocks registration; env booleans parsed in `src/env.js` (not `z.coerce.boolean`) + +### Development Tools +- Use ESLint and Prettier for code formatting +- Use TypeScript for type safety +- Exclusively use bun for development and production. Do not use Node.js or Deno. +- Stay away from starting development servers or running builds unless absolutely necessary. +- Run lints and typechecks when helpful. + +## Component Architecture + +### UI Components (shadcn/ui) +- Use shadcn/ui components as the foundation +- Follow existing component patterns +- Use `cn()` utility for conditional className merging +- Maintain consistent spacing (4px grid system) + +### Component Organization +- **Base UI Components**: `src/components/ui/` - Pure, portable shadcn/ui components +- **Project Components**: `src/components/` - Project-specific reusable components +- **Page Components**: `src/app/_components/` - Page-specific components + +### UI Component Rules + +#### What Belongs in `src/components/ui/` (Portable) +- **Pure shadcn/ui components**: button, input, select, dialog, etc. +- **Generic layout components**: page-layout, card, table +- **Basic form components**: input, textarea, checkbox, switch +- **Navigation components**: breadcrumb, navigation-menu +- **Feedback components**: badge, alert-dialog, toast +- **Data display**: table, skeleton, progress +- **Overlay components**: dialog, sheet, popover, dropdown-menu + +#### What Should Move to `src/components/` (Project-Specific) +- **Business logic components**: address-form, status-badge, data-table +- **Domain-specific forms**: client-form, invoice-form, business-form +- **Custom layouts**: page-header, dashboard-breadcrumbs +- **Feature components**: invoice-list, client-list, editable-invoice-items +- **Navigation**: Sidebar, Navbar, navigation +- **Branding**: logo, AddressAutocomplete + +#### Immediate Reorganization Needed +**Move from `src/components/ui/` to `src/components/forms/`:** +- `address-form.tsx` - Business-specific address handling +- `file-upload.tsx` - Project-specific file upload logic + +**Move from `src/components/ui/` to `src/components/data/`:** +- `data-table.tsx` - Enhanced with business logic +- `status-badge.tsx` - Invoice status specific +- `stats-card.tsx` - Dashboard-specific statistics + +**Move from `src/components/ui/` to `src/components/layout/`:** +- `page-layout.tsx` - Project-specific layout patterns +- `quick-action-card.tsx` - Dashboard-specific actions +- `floating-action-bar.tsx` - Project-specific floating actions + +**Keep in `src/components/ui/` (Portable):** +- `button.tsx`, `input.tsx`, `select.tsx` - Pure shadcn/ui +- `dialog.tsx`, `sheet.tsx`, `popover.tsx` - Generic overlays +- `table.tsx`, `card.tsx`, `badge.tsx` - Base components +- `calendar.tsx`, `date-picker.tsx` - Generic date components +- `dropdown-menu.tsx`, `navigation-menu.tsx` - Generic navigation +- `skeleton.tsx`, `progress.tsx` - Generic loading states + +#### Component Design Principles +- **High Reusability**: Components should accept props for customization +- **Composition over Inheritance**: Use children props and render props +- **Default Values**: Provide sensible defaults for all optional props +- **Type Safety**: Use TypeScript interfaces for all props +- **Accessibility**: Include proper ARIA labels and keyboard navigation +- **Responsive Design**: Mobile-first approach with responsive variants + +#### Component Props Pattern +```typescript +interface ComponentProps { + // Required props + title: string; + + // Optional props with defaults + variant?: "default" | "success" | "warning" | "error"; + size?: "sm" | "md" | "lg"; + + // Styling props + className?: string; + + // Event handlers + onClick?: () => void; + onChange?: (value: string) => void; + + // Content + children?: React.ReactNode; + + // Accessibility + "aria-label"?: string; +} +``` + +#### Component Reusability Guidelines +- **Configurable Content**: Use props for text, labels, and content +- **Flexible Styling**: Accept className and style props for customization +- **Variant System**: Use variant props for different visual states +- **Size Variants**: Provide consistent size options (sm, md, lg, xl) +- **Icon Support**: Accept icon props for visual customization +- **Loading States**: Include loading/skeleton variants +- **Error States**: Handle error states gracefully +- **Empty States**: Provide empty state components + +#### Component Customization Examples +```typescript +// Good: Highly customizable component +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + title?: string; + description?: string; + actions?: React.ReactNode; + searchKey?: string; + searchPlaceholder?: string; + showPagination?: boolean; + pageSize?: number; + className?: string; + emptyState?: React.ReactNode; + loading?: boolean; +} + +// Good: Flexible form component +interface FormFieldProps { + label: string; + name: string; + type?: "text" | "email" | "password" | "number"; + placeholder?: string; + required?: boolean; + error?: string; + className?: string; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; + disabled?: boolean; + onChange?: (value: string) => void; +} +``` + +#### Component File Structure +``` +src/components/ +├── ui/ # Portable base components +│ ├── button.tsx # Pure shadcn/ui components +│ ├── input.tsx +│ └── ... +├── forms/ # Project-specific forms +│ ├── address-form.tsx +│ ├── client-form.tsx +│ └── invoice-form.tsx +├── layout/ # Layout components +│ ├── page-header.tsx +│ ├── sidebar.tsx +│ └── navbar.tsx +├── data/ # Data display components +│ ├── data-table.tsx +│ ├── status-badge.tsx +│ └── invoice-list.tsx +├── navigation/ # Navigation components +│ ├── breadcrumbs.tsx +│ └── navigation.tsx +└── branding/ # Brand-specific components + ├── logo.tsx + └── address-autocomplete.tsx +``` + +### Styling Guidelines +- **Primary Color**: Green (#16a34a) for branding +- **Font**: Geist font family for professional typography +- **Tailwind CSS**: Use utility classes consistently +- **Responsive Design**: Mobile-first approach + +## Business Logic Patterns + +### Invoice Management +- **Invoice Creation**: Multi-step process with validation +- **Line Items**: Flexible pricing with custom rates +- **Status Tracking**: draft → sent → paid/overdue +- **PDF Generation**: Professional invoice formatting + +### Client Management +- **Contact Information**: Complete address and contact details +- **Search & Filter**: Efficient client lookup +- **Data Validation**: Proper email and phone validation + +### Business Profile +- **Default Business**: One default business per user +- **Logo Support**: Professional branding +- **Tax Information**: Business tax details + +## API Development Rules + +### tRPC Router Patterns +```typescript +// Follow this pattern for all routers +export const exampleRouter = createTRPCRouter({ + create: protectedProcedure + .input(z.object({ /* validation schema */ })) + .mutation(async ({ ctx, input }) => { + // Business logic here + }), + + list: protectedProcedure + .input(z.object({ /* pagination/filtering */ })) + .query(async ({ ctx, input }) => { + // Query logic here + }), +}); +``` + +### Error Handling +- Use toast notifications for user feedback +- Implement proper form validation +- Handle API errors gracefully +- Provide clear error messages + +### Security +- Always validate user input with Zod +- Check user permissions for all operations +- Sanitize data before database operations +- Use proper authentication checks + +## Database Schema Rules + +### Table Structure +- **UUID Primary Keys**: Use `crypto.randomUUID()` for all IDs +- **Timestamps**: Include `createdAt` and `updatedAt` fields +- **User Relations**: All business data linked to users +- **Indexes**: Proper indexing for performance + +### Migrations +When adding a new migration: +1. Create the SQL file in `drizzle/` following the numbering sequence (e.g. `0011_my_change.sql`) +2. **Always update `drizzle/meta/_journal.json`** to include the new entry — Drizzle's migrate runner uses this file to determine which migrations to apply. If the entry is missing, the migration will be silently skipped on deploy. + +The journal entry format: +```json +{ + "idx": 10, + "version": "7", + "when": 1780704000000, + "tag": "0010_my_change", + "breakpoints": true +} +``` +Use a Unix timestamp in milliseconds for `when`, incrementing `idx` by 1 from the previous entry. + +Migrations run automatically at container startup via `bun migrate.ts` (see Dockerfile `CMD`). Do not run them manually. + +### Relationships +- **Users → Clients**: One-to-many +- **Users → Businesses**: One-to-many +- **Users → Invoices**: One-to-many +- **Clients → Invoices**: One-to-many +- **Businesses → Invoices**: One-to-many +- **Invoices → Invoice Items**: One-to-many + +## File Naming Conventions + +### Components & Pages +- **Components**: PascalCase (e.g., `ClientList.tsx`) +- **Pages**: kebab-case (e.g., `new-client.tsx`) +- **Layouts**: `layout.tsx` (Next.js convention) +- **API Routes**: `route.ts` (Next.js convention) + +### Utilities & Helpers +- **Utilities**: camelCase (e.g., `formatCurrency.ts`) +- **Constants**: UPPER_SNAKE_CASE +- **Types**: PascalCase (e.g., `InvoiceStatus`) + +## Development Workflow + +### Adding New Features +1. **Database Schema**: Update schema with migrations +2. **tRPC Router**: Add procedures with validation +3. **UI Components**: Create components using shadcn/ui +4. **Pages**: Implement pages with proper routing +5. **Testing**: Verify functionality and error handling + +### Code Quality +- **ESLint**: Follow existing linting rules +- **Prettier**: Consistent code formatting +- **TypeScript**: Strict type checking +- **Performance**: Optimize database queries and React components + +## Business Logic Specifics + +### Invoice Calculations +- **Subtotal**: Sum of all line items +- **Tax**: Apply tax rate to subtotal +- **Total**: Subtotal + tax +- **Currency**: Handle decimal precision properly + +### Status Management +- **Draft**: Initial state, editable +- **Sent**: Invoice sent to client +- **Paid**: Payment received +- **Overdue**: Past due date + +### Data Validation +- **Email**: Proper email format validation +- **Phone**: International phone number support +- **Address**: Complete address validation +- **Currency**: Proper decimal handling + +## Performance Guidelines + +### Database Optimization +- Use proper indexes for frequently queried fields +- Implement pagination for large datasets +- Use transactions for data consistency +- Optimize query patterns + +### Frontend Performance +- Use React.memo for expensive components +- Implement proper loading states +- Optimize bundle size +- Use Next.js Image component for images + +### Caching Strategy +- Use React Query for client-side caching +- Implement proper cache invalidation +- Use Next.js caching where appropriate + +## Security Considerations + +### Authentication & Authorization +- All routes require proper authentication +- Check user ownership for all operations +- Implement proper session management +- Use secure password hashing + +### Data Protection +- Validate all user inputs +- Sanitize data before database operations +- Implement proper error handling +- Use HTTPS in production + +### Business Data Security +- User data isolation +- Proper access controls +- Audit trails for sensitive operations +- Secure API endpoints + +## Testing & Quality Assurance + +### Manual Testing Checklist +- [ ] Authentication flows work correctly +- [ ] Form validation provides clear feedback +- [ ] Responsive design on all screen sizes +- [ ] Database operations handle errors gracefully +- [ ] PDF generation works correctly +- [ ] Navigation and routing function properly + +### Code Review Guidelines +- Check for proper error handling +- Verify type safety +- Ensure consistent styling +- Review security implications +- Test business logic accuracy + +## Deployment & Production + +### Environment Configuration +- Use proper environment variables (see `.env.example` and `src/env.js`) +- `BETTER_AUTH_URL` / `NEXT_PUBLIC_APP_URL` must match the public hostname +- Secure database connections; `DB_DISABLE_SSL=true` for compose Postgres + +### Database Management +- Use migrations for schema changes +- Backup data regularly +- Monitor database performance +- Handle database errors gracefully + +## Common Patterns & Anti-Patterns + +### ✅ Do's +- Use tRPC for all API calls +- Implement proper loading states +- Use toast notifications for feedback +- Follow existing component patterns +- Validate all user inputs +- Use proper TypeScript types + +### ❌ Don'ts +- Don't use direct fetch calls +- Don't skip input validation +- Don't ignore error handling +- Don't hardcode business logic +- Don't use any types unnecessarily +- Don't skip proper authentication checks + +## Emergency Procedures + +### Critical Issues +- **Data Loss**: Immediate database backup +- **Security Breach**: Rotate all secrets +- **Performance Issues**: Database query optimization +- **User Complaints**: Prioritize user experience fixes + +### Rollback Strategy +- Keep database migrations reversible +- Maintain version control for all changes +- Test rollback procedures regularly +- Document emergency procedures + +## Remember +This is a business application where reliability, security, and professional user experience are critical. Every decision should prioritize these values over development convenience or flashy features. + +- Don't create demo pages unless absolutely necessary. +- Don't create unnecessary complexity. +- Don't run builds unless absolutely necessary, if you do, kill the dev servers. +- Don't start new dev servers unless asked. +- Don't start drizzle studio- you cannot do anything with it. diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..b2a98e8 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1 +FROM oven/bun:1 AS base +WORKDIR /usr/src/app + +FROM base AS install +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile + +# Next.js build must run on Node — Bun 1.3.x can SIGSEGV on Linux arm64 during +# the "Collecting page data" worker phase (oven-sh/bun#...). Runtime stays on Bun. +FROM node:22-bookworm-slim AS build +WORKDIR /usr/src/app +COPY --from=install /usr/src/app/node_modules node_modules +COPY . . + +ARG NEXT_PUBLIC_APP_URL=http://localhost:3000 +ARG BETTER_AUTH_URL=http://localhost:3000 + +# Low-memory Docker build profile: +# - skip tsc inside `next build` (run `bun run check` in CI instead) +ENV DOCKER_BUILD=1 \ + NODE_ENV=production \ + SKIP_ENV_VALIDATION=1 \ + NEXT_TELEMETRY_DISABLED=1 \ + BETTER_AUTH_URL=${BETTER_AUTH_URL} \ + NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL} \ + AUTH_SECRET=docker-build-placeholder-secret-do-not-use \ + DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres +RUN node ./node_modules/next/dist/bin/next build + +FROM base AS release +ENV NODE_ENV=production \ + PORT=3000 \ + HOSTNAME=0.0.0.0 \ + NEXT_TELEMETRY_DISABLED=1 + +COPY --from=build /usr/src/app/.next ./.next +COPY --from=build /usr/src/app/public ./public +COPY --from=install /usr/src/app/node_modules node_modules +COPY --from=build /usr/src/app/package.json ./package.json +COPY --from=build /usr/src/app/drizzle.config.ts ./drizzle.config.ts +COPY --from=build /usr/src/app/drizzle ./drizzle +COPY --from=build /usr/src/app/src/server/db/migrate.ts ./migrate.ts + +RUN chmod -R a+rX drizzle public migrate.ts + +USER bun +EXPOSE 3000 +CMD ["sh", "-c", "bun migrate.ts && bun run start"] diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..89a4ecf --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,280 @@ +![beenvoice Logo](public/beenvoice-logo.png) + +# beenvoice-web + +Web application and API for **beenvoice** — invoicing for freelancers and small businesses. Includes the Next.js dashboard, tRPC API, better-auth, PostgreSQL persistence, PDF/email delivery, time tracking, and an MCP automation endpoint. + +**Repository:** [git.soconnor.dev/soconnor/beenvoice-web](https://git.soconnor.dev/soconnor/beenvoice-web) +**Architecture:** [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) +**Mobile companion:** [beenvoice-app](https://git.soconnor.dev/soconnor/beenvoice-app) (separate repo; often checked out beside this one in a workspace) + +## Stack + +| Layer | Technology | +|-------|------------| +| App | Next.js 16 App Router, React 19 | +| API | tRPC 11 + SuperJSON | +| Database | PostgreSQL 17, Drizzle ORM | +| Auth | better-auth (email/password, optional Authentik OIDC, Expo mobile) | +| UI | shadcn/ui, Tailwind CSS v4 | +| Email / PDF | Resend, `@react-pdf/renderer` | +| Runtime | Bun | + +## Features + +- Clients, businesses, invoices (line items, tax, status workflow) +- Time clock with one running timer per user; clock-out can append invoice lines +- Expenses, payments, recurring invoices, invoice templates +- PDF export and email delivery (Resend) +- Public invoice links (`/i/[token]`) +- CSV import, reports, platform branding / admin settings +- MCP API (`/api/mcp`) for automation via API keys (`bv_…`) +- Optional Authentik OIDC SSO + +## Prerequisites + +- [Bun](https://bun.sh) 1.x +- Docker & Docker Compose (for PostgreSQL locally or full-stack deploy) +- Git + +## Local development + +### 1. Clone and install + +```bash +git clone https://git.soconnor.dev/soconnor/beenvoice-web.git +cd beenvoice-web +bun install +``` + +### 2. Environment + +```bash +cp .env.example .env.local +``` + +Edit `.env.local` for local dev. Minimum: + +```env +DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres +DB_DISABLE_SSL=true +AUTH_SECRET=your-dev-secret # openssl rand -base64 32 +BETTER_AUTH_URL=http://localhost:3000 +NEXT_PUBLIC_APP_URL=http://localhost:3000 +``` + +Email and SSO are optional for local work — leave `RESEND_*` and `AUTHENTIK_*` blank unless you need them. + +### 3. Database + +Start Postgres (dev compose exposes port 5432): + +```bash +docker compose -f docker-compose.dev.yml up -d +``` + +After a fresh volume (`docker compose down -v`), Postgres starts empty — you must apply schema before registering or signing in. + +Apply schema (pick one): + +```bash +bun run db:push # fast iteration during development +# bun run db:migrate # same migrations the Docker image runs in production +``` + +**Demo account.** For App Store review and local testing, `bun run db:migrate` creates a pre-populated `demo@example.com` account (`db:push` does not) with the public password `demo123`. + +To rotate the credential temporarily, provision a private password: + +```bash +DEMO_ACCOUNT_PASSWORD='' bun run demo:provision +``` + +Provisioning rotates the credential and invalidates prior sessions. Do not commit or publish a private replacement password. The account includes a sample business, clients, and invoices (draft, sent, and paid). + +### 4. Run + +```bash +bun run dev +``` + +Open [http://localhost:3000](http://localhost:3000), register at `/auth/register`, or sign in with the demo account above. + +## Docker deployment (app + database) + +The production compose file runs the Next.js app and PostgreSQL. + +**Container startup** runs `bun migrate.ts && bun run start` (see `Dockerfile`). Drizzle only applies **pending** migrations — safe to run on every restart; already-applied migrations are skipped. + +The Docker **build** runs `next build` on **Node 22** (Bun can crash on Linux arm64 during the page-data worker phase). The **runtime** image still uses Bun for migrations and `next start`. `docker-compose.yml` does not set container memory or CPU limits — containers can use whatever the Docker host provides. + +### 1. Configure + +```bash +cp .env.example .env +``` + +Set at least: + +```env +AUTH_SECRET= +BETTER_AUTH_URL=https://your-public-hostname +NEXT_PUBLIC_APP_URL=https://your-public-hostname +``` + +`BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` must match the URL users actually use in the browser (scheme + host + port). If they point at `localhost` but you access the app via another hostname, sign-up and sign-in will fail (often with a vague **"REQUIRED"** toast). + +`NEXT_PUBLIC_*` values are embedded at **image build** time. Rebuild after changing `NEXT_PUBLIC_APP_URL`, white-label defaults, or `NEXT_PUBLIC_AUTHENTIK_ENABLED`: + +```bash +docker compose build --no-cache app +``` + +`BETTER_AUTH_URL` and `AUTH_SECRET` are read at **container runtime** from `.env` — you can change them without rebuilding, then restart the app container. + +### 2. First start (or after code changes) + +```bash +./scripts/docker-deploy.sh +# or: bun run docker:deploy +# or: docker compose up -d --build +``` + +`--build` is required after code changes. A plain `docker compose up -d` reuses the existing `beenvoice:local` image and **does not** pick up new code from `git pull`. The deploy script tags the image with the current git SHA (`beenvoice:`) so each deploy gets a distinct image. + +App listens on `${WEB_PORT:-${PORT:-3000}}` on the host (container port is always 3000). Postgres stays on the internal compose network. + +### Scheduled recurring invoices + +The app container does not run a cron daemon. It starts the web server with +`bun migrate.ts && bun run start`, and recurring invoice generation only happens +when something calls `POST /api/cron/generate-recurring` with +`Authorization: Bearer $CRON_SECRET`. + +- **Coolify deploys:** use a Coolify scheduled task to call the endpoint. +- **Full Docker deploys:** use host cron, a small scheduler sidecar, or an + external scheduler to call + `http://localhost:${WEB_PORT:-${PORT:-3000}}/api/cron/generate-recurring`. + +### 3. Updating an existing deploy + +```bash +git pull +./scripts/docker-deploy.sh # recommended: rebuild + tag with git SHA + restart +# or: docker compose up -d --build +``` + +| Command | New code? | Migrations run? | +|---------|-----------|-----------------| +| `git pull` only | No | No | +| `docker compose up -d` (no `--build`) | No — reuses `beenvoice:local` | Only if the app container restarts (same image) | +| `./scripts/docker-deploy.sh` or `docker compose up -d --build` | Yes | Yes — on app container start | +| `docker compose restart app` | No | Yes — migrate runs again (no-op if up to date) | + +Prune old app images occasionally: `docker image prune -f` (or remove specific `beenvoice:*` tags). + +To verify migration files match the journal before deploy: `bun run db:verify-journal`. + +### Coolify + +For self-hosted [Coolify](https://coolify.io) deploys (especially `ENOTFOUND garage` with Application + separate Garage compose), see **[docs/COOLIFY.md](./docs/COOLIFY.md)**. Recommended: deploy [`docker-compose.coolify.yml`](./docker-compose.coolify.yml) as a single Compose resource. + +### 4. Sign-ups + +Registration is **enabled** by default. To block new email/password accounts: + +```env +DISABLE_SIGNUPS=true +``` + +Use the literal strings `true` or `false` (or omit the variable). Do not rely on bare boolean coercion from shell/compose — the app parses these explicitly. + +### 5. Optional services + +| Variable | Purpose | +|----------|---------| +| `RESEND_API_KEY`, `RESEND_DOMAIN` | Invoice and password-reset email | +| `AUTHENTIK_ISSUER`, `AUTHENTIK_CLIENT_ID`, `AUTHENTIK_CLIENT_SECRET` | OIDC SSO (also set `NEXT_PUBLIC_AUTHENTIK_ENABLED=true` and rebuild) | +| `CRON_SECRET` | Protects `/api/cron/generate-recurring` | +| `DISABLE_SIGNUPS=true` | Block new registrations | + +## Project structure + +``` +beenvoice-web/ +├── src/app/ # Routes (dashboard, auth, /api/*) +├── src/server/api/ # tRPC routers +├── src/server/db/ # Drizzle schema, pool, migrate.ts +├── src/components/ # UI (ui/, forms/, layout/, branding/) +├── src/lib/ # auth, PDF, email, branding helpers +├── drizzle/ # SQL migrations +├── Dockerfile # Production image (migrate + next start) +├── docker-compose.yml # App + Postgres + Garage (deploy) +├── docker-compose.coolify.yml # Coolify Compose (app + db + garage) +├── docker-compose.coolify-garage.yml # Garage-only for Coolify Application pairing +├── docker-compose.dev.yml # Postgres only (local dev) +└── docs/ # Architecture and UI guides +``` + +See [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) for routers, schema, auth flows, and MCP. + +## Scripts + +```bash +# App +bun run dev # next dev --turbo +bun run build # production build +bun run start # next start +bun run check # eslint + tsc + +# Database +bun run db:push # push schema (local dev) +bun run db:migrate # run drizzle migrations +bun run db:generate # generate new migration SQL +bun run db:studio # Drizzle Studio + +# Formatting +bun run lint +bun run lint:fix +bun run format:write +bun run typecheck + +# Docker helpers +bun run docker:up # dev Postgres only (Colima + docker-compose.dev.yml) +bun run docker:down # stop dev Postgres + colima +bun run docker:deploy # production: rebuild app image + docker-compose.yml up -d +``` + +Full-stack deploy uses `bun run docker:deploy` or `./scripts/docker-deploy.sh` (see [Docker deployment](#docker-deployment-app--database)), not `bun run docker:up`. + +## API surface + +| Endpoint | Auth | Purpose | +|----------|------|---------| +| `/api/trpc` | Session cookie or API key | Primary API (web + mobile) | +| `/api/auth/*` | Varies | better-auth + custom register/reset REST | +| `/api/mcp` | API key only | JSON-RPC automation tools | +| `/i/[token]` | Public token | Client invoice view | +| `/api/i/[token]/pdf` | Public token | Invoice PDF download | + +Business logic lives in `src/server/api/routers/` with Zod validation. + +## Customization + +- **Runtime branding:** Dashboard → Administration (platform settings) +- **Build-time defaults:** `NEXT_PUBLIC_BRAND_*` in `.env` (rebuild Docker image to apply) +- **Theme / fonts:** `src/styles/globals.css`, appearance settings in the app +- **Logo component:** `src/components/branding/logo.tsx` + +## Documentation + +| Doc | Contents | +|-----|----------| +| [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) | Stack, routers, schema, auth, Docker, MCP | +| [docs/COOLIFY.md](./docs/COOLIFY.md) | Coolify deploy paths and Garage networking | +| [docs/README.md](./docs/README.md) | Index of UI and product guides | +| [AGENTS.md](./AGENTS.md) | Conventions for AI-assisted development | + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/apps/web/bun.lock b/apps/web/bun.lock new file mode 100644 index 0000000..2a5f5bb --- /dev/null +++ b/apps/web/bun.lock @@ -0,0 +1,1938 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "beenvoice", + "dependencies": { + "@aws-sdk/client-s3": "^3.1075.0", + "@better-auth/expo": "^1.6.19", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@fontsource-variable/playfair-display": "^5.2.8", + "@radix-ui/react-alert-dialog": "^1.1.16", + "@radix-ui/react-avatar": "^1.1.12", + "@radix-ui/react-checkbox": "^1.3.4", + "@radix-ui/react-collapsible": "^1.1.13", + "@radix-ui/react-dialog": "^1.1.16", + "@radix-ui/react-dropdown-menu": "^2.1.17", + "@radix-ui/react-label": "^2.1.9", + "@radix-ui/react-navigation-menu": "^1.2.15", + "@radix-ui/react-popover": "^1.1.16", + "@radix-ui/react-progress": "^1.1.9", + "@radix-ui/react-select": "^2.3.0", + "@radix-ui/react-separator": "^1.1.9", + "@radix-ui/react-slot": "^1.2.5", + "@radix-ui/react-switch": "^1.3.0", + "@radix-ui/react-tabs": "^1.1.14", + "@radix-ui/react-tooltip": "^1.2.9", + "@react-pdf/renderer": "^4.5.1", + "@t3-oss/env-nextjs": "^0.12.0", + "@tanstack/react-query": "^5.101.0", + "@tanstack/react-table": "^8.21.3", + "@tiptap/extension-color": "^3.13.0", + "@tiptap/extension-list-item": "^3.13.0", + "@tiptap/extension-text-align": "^3.13.0", + "@tiptap/extension-text-style": "^3.13.0", + "@tiptap/react": "^3.13.0", + "@tiptap/starter-kit": "^3.13.0", + "@trpc/client": "^11.17.0", + "@trpc/react-query": "^11.17.0", + "@trpc/server": "^11.17.0", + "bcryptjs": "^3.0.3", + "better-auth": "^1.6.16", + "chrono-node": "^2.9.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.4.0", + "dotenv": "^17.4.2", + "drizzle-orm": "^0.45.2", + "file-saver": "^2.0.5", + "framer-motion": "^12.40.0", + "fuse.js": "^7.4.2", + "lucide-react": "^0.525.0", + "next": "^16.2.12", + "pg": "8.21.0", + "react": "^19.2.8", + "react-colorful": "^5.7.0", + "react-day-picker": "^9.12.0", + "react-dom": "^19.2.8", + "react-dropzone": "^14.3.8", + "recharts": "^3.8.1", + "resend": "^4.8.0", + "server-only": "^0.0.1", + "sharp": "^0.35.3", + "sonner": "^2.0.7", + "superjson": "^2.2.6", + "tailwind-merge": "^3.6.0", + "trpc": "^0.11.3", + "zod": "^3.25.76", + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.3.0", + "@types/bcryptjs": "^2.4.6", + "@types/file-saver": "^2.0.7", + "@types/node": "^20.19.26", + "@types/pg": "^8.20.0", + "@types/raf": "^3.4.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "babel-plugin-react-compiler": "^1.0.0", + "baseline-browser-mapping": "^2.10.34", + "drizzle-kit": "^0.31.10", + "eslint": "^9.39.1", + "eslint-config-next": "^16.2.7", + "eslint-plugin-drizzle": "^0.2.3", + "postcss": "^8.5.15", + "prettier": "3.8.3", + "prettier-plugin-tailwindcss": "^0.6.14", + "tailwindcss": "^4.3.0", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.60.1", + }, + }, + }, + "trustedDependencies": [ + "@tailwindcss/oxide", + "sharp", + "esbuild", + "unrs-resolver", + ], + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], + + "@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/checksums": ["@aws-sdk/checksums@3.1000.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.974.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-v0U9S7gBIme3OTgt1LdbAF4RpvavCc+4GK1+1xqAcqtbrHsEhjQo6R45LKcjhs/+WrRJij1Y0Gztw7QPAIeUfA=="], + + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1075.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.23", "@aws-sdk/credential-provider-node": "^3.972.58", "@aws-sdk/middleware-flexible-checksums": "^3.974.33", "@aws-sdk/middleware-sdk-s3": "^3.972.54", "@aws-sdk/signature-v4-multi-region": "^3.996.35", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-h1A6nIl1YX6Y45enGsTK7ef3ZrOnBiQJ1qF5R2K/nMWfsu6A9mc2Y5T66nxerABzyjjyyvign3MrzafnFoQKmA=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.974.23", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@aws-sdk/xml-builder": "^3.972.31", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.49", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.51", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.56", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/credential-provider-env": "^3.972.49", "@aws-sdk/credential-provider-http": "^3.972.51", "@aws-sdk/credential-provider-login": "^3.972.55", "@aws-sdk/credential-provider-process": "^3.972.49", "@aws-sdk/credential-provider-sso": "^3.972.55", "@aws-sdk/credential-provider-web-identity": "^3.972.55", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.55", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.58", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.49", "@aws-sdk/credential-provider-http": "^3.972.51", "@aws-sdk/credential-provider-ini": "^3.972.56", "@aws-sdk/credential-provider-process": "^3.972.49", "@aws-sdk/credential-provider-sso": "^3.972.55", "@aws-sdk/credential-provider-web-identity": "^3.972.55", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.49", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.55", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/token-providers": "3.1074.0", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.55", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg=="], + + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.33", "", { "dependencies": { "@aws-sdk/checksums": "^3.1000.8", "tslib": "^2.6.2" } }, "sha512-qMgQSPemQq2/eW/e/0+SpY4kYR5L7dUgBiVdEc5bd+ztHNv07ZMYiI+sTiir3TgKndFfglSw/VFi7oZJ6bZ63g=="], + + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.54", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/signature-v4-multi-region": "^3.996.35", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-GDfDQ0gwLFRKN9gWIKcmVrHJ3e7XagnY7N1LLzMVNgnOnuY7f/ALgmy3CuBjosWD95T/Z6e+gs1IeWmLPkyLKQ=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.23", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.23", "@aws-sdk/signature-v4-multi-region": "^3.996.35", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.35", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1074.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.973.13", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.8", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.31", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@better-auth/core": ["@better-auth/core@1.6.16", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.2.2", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.6", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-a0+ZNaaYYxOdFXFXmOE36TgtYN8QDzSYDozaAH0zsiWB0oyljsENyCxHJSekysISftb0rFpVXNdw525aEAOa6w=="], + + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.16", "", { "peerDependencies": { "@better-auth/core": "^1.6.16", "@better-auth/utils": "0.4.1", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-AZjswadpR7zlQduj3fRSsu1R5ldQRR9AeFqoxXRI4colrQhevOVY+tJr8RTJv9Nh18e9FMYDXUju2GX+QWHDzg=="], + + "@better-auth/expo": ["@better-auth/expo@1.6.19", "", { "dependencies": { "@better-fetch/fetch": "1.3.1", "better-call": "1.3.6", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.19", "better-auth": "^1.6.19", "expo-constants": ">=17.0.0", "expo-linking": ">=7.0.0", "expo-network": ">=8.0.7", "expo-web-browser": ">=14.0.0" }, "optionalPeers": ["expo-constants", "expo-linking", "expo-network", "expo-web-browser"] }, "sha512-+v8wYQPu9MhIlEQxBzBBpG4LUjs5MMrup4EWn9N+yTstbaHhOoLwu2DUfHn+IZypcV2rlTCFE+JCffxlnihM7w=="], + + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.16", "", { "peerDependencies": { "@better-auth/core": "^1.6.16", "@better-auth/utils": "0.4.1", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-ys/feL1p6By3/rQlMZ8QTgf9K2tZAIp1p+fGqT2krIoG5r+UsH3gMkUdbHlYxLt790Bo+Njkiqt59P0BMNsi+g=="], + + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.16", "", { "peerDependencies": { "@better-auth/core": "^1.6.16", "@better-auth/utils": "0.4.1" } }, "sha512-8mDqe+2PMF9hUxjGNP1NOcqU1AqjUgmE8YC1HTtxa+LjnO7zsAPSxGSyo1L+7buFNLtiNyGFxccHpwOkO4/Msw=="], + + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.16", "", { "peerDependencies": { "@better-auth/core": "^1.6.16", "@better-auth/utils": "0.4.1", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-JbUg/v3m9WUX94ivVdUOF8t/w2mWNBWvqYMqyWybfHQEPR8cvcqsqpfYvwg9HLBrYwhKXBS3KcJ1Rtk6gZ19Yw=="], + + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.16", "", { "peerDependencies": { "@better-auth/core": "^1.6.16", "@better-auth/utils": "0.4.1", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-2bIlA7wjBx+4N2QcM32xL/YojRuJpDvskXqT/dGYKToDIEl/7yr12cLYlqeaFLL0O0s5qNZ8jbDtlCz20eogeQ=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.16", "", { "peerDependencies": { "@better-auth/core": "^1.6.16", "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.2.2" } }, "sha512-A782UQvlqZBddw0j2Q6tdroHulIpMlqQh/pbw2up30drLi66jz1ttgShRmryfOLAqN4DHqteuRrSsqDDrsp/pA=="], + + "@better-auth/utils": ["@better-auth/utils@0.4.1", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-SZBPRPF3z0nBvE5ygOkxae35wnnXPRShmqFo78S+qslLeFoPu/pMgnXAuNKFMMybac3tiLaVg1e3MQW5MC+1iA=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], + + "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], + + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], + + "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], + + "@dnd-kit/modifiers": ["@dnd-kit/modifiers@9.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw=="], + + "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], + + "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + + "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + + "@fontsource-variable/playfair-display": ["@fontsource-variable/playfair-display@5.2.8", "", {}, "sha512-ZzVIXPOrL85yyOvZYoBzUszIJM+xKkHqni4IYn2CVLaGQQdJR8sBeC8yFNgjxSJ7ludTwta8qpULeOFuk5X75A=="], + + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], + + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@next/env": ["@next/env@16.2.12", "", {}, "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg=="], + + "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.7", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-VbS+QgMHqvIDMTIqD2xMBKK1otIpdAUKA8VLHFwR9h6OfU/mOm7w/69nQcvdmI8hCk99Wr2AsGLn/PJ/tMHw1w=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.12", "", { "os": "win32", "cpu": "x64" }, "sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw=="], + + "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.4", "", {}, "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ=="], + + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dialog": "1.1.16", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPaIgo0mxYlvcFaM9jB2Uot9TjGXMuAPEvrc6BOLeV+I5U8s1dkIoouYaa6lmSfc5SPMo5x5djOTOTvaigdGMQ=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig=="], + + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.12", "", { "dependencies": { "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-is-hydrated": "0.1.1", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NQCQyWC7QrDPhjMn8hUqFeU0lUrprIgm1AyMgLbzuQJibNnatdc3SSMo3/UGFu/eUkJUU1cEcKCnyhXTQzq6tA=="], + + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m3JmIOAX5ZzZ6VPjxEU2dbTOhoHi0nT5riwcDwe8idocsWf4a5DXJLDtZ6LfJwMBx7W+A2b7kp2TgPEKtaiF6A=="], + + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F0s8+p2XNpfc3k02zBfB0jPWbkHVG162+p7BdUMyJ2308QMqZ+oaclX+FAzKFovgL5OqRU+Rvy6f/vbdlJVaqA=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.9", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l9ok83YBclEZhbjgzt76Hw733e6cvRKPNgO6GJ/IETlufXG9p+fRu2wlvpImQvR6xdJ8h7J8J2DBvsPEiEsKMw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-escape-keydown": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-menu": "2.1.17", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-S6b3Jm57sY5EdDyOMLkacbB0qMnKhy1RCKZCt795ZkmtUOAvojYIZ5p7dXHIh5Cyr3jCLLI5/g64V3FKLudZmw=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.9", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-rDoTeMbCwRVcnmo7NGT9IlPo1yXmEI+xc1URP3oeewwZEV4mdTp1dYUhYbQdo4D1q2SjKVvv4N1gNY77QAQtjA=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.0", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-roving-focus": "1.1.12", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-callback-ref": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-fmbNnFyf+JYCN0DhhWnEdUTDnZD1mXaPQWivdsPIb8oOSbARfD3LIQJbLCG8a8QLCwoMxiJ7GVPIFcC8Dw8v2Q=="], + + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/fS8hKCcRt4DwCGa5QIB3juRXmfYSOk4a2AEe/BDIyy7Hm+eje2Y13oUx5zejl+wFt1owrM7E8NWlbaEl5EGpg=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.0", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.0", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-rect": "1.1.2", "@radix-ui/react-use-size": "1.1.2", "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9PB589e1aWZbrlFUHdz6WiPCL+xLZHQFX7oibqG/6Q0SwOkxDyQX9W/cyPa+sAPPKuC8cpLCpRczE5a/1DiwVQ=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.6", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.5", "", { "dependencies": { "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA=="], + + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.9", "", { "dependencies": { "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+EOkvg1Zn1vI1+fRDfRSAiJ7BWfcDAo5ASMmbqrcLZ4s4USk2FGkoHgeb2X+CkUgo2zJMiyObwf1k44CrRWsyw=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.3.0", "", { "dependencies": { "@radix-ui/number": "1.1.2", "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.9", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.0", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-layout-effect": "1.1.2", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-visually-hidden": "1.2.5", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mENc7WpJvJcW8hlMpzfFcHcEhTvYS5JMBmi9HVC1Q00uhBwML086MHYUV8QQdQv6lcu0Wg8dzd1RB8AFADcG/g=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.3.0", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-use-previous": "1.1.2", "@radix-ui/react-use-size": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-GP1EZwhoZO/GGnhM1P5/2Vpm8iN8EnngyU0oezn2l78kN8tj25pyrvjIaT7azBhK615KSt+P2w39y57YV5jVkA=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-roving-focus": "1.1.12", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.9", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-popper": "1.3.0", "@radix-ui/react-portal": "1.1.11", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5", "@radix-ui/react-use-controllable-state": "1.2.3", "@radix-ui/react-visually-hidden": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-u6F9MmTtBSLkiXNVDrtB/yPCZarM9smNswC24YYLV/M+bth6J3Gs3vlJezEoFwKZvPvxhCpUYdUnOsNG/0XOlA=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.2", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.2", "", { "dependencies": { "@radix-ui/rect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.5", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="], + + "@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="], + + "@react-pdf/fns": ["@react-pdf/fns@3.1.3", "", {}, "sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w=="], + + "@react-pdf/font": ["@react-pdf/font@4.0.8", "", { "dependencies": { "@react-pdf/pdfkit": "^5.1.1", "@react-pdf/types": "^2.11.1", "fontkit": "^2.0.2", "is-url": "^1.2.4" } }, "sha512-deNd+emtZAJho1IlzKL9bRoLAGv/6oXOIKO2oZfs4RuXUrK1onLHbJO7e2YoVLPFP/sQxisRTnzdJFtd35iKwA=="], + + "@react-pdf/image": ["@react-pdf/image@3.1.0", "", { "dependencies": { "@react-pdf/svg": "^1.1.0", "jay-peg": "^1.1.1", "png-js": "^2.0.0" } }, "sha512-ks7Ry8v711r8NvKWSELehj0BXBNPRihSnWsM09nDD8Ur175zbWBCK217LLwQMKDNYDVpkZaipdoJPom1LGaE9g=="], + + "@react-pdf/layout": ["@react-pdf/layout@4.6.1", "", { "dependencies": { "@react-pdf/fns": "3.1.3", "@react-pdf/image": "^3.1.0", "@react-pdf/primitives": "^4.3.0", "@react-pdf/stylesheet": "^6.2.1", "@react-pdf/textkit": "^6.3.0", "@react-pdf/types": "^2.11.1", "emoji-regex-xs": "^1.0.0", "queue": "^6.0.1", "yoga-layout": "^3.2.1" } }, "sha512-gN6PmWoEffvlIkifLfEhMsVucRywVMyH3rnxdyOVOhGy0nWJKKGpHyPc4plbDdpP6EfZ0r8prHXujDSkIG2nSA=="], + + "@react-pdf/pdfkit": ["@react-pdf/pdfkit@5.1.1", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@noble/ciphers": "^1.0.0", "@noble/hashes": "^1.6.0", "browserify-zlib": "^0.2.0", "fontkit": "^2.0.2", "jay-peg": "^1.1.1", "js-md5": "^0.8.3", "linebreak": "^1.1.0", "png-js": "^2.0.0", "vite-compatible-readable-stream": "^3.6.1" } }, "sha512-wNcdSsNlNYyGHGAgIdt453egBF7fiF9UxpRlklUfVvu8OWCrUppG9xiUrPLVoKiqWet5tMi0w6LmuFUJuYqjEg=="], + + "@react-pdf/primitives": ["@react-pdf/primitives@4.3.0", "", {}, "sha512-nYXoZ36pvwNzbc54+DbL8RCn15jU7woJ9D/svnh5tpUXekJ+CbI4mZLo6boSv24CvJgychOu6h7gxX03B4ps0A=="], + + "@react-pdf/reconciler": ["@react-pdf/reconciler@2.0.0", "", { "dependencies": { "object-assign": "^4.1.1", "scheduler": "0.25.0-rc-603e6108-20241029" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw=="], + + "@react-pdf/render": ["@react-pdf/render@4.5.1", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@react-pdf/fns": "3.1.3", "@react-pdf/primitives": "^4.3.0", "@react-pdf/textkit": "^6.3.0", "@react-pdf/types": "^2.11.1", "abs-svg-path": "^0.1.1", "color-string": "^2.1.4", "normalize-svg-path": "^1.1.0", "parse-svg-path": "^0.1.2", "svg-arc-to-cubic-bezier": "^3.2.0" } }, "sha512-IW/N4HWJWtioBXCf7n02IR24VJJ8gbdS3jGypf+vW/rSErEx3/URRzh9UK6Ma8Fpog9+T/W6GE2NHJ5AAKHhVA=="], + + "@react-pdf/renderer": ["@react-pdf/renderer@4.5.1", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@react-pdf/fns": "3.1.3", "@react-pdf/font": "^4.0.8", "@react-pdf/layout": "^4.6.1", "@react-pdf/pdfkit": "^5.1.1", "@react-pdf/primitives": "^4.3.0", "@react-pdf/reconciler": "^2.0.0", "@react-pdf/render": "^4.5.1", "@react-pdf/types": "^2.11.1", "events": "^3.3.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", "queue": "^6.0.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-5r1VQrE6FRLXX5wWUxwZzM24E2BJMo6g8AQWuS8WyPs9ugu5yMnb2g8/RpPYka/Z6J+RUEWc32wty2NoUJF42Q=="], + + "@react-pdf/stylesheet": ["@react-pdf/stylesheet@6.2.1", "", { "dependencies": { "@react-pdf/fns": "3.1.3", "@react-pdf/types": "^2.11.1", "color-string": "^2.1.4", "hsl-to-hex": "^1.0.0", "media-engine": "^1.0.3", "postcss-value-parser": "^4.1.0" } }, "sha512-2+UEk+7e+z8baaWi2l5kPLWmwtJeOI+T5wW9GGeN3iDH7vd3kbTqOpN1yt9mmfNVZFxQsnDHpznFb5v5UF983A=="], + + "@react-pdf/svg": ["@react-pdf/svg@1.1.0", "", { "dependencies": { "@react-pdf/primitives": "^4.3.0" } }, "sha512-cTIHXiz9x1HrbfqzfxfZP3FRdDwUXG77QWF6Fb5MP/lV3ONxR+g0Z3hwtBatCS9HeGBQCpxX/Lzb8wHE+co1PA=="], + + "@react-pdf/textkit": ["@react-pdf/textkit@6.3.0", "", { "dependencies": { "@react-pdf/fns": "3.1.3", "bidi-js": "^1.0.2", "hyphen": "^1.6.4", "unicode-properties": "^1.4.1" } }, "sha512-v6+V8nAcVwm7s2s1jIG2MD3Iw//x/k+XrH1foWOELBE4b32pyDgKyPXN/6KJE0dnX7+fVy27uctLNCLNMvzKzQ=="], + + "@react-pdf/types": ["@react-pdf/types@2.11.1", "", { "dependencies": { "@react-pdf/font": "^4.0.8", "@react-pdf/primitives": "^4.3.0", "@react-pdf/stylesheet": "^6.2.1" } }, "sha512-i9xQgfaDU9QoeNnbp6rltXCWg1huEh195rpOuN8cE4BZ2FuLdQrsIcb2dhFF9aOxXf+XBA6LOSpIW051MDD/bw=="], + + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], + + "@smithy/core": ["@smithy/core@3.26.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.5.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.5.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA=="], + + "@smithy/types": ["@smithy/types@4.15.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + + "@t3-oss/env-core": ["@t3-oss/env-core@0.12.0", "", { "peerDependencies": { "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw=="], + + "@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.12.0", "", { "dependencies": { "@t3-oss/env-core": "0.12.0" }, "peerDependencies": { "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-rFnvYk1049RnNVUPvY8iQ55AuQh1Rr+qZzQBh3t++RttCGK4COpXGNxS4+45afuQq02lu+QAOy/5955aU8hRKw=="], + + "@tabby_ai/hijri-converter": ["@tabby_ai/hijri-converter@1.0.5", "", {}, "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.101.0", "", {}, "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.101.0", "", { "dependencies": { "@tanstack/query-core": "5.101.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg=="], + + "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], + + "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + + "@tiptap/core": ["@tiptap/core@3.22.4", "", { "peerDependencies": { "@tiptap/pm": "3.22.4" } }, "sha512-vGIGm/HpqLg8EAAQXQ+koV+/S828OEpzocfWcPOwo1u2QUVf9dQG47Yy6JJ8zFFaJwfv4dBcOXli+7BrJwsxDQ=="], + + "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-7/61kNPbGFhMgM//zMknD0pSb69rGdRIkpulXOWS1JBrFHkH6hjZDfrOETNzgKkO+NlmzVl9rXSTv0xauS3lzA=="], + + "@tiptap/extension-bold": ["@tiptap/extension-bold@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-jIaPKfNOQu2lhpbLDvtwlQqM+mjF+Kk+auHpzYjBnsuwUli1Cl5ZOau7RH+rru/SQvZe1DtpQlANujDywugZAA=="], + + "@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.22.4", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "3.22.4", "@tiptap/pm": "3.22.4" } }, "sha512-v4pux5Ql3THAEjaLMY4ldtdy/Xy2qU7PJLBkq8ugLp8qicaKC+tpqxp6sGif4vLIjz7Ap5hurRbTNbXzszyyHA=="], + + "@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.22.4", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.4" } }, "sha512-TB+d3fGcTixYjO7coKqTr1mGTJuqr8hjDCPUFgzuvKyJnBhqWITmBzQ/8CLq4rr6mihgGURbD3N+xkQuPAKFiw=="], + + "@tiptap/extension-code": ["@tiptap/extension-code@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-cnbxmVhAcc7X3G81QUYEmKP0ve2hRmvAiFXBuuv9RUtQlBiRnzmhHoJOMgkX0CsMR7+8kMRpTfeDUYq2xp5s5w=="], + + "@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4", "@tiptap/pm": "3.22.4" } }, "sha512-MEurzNXfMET3rhjpoPJYUgMfxTdTqbzT9+ToFrqNGAHocdXVm6m1hhO2frVC7fEtHPnxXKsn0Z3NUbCRkRTLuA=="], + + "@tiptap/extension-color": ["@tiptap/extension-color@3.22.4", "", { "peerDependencies": { "@tiptap/extension-text-style": "3.22.4" } }, "sha512-1vDuVsrOETshe4j4nZhWalbKYcWfNybRCe30h829ExX06XwFryUYLb/LgTIaGCr9beWZUldsK+vOkBWdDTGMTw=="], + + "@tiptap/extension-document": ["@tiptap/extension-document@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-XQKla1+703FqQJC48tPDVgt9ucGiFbIEmQdOg5L5o07z9a6/NzuaZAc+1zJ7NxcUZzy+z6wBn1PrVMTiqiSXlw=="], + + "@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.22.4", "", { "peerDependencies": { "@tiptap/extensions": "3.22.4" } }, "sha512-N9/yMDC35jJp0V/naL0+6gi4gUDUIcPpWEzFdCDWUSYBA8mt41c1kI1ZU7UTKYIBzTClenhYHRc2XKZxxx0+LQ=="], + + "@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.22.4", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.22.4", "@tiptap/pm": "3.22.4" } }, "sha512-DFuyYxgaZPgxum5z1yvJPbfYCvDdO8geXsdyqt0qYYdiat3aGE4ncJhiLRIFDhSHBhaZg5eCgu/YPYAN6jZnrA=="], + + "@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.22.4", "", { "peerDependencies": { "@tiptap/extensions": "3.22.4" } }, "sha512-UYBEUj3SFpKINIE7AdzcyeS3xICK+ee+YLBbuqNXyHStYChjJOohzJehqiqhjR16A88KQQ+ZjgyDcItKGygSog=="], + + "@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-xq+a4dE7T6VwApCkh/yU3p30gn3F8g8Arb9CyEZm58/WIJUIGvHSTjDdHmvU16+kiWSBg+wOOsaFHhYjJjxcKA=="], + + "@tiptap/extension-heading": ["@tiptap/extension-heading@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-TUaj5f0Ir5qy9HKKt2ocnwfXKpZDYeHgbbP9gshKFzdq5PLe1RbIgkjfy6bnoI865cYjmPYWRjcT7XsKyIcb9Q=="], + + "@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4", "@tiptap/pm": "3.22.4" } }, "sha512-cCI1HekGQwhY/MbgaKQ0R/7HcH5ZM1oFAyI/J72QGLC0XnF403S/OXoHMuBWr1mCu8hNiQWCzeNRJUty0iytNw=="], + + "@tiptap/extension-italic": ["@tiptap/extension-italic@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-fVSDx5AYXgDI3v2zZIqb7V8EewthwM2NJ/ZCX+XaxRsqNEpnjVhgHs7UlvDqK1wj2OJ6zmUNjPtVlAFRxwT+HQ=="], + + "@tiptap/extension-link": ["@tiptap/extension-link@3.22.4", "", { "dependencies": { "linkifyjs": "^4.3.2" }, "peerDependencies": { "@tiptap/core": "3.22.4", "@tiptap/pm": "3.22.4" } }, "sha512-uoP3yus02uwGPVzW2QaEPJWVIrUb/r5nKm6c8DiJv9fNSX1+gykZZMg42c6GwRFLZ/vyfWjVCbAE03VMUqafgA=="], + + "@tiptap/extension-list": ["@tiptap/extension-list@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4", "@tiptap/pm": "3.22.4" } }, "sha512-Xe8UFvvHmyp/c/TJsFwlwU9CWACYbBirNsluJ3U1+H8BTu1wqdrT/AXR5uIXeyCl5kiWKgX5q71eHWbYFOrqrg=="], + + "@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.22.4", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.4" } }, "sha512-H659KXTvggSypIDWSOJBZ37jh9pKjQriDDvYPYvOZCdfij0D0hsDXN/wXoypArneUkoBdgruHfTtMkFOaQlgkw=="], + + "@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.22.4", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.4" } }, "sha512-t/zhker4oIS78AIGYDdFFfZC6zSBlszfD7z/zqFLGCg5PHNNgkZK5hKj6Vyix6D2SapRn/ajnx+8mhbKIUH5eA=="], + + "@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.22.4", "", { "peerDependencies": { "@tiptap/extension-list": "3.22.4" } }, "sha512-w77hPVf7pcHt97vfrybg/l0t5CimCd4y75OJKuHuo3CfgM5xbUP/gaPNMDyLLe7MYole/UHi/XvG3XjgzqTzAw=="], + + "@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-de6dFkIhigiENESY6rNJ3yTVS/337ybfP30dNPudTwGe9oAu9ZCS+04j6QCvXSjhlI3ULiv7wiSHqrP26Gd+Hw=="], + + "@tiptap/extension-strike": ["@tiptap/extension-strike@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-aRHWQj42HiailXSC9LkKYM3jWMcSeGwOjbqM4PiuxQZmHVDRFmeHkfJItOdn2cSHaO0vuEVK+TvrWUWsBFi3pg=="], + + "@tiptap/extension-text": ["@tiptap/extension-text@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-mM69uUW5cSxIhyEpWXi/YcfyupcJMDLCPEfYi62awH0iOP/LRoCv/nHjJq4Hyj/KxRJbe8HKwIUnqaCUf7m5Pg=="], + + "@tiptap/extension-text-align": ["@tiptap/extension-text-align@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-W7TnXWSyfDXSatGXp5y/CahE8G4btrQPb0/sy+eG+42FxdzYsqvh1ys3OE9j2XSuTrZ1q/tZA/NLPUkc7vw6Kw=="], + + "@tiptap/extension-text-style": ["@tiptap/extension-text-style@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-24DVBdySNKq3ovY+v9ERVxAyHStDa6ftUlyoHuZv0YXQ2amjUNOmqQtGEHBIULpCbBb1jZ+atHhv9MBZ0Ia9Pw=="], + + "@tiptap/extension-underline": ["@tiptap/extension-underline@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-08kGdbhIrA6h10GWXqOkqIveaBj5tmxclK208/nUIAlonI9hPd739vu7fmVtpnmqCnSSNpoRtU4u6Gj5at0ZpA=="], + + "@tiptap/extensions": ["@tiptap/extensions@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4", "@tiptap/pm": "3.22.4" } }, "sha512-fOe8VptJvLPs32bNdUYo8SRyljwqKNQVXWW056VoXIc5en/59OdJlJQVeHI0jRRciH3MtrqODi/gfJR0VHNZ8A=="], + + "@tiptap/pm": ["@tiptap/pm@3.22.4", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-keymap": "^1.2.2", "prosemirror-model": "^1.24.1", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.38.1" } }, "sha512-hj8Qka6WcHRllHUdeSjDnq2XaisUo4KsoGJc1WcFpoa1Yd+OeD861zUMnV7DFVGdZRy45Obht0CUYJpXQ4yA4w=="], + + "@tiptap/react": ["@tiptap/react@3.22.4", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.22.4", "@tiptap/extension-floating-menu": "^3.22.4" }, "peerDependencies": { "@tiptap/core": "3.22.4", "@tiptap/pm": "3.22.4", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XIQZPwLakR1t8+Q1UeCpr+kUHDWxpJzGy9r2xUi3mpPd6Wh8dtNltScBkUlCcr0sqc6J1GF6Is02JJVQGmCZMA=="], + + "@tiptap/starter-kit": ["@tiptap/starter-kit@3.22.4", "", { "dependencies": { "@tiptap/core": "^3.22.4", "@tiptap/extension-blockquote": "^3.22.4", "@tiptap/extension-bold": "^3.22.4", "@tiptap/extension-bullet-list": "^3.22.4", "@tiptap/extension-code": "^3.22.4", "@tiptap/extension-code-block": "^3.22.4", "@tiptap/extension-document": "^3.22.4", "@tiptap/extension-dropcursor": "^3.22.4", "@tiptap/extension-gapcursor": "^3.22.4", "@tiptap/extension-hard-break": "^3.22.4", "@tiptap/extension-heading": "^3.22.4", "@tiptap/extension-horizontal-rule": "^3.22.4", "@tiptap/extension-italic": "^3.22.4", "@tiptap/extension-link": "^3.22.4", "@tiptap/extension-list": "^3.22.4", "@tiptap/extension-list-item": "^3.22.4", "@tiptap/extension-list-keymap": "^3.22.4", "@tiptap/extension-ordered-list": "^3.22.4", "@tiptap/extension-paragraph": "^3.22.4", "@tiptap/extension-strike": "^3.22.4", "@tiptap/extension-text": "^3.22.4", "@tiptap/extension-underline": "^3.22.4", "@tiptap/extensions": "^3.22.4", "@tiptap/pm": "^3.22.4" } }, "sha512-qWjw+vfdin1rzMRpRU4cC5tLTwMJtUpXeQukv+6mOqqvhptuwuZBjUHImVEJaSPoHXS7+1ut+nTnrLyWyEuE5Q=="], + + "@trpc/client": ["@trpc/client@11.17.0", "", { "peerDependencies": { "@trpc/server": "11.17.0", "typescript": ">=5.7.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-KpJBFrbKTDeVCFv/3ckL1XBBH5Yssn8hethI/rUy7GIpTj+VzjtPjykDqJpzobuVOz+d26cXCSu1t4I6MYI5Zg=="], + + "@trpc/react-query": ["@trpc/react-query@11.17.0", "", { "peerDependencies": { "@tanstack/react-query": "^5.80.3", "@trpc/client": "11.17.0", "@trpc/server": "11.17.0", "react": ">=18.2.0", "typescript": ">=5.7.2" } }, "sha512-AGcl5YAF8NnhBmyJ6PqJqKb1M5VTGSoNRNqJ3orct4o4epdcg0GWhW+qT9q6gPzs/2ImIwYCdfFpgNGdZ9yLHA=="], + + "@trpc/server": ["@trpc/server@11.17.0", "", { "peerDependencies": { "typescript": ">=5.7.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-jbAOUe0PpUTCYqziyu+8vYXZdDXPudZgnEhWCQ2NjKnVEjfE93RqHTt1oycZJv/HNf51YlRXfEEwSIAbb161rw=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/bcryptjs": ["@types/bcryptjs@2.4.6", "", {}, "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/file-saver": ["@types/file-saver@2.0.7", "", {}, "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/node": ["@types/node@20.19.39", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw=="], + + "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], + + "@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="], + + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/type-utils": "8.60.1", "@typescript-eslint/utils": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.1", "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1" } }, "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.60.1", "", {}, "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.1", "@typescript-eslint/tsconfig-utils": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag=="], + + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], + + "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], + + "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], + + "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], + + "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], + + "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], + + "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], + + "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], + + "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], + + "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], + + "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], + + "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], + + "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], + + "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], + + "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], + + "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], + + "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], + + "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], + + "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + + "abs-svg-path": ["abs-svg-path@0.1.1", "", {}, "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "axe-core": ["axe-core@4.11.3", "", {}, "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg=="], + + "axios": ["axios@0.19.2", "", { "dependencies": { "follow-redirects": "1.5.10" } }, "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.34", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw=="], + + "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="], + + "better-auth": ["better-auth@1.6.16", "", { "dependencies": { "@better-auth/core": "1.6.16", "@better-auth/drizzle-adapter": "1.6.16", "@better-auth/kysely-adapter": "1.6.16", "@better-auth/memory-adapter": "1.6.16", "@better-auth/mongo-adapter": "1.6.16", "@better-auth/prisma-adapter": "1.6.16", "@better-auth/telemetry": "1.6.16", "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.2.2", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.6", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-YlBITnH3LIBRD+JpR1XRIToJAVVpoQvZzRc4sm5W0/bnPZKLbsmtXbVWJF3ypo9TVnF6geczJKprG/CsWT07Wg=="], + + "better-call": ["better-call@1.3.6", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-no1jI+h6Bkxs1NVBo4rONbVIzsPjZ8IUu7IHaJBiFwVX1XEQGN8KpHots5fSWmXe9nNyLuLIcgx6WEUcE6EDaA=="], + + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], + + "browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="], + + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chrono-node": ["chrono-node@2.9.1", "", {}, "sha512-nqP8Zp11efCYQIESXPxeDM8ikzN5BDb3Zzou+a66fZq+X2hzKFdsNLQE2/uBAh//BZEMbaMo1eTnagK7hOenAg=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="], + + "color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], + + "date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], + + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], + + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.344", "", {}, "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="], + + "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "es-toolkit": ["es-toolkit@1.46.0", "", {}, "sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + + "eslint-config-next": ["eslint-config-next@16.2.7", "", { "dependencies": { "@next/eslint-plugin-next": "16.2.7", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-CQ2aNXkrsjaGA2oJBE1LYnlRdphIAQE9ZQfX9hSv1PNGPyiOMSaVeBfTIO29QxYz+ij/hZudK0cfpCG1HXWstg=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], + + "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-drizzle": ["eslint-plugin-drizzle@0.2.3", "", { "peerDependencies": { "eslint": ">=8.0.0" } }, "sha512-BO+ymHo33IUNoJlC0rbd7HP9EwwpW4VIp49R/tWQF/d2E1K2kgTf0tCXT0v9MSiBr6gGR1LtPwMLapTKEWSg9A=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + + "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], + + "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "file-saver": ["file-saver@2.0.5", "", {}, "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="], + + "file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + + "follow-redirects": ["follow-redirects@1.5.10", "", { "dependencies": { "debug": "=3.1.0" } }, "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ=="], + + "fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "framer-motion": ["framer-motion@12.40.0", "", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "fuse.js": ["fuse.js@7.4.2", "", {}, "sha512-LVbzjD4WA6UP5B1UnP8wuaXJiLnqMdM/E4fiJXTJ5haJ5b/MBNsK29h2fm6swEoQaVQjvYFWKLE2RanyZIoRVQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + + "hsl-to-hex": ["hsl-to-hex@1.0.0", "", { "dependencies": { "hsl-to-rgb-for-reals": "^1.1.0" } }, "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA=="], + + "hsl-to-rgb-for-reals": ["hsl-to-rgb-for-reals@1.1.1", "", {}, "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg=="], + + "html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", "htmlparser2": "^8.0.2", "selderee": "^0.11.0" } }, "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg=="], + + "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + + "hyphen": ["hyphen@1.14.1", "", {}, "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-url": ["is-url@1.2.4", "", {}, "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "jay-peg": ["jay-peg@1.1.1", "", { "dependencies": { "restructure": "^3.0.0" } }, "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + + "js-md5": ["js-md5@0.8.3", "", {}, "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kysely": ["kysely@0.29.2", "", {}, "sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg=="], + + "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], + + "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + + "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="], + + "linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-engine": ["media-engine@1.0.3", "", {}, "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="], + + "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "nanostores": ["nanostores@1.3.0", "", {}, "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA=="], + + "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "next": ["next@16.2.12", "", { "dependencies": { "@next/env": "16.2.12", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.12", "@next/swc-darwin-x64": "16.2.12", "@next/swc-linux-arm64-gnu": "16.2.12", "@next/swc-linux-arm64-musl": "16.2.12", "@next/swc-linux-x64-gnu": "16.2.12", "@next/swc-linux-x64-musl": "16.2.12", "@next/swc-win32-arm64-msvc": "16.2.12", "@next/swc-win32-x64-msvc": "16.2.12", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw=="], + + "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], + + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + + "normalize-svg-path": ["normalize-svg-path@1.1.0", "", { "dependencies": { "svg-arc-to-cubic-bezier": "^3.0.0" } }, "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-svg-path": ["parse-svg-path@0.1.2", "", {}, "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="], + + "parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], + + "pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="], + + "pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="], + + "pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="], + + "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "png-js": ["png-js@2.0.0", "", { "dependencies": { "fflate": "^0.8.2" } }, "sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "prosemirror-changeset": ["prosemirror-changeset@2.4.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw=="], + + "prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="], + + "prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="], + + "prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.1", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw=="], + + "prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="], + + "prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="], + + "prosemirror-model": ["prosemirror-model@1.25.4", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA=="], + + "prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="], + + "prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="], + + "prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="], + + "prosemirror-transform": ["prosemirror-transform@1.12.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="], + + "prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], + + "react-colorful": ["react-colorful@5.7.0", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg=="], + + "react-day-picker": ["react-day-picker@9.14.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "@tabby_ai/hijri-converter": "1.0.5", "date-fns": "^4.1.0", "date-fns-jalali": "4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA=="], + + "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], + + "react-dropzone": ["react-dropzone@14.4.1", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-QDuV76v3uKbHiH34SpwifZ+gOLi1+RdsCO1kl5vxMT4wW8R82+sthjvBw4th3NHF/XX6FBsqDYZVNN+pnhaw0g=="], + + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "react-promise-suspense": ["react-promise-suspense@0.3.4", "", { "dependencies": { "fast-deep-equal": "^2.0.1" } }, "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ=="], + + "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="], + + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + + "resend": ["resend@4.8.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA=="], + + "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], + + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="], + + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svg-arc-to-cubic-bezier": ["svg-arc-to-cubic-bezier@3.2.0", "", {}, "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g=="], + + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + + "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], + + "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "trpc": ["trpc@0.11.3", "", { "dependencies": { "axios": "^0.19.2" } }, "sha512-vfj6WrxYk8XDZzCsFNLwo5WhlKi4IYmVRzRgRQAlcK8zH4sY0yIAlJ7Nd1lZcGFe985GfP2LZLoEsCrMsIl/tA=="], + + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="], + + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.60.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.1", "@typescript-eslint/parser": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], + + "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], + + "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + + "vite-compatible-readable-stream": ["vite-compatible-readable-stream@3.6.1", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ=="], + + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + + "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@better-auth/core/@better-fetch/fetch": ["@better-fetch/fetch@1.2.2", "", {}, "sha512-xlgQcYROGFgKg5FY7ZLppFmG7rR5Hkmz7tgDuQeR79i5KhKRjr2QC9xsBG2qEGPJJjf9bxzg/NMW2hEUWs5OnA=="], + + "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@better-auth/expo/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@better-auth/telemetry/@better-fetch/fetch": ["@better-fetch/fetch@1.2.2", "", {}, "sha512-xlgQcYROGFgKg5FY7ZLppFmG7rR5Hkmz7tgDuQeR79i5KhKRjr2QC9xsBG2qEGPJJjf9bxzg/NMW2hEUWs5OnA=="], + + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], + + "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], + + "@react-email/render/prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "@react-pdf/pdfkit/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + + "@react-pdf/pdfkit/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@react-pdf/reconciler/scheduler": ["scheduler@0.25.0-rc-603e6108-20241029", "", {}, "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA=="], + + "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "better-auth/@better-fetch/fetch": ["@better-fetch/fetch@1.2.2", "", {}, "sha512-xlgQcYROGFgKg5FY7ZLppFmG7rR5Hkmz7tgDuQeR79i5KhKRjr2QC9xsBG2qEGPJJjf9bxzg/NMW2hEUWs5OnA=="], + + "better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "better-call/@better-fetch/fetch": ["@better-fetch/fetch@1.2.2", "", {}, "sha512-xlgQcYROGFgKg5FY7ZLppFmG7rR5Hkmz7tgDuQeR79i5KhKRjr2QC9xsBG2qEGPJJjf9bxzg/NMW2hEUWs5OnA=="], + + "brotli/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.10.23", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g=="], + + "color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "eslint-plugin-react-hooks/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "follow-redirects/debug": ["debug@3.1.0", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g=="], + + "fontkit/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], + + "is-bun-module/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "next/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "pg/pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="], + + "react-day-picker/date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="], + + "tsx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + + "unicode-properties/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "@img/sharp-freebsd-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + + "follow-redirects/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "next/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "next/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "next/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "next/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "next/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "next/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "next/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "next/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "next/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "next/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "next/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "next/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "next/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "next/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "next/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "next/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "next/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "next/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "next/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "next/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "next/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "next/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "next/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "next/sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + } +} diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 0000000..f5d25ec --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/apps/web/docker-compose.coolify-garage.yml b/apps/web/docker-compose.coolify-garage.yml new file mode 100644 index 0000000..ecfd235 --- /dev/null +++ b/apps/web/docker-compose.coolify-garage.yml @@ -0,0 +1,74 @@ +# Garage-only stack for Coolify when beenvoice runs as a separate Application resource. +# +# Deploy: Coolify → Docker Compose → compose file: docker-compose.coolify-garage.yml +# +# ── Pair with a beenvoice Application (pick ONE) ─────────────────────────────── +# +# A) Public Garage URL (most reliable — no shared Docker network required) +# 1. Redeploy this stack (includes SERVICE_FQDN_GARAGE_3900 below). +# 2. Garage resource → assign a domain for port 3900 (e.g. s3.example.com). +# 3. Copy SERVICE_URL_GARAGE_3900 from this resource's Environment tab. +# 4. beenvoice Application → S3_ENDPOINT= → redeploy beenvoice. +# +# B) Internal Docker DNS (same Coolify destination network) +# 1. Garage resource → Advanced → enable "Connect to Predefined Network" → redeploy. +# 2. beenvoice Application → same destination → enable "Connect to Predefined Network". +# 3. beenvoice → S3_ENDPOINT=http://garage-:3900 +# +# Recommended long-term: deploy docker-compose.coolify.yml as one stack (app+db+garage). +# See docs/COOLIFY.md. +services: + garage: + image: dxflrs/garage:v2.3.0 + environment: + GARAGE_DEFAULT_ACCESS_KEY: ${S3_ACCESS_KEY} + GARAGE_DEFAULT_SECRET_KEY: ${S3_SECRET_KEY} + GARAGE_DEFAULT_BUCKET: ${S3_BUCKET:-beenvoice-receipts} + SERVICE_FQDN_GARAGE_3900: + configs: + - source: garage_config + target: /etc/garage.toml + volumes: + - beenvoice_garage_meta:/var/lib/garage/meta + - beenvoice_garage_data:/var/lib/garage/data + command: ["/garage", "server", "--single-node", "--default-bucket"] + expose: + - "3900" + healthcheck: + test: ["CMD", "/garage", "status"] + interval: 5s + timeout: 5s + retries: 15 + start_period: 20s + restart: unless-stopped + +volumes: + beenvoice_garage_meta: + beenvoice_garage_data: + +configs: + garage_config: + content: | + metadata_dir = "/var/lib/garage/meta" + data_dir = "/var/lib/garage/data" + db_engine = "sqlite" + replication_factor = 1 + + rpc_bind_addr = "[::]:3901" + rpc_public_addr = "garage:3901" + rpc_secret = "rpc_secret_change_me_in_production" + + [s3_api] + s3_region = "garage" + api_bind_addr = "[::]:3900" + root_domain = ".s3.garage" + + [s3_web] + bind_addr = "[::]:3902" + root_domain = ".web.garage" + index = "index.html" + + [admin] + api_bind_addr = "[::]:3903" + admin_token = "beenvoice_garage_admin_token_change_me_in_production" + metrics_token = "beenvoice_garage_metrics_token_change_me_in_production" diff --git a/apps/web/docker-compose.coolify.yml b/apps/web/docker-compose.coolify.yml new file mode 100644 index 0000000..1e2e942 --- /dev/null +++ b/apps/web/docker-compose.coolify.yml @@ -0,0 +1,124 @@ +# beenvoice on Coolify — single Docker Compose resource (recommended). +# +# Deploy: Coolify → New Resource → Docker Compose → compose file: docker-compose.coolify.yml +# +# 1. Assign a domain to the `app` service in Coolify (SERVICE_FQDN_APP wires Traefik). +# 2. Set AUTH_SECRET, POSTGRES_PASSWORD, S3_ACCESS_KEY, S3_SECRET_KEY in the resource env (see .env.example). +# 3. Do NOT override S3_ENDPOINT — this stack sets http://garage:3900 on the shared network. +# 4. Rebuild after changing NEXT_PUBLIC_* (image build args use SERVICE_URL_APP). +# +# Migrating from Application + separate Postgres + Garage compose: +# - Export Postgres data, point DATABASE_URL at this stack's `db` service, redeploy once here. +# - Or keep external Postgres and remove the `db` service + volume from this file. +services: + app: + build: + context: . + args: + NEXT_PUBLIC_APP_URL: ${SERVICE_URL_APP:-${NEXT_PUBLIC_APP_URL:-http://localhost:3000}} + BETTER_AUTH_URL: ${SERVICE_URL_APP:-${BETTER_AUTH_URL:-http://localhost:3000}} + image: ${BEENVOICE_IMAGE:-beenvoice:coolify} + environment: + SERVICE_FQDN_APP: + NODE_ENV: production + AUTH_SECRET: ${AUTH_SECRET:?Set AUTH_SECRET in Coolify env} + DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-postgres} + DB_DISABLE_SSL: "true" + BETTER_AUTH_URL: ${SERVICE_URL_APP:-${BETTER_AUTH_URL:-http://localhost:3000}} + NEXT_PUBLIC_APP_URL: ${SERVICE_URL_APP:-${NEXT_PUBLIC_APP_URL:-http://localhost:3000}} + RESEND_API_KEY: ${RESEND_API_KEY:-} + RESEND_DOMAIN: ${RESEND_DOMAIN:-} + NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID:-} + NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${NEXT_PUBLIC_UMAMI_SCRIPT_URL:-https://analytics.umami.is/script.js} + NEXT_PUBLIC_AUTHENTIK_ENABLED: ${NEXT_PUBLIC_AUTHENTIK_ENABLED:-false} + DISABLE_SIGNUPS: ${DISABLE_SIGNUPS:-true} + CRON_SECRET: ${CRON_SECRET:-} + AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER:-} + AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID:-} + AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET:-} + AUTHENTIK_ORIGIN: ${AUTHENTIK_ORIGIN:-} + S3_ENDPOINT: http://garage:3900 + S3_BUCKET: ${S3_BUCKET:-beenvoice-receipts} + S3_ACCESS_KEY: ${S3_ACCESS_KEY} + S3_SECRET_KEY: ${S3_SECRET_KEY} + S3_REGION: ${S3_REGION:-garage} + expose: + - "3000" + depends_on: + db: + condition: service_healthy + garage: + condition: service_healthy + restart: unless-stopped + + db: + image: postgres:17-alpine + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_DB: ${POSTGRES_DB:-postgres} + volumes: + - beenvoice_pg_data:/var/lib/postgresql/data + healthcheck: + test: + ["CMD-SHELL", 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"'] + interval: 5s + timeout: 5s + retries: 10 + restart: unless-stopped + + garage: + image: dxflrs/garage:v2.3.0 + environment: + GARAGE_DEFAULT_ACCESS_KEY: ${S3_ACCESS_KEY} + GARAGE_DEFAULT_SECRET_KEY: ${S3_SECRET_KEY} + GARAGE_DEFAULT_BUCKET: ${S3_BUCKET:-beenvoice-receipts} + SERVICE_FQDN_GARAGE_3900: + configs: + - source: garage_config + target: /etc/garage.toml + volumes: + - beenvoice_garage_meta:/var/lib/garage/meta + - beenvoice_garage_data:/var/lib/garage/data + command: ["/garage", "server", "--single-node", "--default-bucket"] + expose: + - "3900" + healthcheck: + test: ["CMD", "/garage", "status"] + interval: 5s + timeout: 5s + retries: 15 + start_period: 20s + restart: unless-stopped + +volumes: + beenvoice_pg_data: + beenvoice_garage_meta: + beenvoice_garage_data: + +configs: + garage_config: + content: | + metadata_dir = "/var/lib/garage/meta" + data_dir = "/var/lib/garage/data" + db_engine = "sqlite" + replication_factor = 1 + + rpc_bind_addr = "[::]:3901" + rpc_public_addr = "garage:3901" + rpc_secret = "rpc_secret_change_me_in_production" + + [s3_api] + s3_region = "garage" + api_bind_addr = "[::]:3900" + root_domain = ".s3.garage" + + [s3_web] + bind_addr = "[::]:3902" + root_domain = ".web.garage" + index = "index.html" + + [admin] + api_bind_addr = "[::]:3903" + admin_token = "beenvoice_garage_admin_token_change_me_in_production" + metrics_token = "beenvoice_garage_metrics_token_change_me_in_production" diff --git a/apps/web/docker-compose.dev.yml b/apps/web/docker-compose.dev.yml new file mode 100644 index 0000000..6f72af6 --- /dev/null +++ b/apps/web/docker-compose.dev.yml @@ -0,0 +1,74 @@ +services: + db: + image: postgres:17-alpine + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_DB: ${POSTGRES_DB:-postgres} + volumes: + - beenvoice_dev_pg_data:/var/lib/postgresql/data + healthcheck: + test: + ["CMD-SHELL", 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"'] + interval: 5s + timeout: 5s + retries: 10 + ports: + - "${POSTGRES_PORT:-5432}:5432" + restart: unless-stopped + + # S3-compatible receipt storage for host dev (`bun dev`). API :3900. + garage: + image: dxflrs/garage:v2.3.0 + environment: + GARAGE_DEFAULT_ACCESS_KEY: ${S3_ACCESS_KEY:-GK3515373e4c851ebaad366558} + GARAGE_DEFAULT_SECRET_KEY: ${S3_SECRET_KEY:-7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34} + GARAGE_DEFAULT_BUCKET: ${S3_BUCKET:-beenvoice-receipts} + configs: + - source: garage_config + target: /etc/garage.toml + volumes: + - beenvoice_dev_garage_meta:/var/lib/garage/meta + - beenvoice_dev_garage_data:/var/lib/garage/data + command: ["/garage", "server", "--single-node", "--default-bucket"] + ports: + - "${GARAGE_API_PORT:-3900}:3900" + healthcheck: + test: ["CMD", "/garage", "status"] + interval: 5s + timeout: 5s + retries: 15 + start_period: 20s + restart: unless-stopped + +volumes: + beenvoice_dev_pg_data: + beenvoice_dev_garage_meta: + beenvoice_dev_garage_data: + +configs: + garage_config: + content: | + metadata_dir = "/var/lib/garage/meta" + data_dir = "/var/lib/garage/data" + db_engine = "sqlite" + replication_factor = 1 + + rpc_bind_addr = "[::]:3901" + rpc_public_addr = "garage:3901" + rpc_secret = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + + [s3_api] + s3_region = "garage" + api_bind_addr = "[::]:3900" + root_domain = ".s3.garage" + + [s3_web] + bind_addr = "[::]:3902" + root_domain = ".web.garage" + index = "index.html" + + [admin] + api_bind_addr = "[::]:3903" + admin_token = "beenvoice_garage_admin_token_change_me_in_production" + metrics_token = "beenvoice_garage_metrics_token_change_me_in_production" diff --git a/apps/web/docker-compose.yml b/apps/web/docker-compose.yml new file mode 100644 index 0000000..a28a5be --- /dev/null +++ b/apps/web/docker-compose.yml @@ -0,0 +1,123 @@ +# Production stack (app + Postgres + Garage). Local dev Postgres/Garage: docker-compose.dev.yml +# +# Coolify: deploy docker-compose.coolify.yml as ONE Docker Compose resource (preferred), +# or this file. S3_ENDPOINT=http://garage:3900 works only inside a single stack. +# Application + separate Garage → docs/COOLIFY.md. +# +# After git pull, rebuild before starting — a plain `docker compose up -d` reuses +# the existing local image and will NOT include new code. Use: +# ./scripts/docker-deploy.sh +# docker compose up -d --build +services: + app: + build: + context: . + args: + NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000} + BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000} + # Fixed default tag (beenvoice:local) is reused until you --build. docker-deploy.sh + # sets BEENVOICE_IMAGE=beenvoice: so each deploy gets a fresh tag. + image: ${BEENVOICE_IMAGE:-beenvoice:local} + environment: + NODE_ENV: production + AUTH_SECRET: ${AUTH_SECRET:?Set AUTH_SECRET in .env} + DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-postgres} + DB_DISABLE_SSL: "true" + BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000} + NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000} + RESEND_API_KEY: ${RESEND_API_KEY:-} + RESEND_DOMAIN: ${RESEND_DOMAIN:-} + NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID:-} + NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${NEXT_PUBLIC_UMAMI_SCRIPT_URL:-https://analytics.umami.is/script.js} + NEXT_PUBLIC_AUTHENTIK_ENABLED: ${NEXT_PUBLIC_AUTHENTIK_ENABLED:-false} + DISABLE_SIGNUPS: ${DISABLE_SIGNUPS:-true} + CRON_SECRET: ${CRON_SECRET:-} + AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER:-} + AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID:-} + AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET:-} + AUTHENTIK_ORIGIN: ${AUTHENTIK_ORIGIN:-} + S3_ENDPOINT: http://garage:3900 + S3_BUCKET: ${S3_BUCKET:-beenvoice-receipts} + S3_ACCESS_KEY: ${S3_ACCESS_KEY:-GK3515373e4c851ebaad366558} + S3_SECRET_KEY: ${S3_SECRET_KEY:-7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34} + S3_REGION: ${S3_REGION:-garage} + ports: + - "${WEB_PORT:-${PORT:-3000}}:3000" + depends_on: + db: + condition: service_healthy + garage: + condition: service_healthy + restart: unless-stopped + + db: + image: postgres:17-alpine + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_DB: ${POSTGRES_DB:-postgres} + volumes: + - beenvoice_pg_data:/var/lib/postgresql/data + healthcheck: + test: + ["CMD-SHELL", 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"'] + interval: 5s + timeout: 5s + retries: 10 + restart: unless-stopped + + # S3-compatible receipt storage (~50–100 MB RAM vs MinIO). API :3900. + garage: + image: dxflrs/garage:v2.3.0 + environment: + GARAGE_DEFAULT_ACCESS_KEY: ${S3_ACCESS_KEY:-GK3515373e4c851ebaad366558} + GARAGE_DEFAULT_SECRET_KEY: ${S3_SECRET_KEY:-7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34} + GARAGE_DEFAULT_BUCKET: ${S3_BUCKET:-beenvoice-receipts} + configs: + - source: garage_config + target: /etc/garage.toml + volumes: + - beenvoice_garage_meta:/var/lib/garage/meta + - beenvoice_garage_data:/var/lib/garage/data + command: ["/garage", "server", "--single-node", "--default-bucket"] + ports: + - "${GARAGE_API_PORT:-3900}:3900" + healthcheck: + test: ["CMD", "/garage", "status"] + interval: 5s + timeout: 5s + retries: 15 + start_period: 20s + restart: unless-stopped + +volumes: + beenvoice_pg_data: + beenvoice_garage_meta: + beenvoice_garage_data: + +configs: + garage_config: + content: | + metadata_dir = "/var/lib/garage/meta" + data_dir = "/var/lib/garage/data" + db_engine = "sqlite" + replication_factor = 1 + + rpc_bind_addr = "[::]:3901" + rpc_public_addr = "garage:3901" + rpc_secret = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + + [s3_api] + s3_region = "garage" + api_bind_addr = "[::]:3900" + root_domain = ".s3.garage" + + [s3_web] + bind_addr = "[::]:3902" + root_domain = ".web.garage" + index = "index.html" + + [admin] + api_bind_addr = "[::]:3903" + admin_token = "beenvoice_garage_admin_token_change_me_in_production" + metrics_token = "beenvoice_garage_metrics_token_change_me_in_production" diff --git a/apps/web/docs/ARCHITECTURE.md b/apps/web/docs/ARCHITECTURE.md new file mode 100644 index 0000000..c23bb36 --- /dev/null +++ b/apps/web/docs/ARCHITECTURE.md @@ -0,0 +1,217 @@ +# beenvoice-web architecture + +Dense reference for the Next.js web application and API. Package manager: **Bun**. Database: **PostgreSQL** via Drizzle ORM. + +**Repository:** [git.soconnor.dev/soconnor/beenvoice-web](https://git.soconnor.dev/soconnor/beenvoice-web) + +## Stack + +| Layer | Technology | +|-------|------------| +| Framework | Next.js 16 App Router (`src/app/`) | +| API | tRPC 11 (`/api/trpc`), SuperJSON transformer | +| ORM | Drizzle + `pg` pool | +| Auth | better-auth (email/password, optional Authentik OIDC, Expo plugin for mobile) | +| UI | shadcn/ui, Tailwind CSS v4, Radix primitives | +| Email | Resend | +| PDF | `@react-pdf/renderer` | + +## Request flow + +``` +Browser / Mobile / MCP client + │ + ├─► /api/auth/* → better-auth handler (session cookies) + ├─► /api/trpc/* → createContext() → appRouter + │ ├─ Bearer / x-api-key → api-key auth + │ └─ else → better-auth session + ├─► /api/mcp → API key only → JSON-RPC tools → tRPC caller + ├─► /api/i/[token]/pdf → public invoice PDF + └─► /dashboard/* → RSC + client components (session required in UI) +``` + +**Context** (`src/server/api/trpc.ts`): `protectedProcedure` requires `ctx.session.user`. API-key auth sets `authSource: "api-key"`; `apiKeys.*` mutations require session (cannot manage keys with a key). + +## Directory layout + +``` +src/ +├── app/ # Routes (pages + route handlers) +│ ├── api/ +│ │ ├── auth/ # better-auth catch-all + custom register/reset REST +│ │ ├── trpc/[trpc]/ # tRPC HTTP adapter +│ │ ├── mcp/ # MCP over HTTP (API key) +│ │ ├── i/[token]/pdf/ # Public PDF +│ │ └── cron/ # Recurring invoice generation (CRON_SECRET) +│ ├── auth/ # sign-in, register, forgot/reset password +│ ├── dashboard/ # Authenticated app shell +│ └── i/[token]/ # Public invoice view +├── components/ # Shared UI (ui/, forms/, layout/, data/) +├── hooks/ +├── lib/ # auth.ts, pdf-export, email templates, branding +├── server/ +│ ├── api/ +│ │ ├── root.ts # appRouter composition +│ │ ├── trpc.ts # procedures, context, timing middleware (dev) +│ │ ├── api-keys.ts +│ │ └── routers/ # one file per domain +│ └── db/ +│ ├── schema.ts # all tables (prefix beenvoice_) +│ ├── index.ts # drizzle + pool +│ └── migrate.ts +├── trpc/ # react.tsx (client), server.ts (RSC) +├── env.js # @t3-oss/env-nextjs validation +└── styles/globals.css +drizzle/ # SQL migrations (0000–0014+) +``` + +## tRPC routers + +Root: `src/server/api/root.ts`. All routers use Zod input validation. + +| Namespace | File | Key procedures | +|-----------|------|----------------| +| `clients` | `routers/clients.ts` | getAll, getById, create, update, delete | +| `businesses` | `routers/businesses.ts` | getAll, getById, getDefault, create, update, delete, setDefault, getEmailConfig, updateEmailConfig | +| `invoices` | `routers/invoices.ts` | getAll, getBillable, getById, create, update, delete, updateStatus, bulk*, previewPdf, public token, **getByPublicToken** (public), sendReminder | +| `payments` | `routers/payments.ts` | getByInvoice, create, delete | +| `expenses` | `routers/expenses.ts` | getAll, getById, create, update, delete | +| `invoiceTemplates` | `routers/invoiceTemplates.ts` | CRUD by template type | +| `recurringInvoices` | `routers/recurring-invoices.ts` | CRUD, pause/resume, generateNow; cron helper `generateDueRecurringInvoices` | +| `timeEntries` | `routers/time-entries.ts` | getAll, getRunning, clockIn, updateRunning, clockOut, create, update, delete, getSummary | +| `dashboard` | `routers/dashboard.ts` | getStats | +| `email` | `routers/email.ts` | sendInvoice | +| `settings` | `routers/settings.ts` | profile, theme, animation prefs, export/import data, admin account roles | +| `apiKeys` | `routers/apiKeys.ts` | list, create, revoke (session-only) | + +### Time clock semantics + +- **One running entry per user** — partial unique index on `(createdById)` where `endedAt IS NULL`. +- `clockIn` — optional client, invoice, rate, backdated `startedAt`; resolves rate from input → client default → business default. +- `clockOut` — optional description update; computes hours; if `invoiceId` set, appends line item; else tries latest open invoice for client. +- Outcomes: `linked_to_invoice`, `saved_no_invoice`, `saved_no_client`, `zero_hours`. + +## Database schema + +Single file: `src/server/db/schema.ts`. Table names use `pgTableCreator` → prefix `beenvoice_`. + +### Auth & platform + +| Table | Notes | +|-------|-------| +| `beenvoice_user` | Core user; role for admin features | +| `beenvoice_account` | OAuth/credential accounts (better-auth) | +| `beenvoice_session` | Sessions; unique token | +| `beenvoice_verification_token` | Email verification / reset | +| `beenvoice_api_key` | `bv_` prefix keys; SHA-256 hash stored | +| `beenvoice_sso_provider` | OIDC/SAML config per user | +| `beenvoice_platform_setting` | Singleton (`id = global`) branding/PDF/appearance | + +### Domain + +| Table | FKs | Notes | +|-------|-----|-------| +| `beenvoice_client` | `createdById` → user | defaultHourlyRate, currency | +| `beenvoice_business` | `createdById` | Resend config, `isDefault` | +| `beenvoice_invoice` | client, business?, user | status draft/sent/paid; `publicToken` | +| `beenvoice_invoice_item` | invoice (cascade) | position ordering | +| `beenvoice_invoice_payment` | invoice, user | payment method enum | +| `beenvoice_expense` | business?, client?, invoice? | billable flags | +| `beenvoice_invoice_template` | user | notes/terms templates | +| `beenvoice_recurring_invoice` | client, business?, user | schedule, `nextDueAt` | +| `beenvoice_recurring_invoice_item` | recurring (cascade) | | +| `beenvoice_time_entry` | client?, invoice?, user | `endedAt` null = running | + +Migrations: `bun run db:generate` → `drizzle/`; apply with `db:push` (dev) or `db:migrate` (prod script). + +## Authentication + +**Server** — `src/lib/auth.ts`: + +- `betterAuth` + `drizzleAdapter` (users, sessions, accounts, verification) +- Plugins: `@better-auth/expo` (mobile SecureStore cookies), `nextCookies()`, optional `genericOAuth` (Authentik) +- Email/password with bcrypt (12 rounds); `DISABLE_SIGNUPS=true` blocks registration (custom `/api/auth/register` and better-auth `disableSignUp`) +- `trustedOrigins`: `BETTER_AUTH_URL`, `NEXT_PUBLIC_APP_URL`, `beenvoice://`, `exp://`, plus Authentik origin when configured + +**Web client** — `src/lib/auth-client.ts`: `createAuthClient` + `genericOAuthClient`. + +**Routes**: + +- `src/app/api/auth/[...all]/route.ts` — better-auth handler +- Custom REST: `register`, `forgot-password`, `reset-password`, `validate-reset-token` (used by mobile and legacy flows) + +**Session cookies**: `better-auth.session_token` or `__Secure-better-auth.session_token` in production. + +## Mobile API contract + +The Expo app (`beenvoice-app`) does **not** use API keys. It: + +1. Calls the same tRPC endpoints with `Authorization` cookie header from `authClient.getCookie()`. +2. Stores session per account in SecureStore via `@better-auth/expo` (`storagePrefix`: `beenvoice:guest` or `beenvoice:auth:{accountId}`). +3. Requires `trustedOrigins` and matching `BETTER_AUTH_URL` for the host the device can reach. + +Ensure `src/lib/auth.ts` keeps the `expo()` plugin enabled. + +## MCP (machine clients) + +`POST /api/mcp` — JSON-RPC 2.0, protocol `2025-11-25`. + +- **Auth**: API key only (`Authorization: Bearer bv_…` or `x-api-key`). Session cookies rejected. +- **Tools**: ~50 tools mirroring tRPC (invoices, clients, time clock, expenses, etc.) +- Implemented in `src/app/api/mcp/route.ts`; delegates to `createCaller(createContext)`. + +API keys: format `bv_`; stored as SHA-256 hash (`src/server/api/api-keys.ts`). + +## Environment variables + +Validated in `src/env.js`. See `.env.example`. + +| Variable | Required | Notes | +|----------|----------|-------| +| `DATABASE_URL` | yes | PostgreSQL connection string | +| `AUTH_SECRET` | prod | `openssl rand -base64 32` | +| `BETTER_AUTH_URL` | yes | Public URL of API (no trailing path) | +| `NEXT_PUBLIC_APP_URL` | yes | Browser-facing URL | +| `DB_DISABLE_SSL` | local | `true` for Docker dev DB | +| `RESEND_API_KEY`, `RESEND_DOMAIN` | optional | Email; blank disables send | +| `AUTHENTIK_*` | optional | OIDC SSO | +| `DISABLE_SIGNUPS` | optional | `true` blocks registration; use string `true`/`false` (parsed in `src/env.js`) | +| `CRON_SECRET` | cron route | Protects `/api/cron/generate-recurring` | +| `NEXT_PUBLIC_BRAND_*` | optional | Build-time white-label defaults | + +## Docker + +| File | Use | +|------|-----| +| `docker-compose.yml` | Deploy: `app` + `db` (Postgres internal); copy `.env.example` → `.env` | +| `docker-compose.dev.yml` | Local dev: Postgres only, port `${POSTGRES_PORT:-5432}` | + +App image built from `Dockerfile`. Container `CMD`: `bun migrate.ts && bun run start` (migrations then `next start` on port 3000). Docker builds run `next build` on Node 22 (not Bun) to avoid arm64 worker crashes; runtime stays on Bun. Docker builds disable React Compiler and use `experimental.webpackMemoryOptimizations` to reduce peak RAM. + +Set `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` to the public hostname before deploy. Rebuild the image when changing `NEXT_PUBLIC_*` build-time vars. + +**Deploy / update:** `git pull && ./scripts/docker-deploy.sh` (or `docker compose up -d --build`). Plain `docker compose up -d` reuses the local `beenvoice:local` image and does not include pulled code. The deploy script tags images as `beenvoice:`. + +## Scripts + +```bash +bun run dev # next dev --turbo +bun run build # production build +bun run db:push # push schema (dev) +bun run db:migrate # run migrations +bun run db:studio # Drizzle Studio +bun run check # eslint + tsc +``` + +## Public / unauthenticated surfaces + +- `invoices.getByPublicToken` (tRPC publicProcedure) +- `/i/[token]` page and `/api/i/[token]/pdf` +- Auth REST endpoints for register/reset + +## Related docs + +- [forms-guide.md](./forms-guide.md), [UI_UNIFORMITY_GUIDE.md](./UI_UNIFORMITY_GUIDE.md) +- [data-table-responsive-guide.md](./data-table-responsive-guide.md) +- [email-features.md](./email-features.md) +- Mobile companion: `../beenvoice-app/docs/ARCHITECTURE.md` diff --git a/apps/web/docs/COOLIFY.md b/apps/web/docs/COOLIFY.md new file mode 100644 index 0000000..b5d9b7b --- /dev/null +++ b/apps/web/docs/COOLIFY.md @@ -0,0 +1,137 @@ +# Coolify deployment — beenvoice + Garage + +beenvoice stores receipt files in S3-compatible storage when `S3_BUCKET`, `S3_ACCESS_KEY`, and `S3_SECRET_KEY` are set. [Garage](https://garagehq.deuxfleurs.fr/) is the default on self-hosted Coolify (~50–100 MB RAM vs MinIO's ~500 MB+). + +## Why `getaddrinfo ENOTFOUND garage` happens + +Docker DNS resolves service names **only inside the same Docker network**. + +| Setup | Does `http://garage:3900` work? | +|-------|--------------------------------| +| Single Compose stack (app + garage together) | Yes — Compose service name `garage` | +| beenvoice **Application** + Garage **separate Compose** | **No** — each resource has its own network by default | +| Application + Garage with shared destination network + correct hostname | Yes — hostname is usually **`garage-`**, not bare `garage` | +| Application + Garage via **public domain** (`SERVICE_URL_GARAGE_3900`) | Yes — no Docker DNS needed | + +Setting `S3_ENDPOINT=http://garage:3900` on a standalone beenvoice Application fails because the app container is not on the Garage stack's network. Node returns `ENOTFOUND garage`. + +Also avoid `http://localhost:3900` inside the app container — that points at the app itself, not Garage. + +--- + +## Quick fix — keep beenvoice as Application + separate Garage compose + +Use this if you are **not** migrating to a single Compose stack today. + +### Path A — public Garage URL (recommended, works without shared Docker network) + +This is the most reliable fix when beenvoice is a Coolify **Application** (Dockerfile) and Garage is a separate Compose resource. + +1. **Update the Garage stack** to the latest `docker-compose.coolify-garage.yml` from this repo (includes `SERVICE_FQDN_GARAGE_3900`) and **redeploy** the Garage resource. +2. In the **Garage Compose resource** → assign a domain for **port 3900** (e.g. `s3.yourdomain.com`). Coolify generates TLS via Traefik/Caddy. +3. Open the Garage resource **Environment** tab and copy **`SERVICE_URL_GARAGE_3900`** (e.g. `https://s3.yourdomain.com`). +4. On the **beenvoice Application** → Environment: + +```env +S3_ENDPOINT=https://s3.yourdomain.com +S3_BUCKET=beenvoice-receipts +S3_ACCESS_KEY= +S3_SECRET_KEY= +S3_REGION=garage +``` + +5. **Redeploy beenvoice** (restart is not enough after env changes on some Coolify versions — trigger a full redeploy). + +`S3_FORCE_PATH_STYLE` defaults to on when `S3_ENDPOINT` is set (required for Garage behind a reverse proxy). Only set `S3_FORCE_PATH_STYLE=false` if you use AWS S3 with virtual-hosted-style buckets. + +### Path B — internal Docker DNS (same destination, no public Garage domain) + +Use when you want S3 API traffic to stay on the Docker network. + +1. Put beenvoice Application and Garage Compose in the **same Coolify project** and **same destination** (server/network). +2. **Garage Compose resource** → **Advanced** → enable **Connect to Predefined Network** → **redeploy Garage**. +3. **beenvoice Application** → **Advanced** → enable **Connect to Predefined Network** (same destination) → **redeploy beenvoice**. +4. Find the Garage resource **UUID** (in the Coolify URL, e.g. `.../service/abc123def456`, or env `COOLIFY_RESOURCE_UUID` on the Garage container). +5. Set on beenvoice Application: + +```env +S3_ENDPOINT=http://garage-:3900 +``` + +Example: resource UUID `k8w2o0g4s0g8` → `S3_ENDPOINT=http://garage-k8w2o0g4s0g8:3900`. + +**Do not use bare `garage`** unless you verified it resolves from inside the beenvoice container (recent Coolify versions may also register the short service name when both sides use Connect to Predefined Network — if `wget http://garage:3900` fails, use the `garage-` form or Path A). + +6. Match credentials and bucket: + +```env +S3_BUCKET=beenvoice-receipts +S3_ACCESS_KEY= +S3_SECRET_KEY= +S3_REGION=garage +``` + +--- + +## Recommended long-term — one Compose stack + +Deploy **[`docker-compose.coolify.yml`](../docker-compose.coolify.yml)** as **one** Coolify **Docker Compose** resource (app + Postgres + Garage). This is the lowest-friction production layout on Coolify. + +1. Coolify → **New Resource** → **Docker Compose** +2. Point at this repo; compose file: **`docker-compose.coolify.yml`** +3. Set env vars from [`.env.example`](../.env.example): `AUTH_SECRET`, `POSTGRES_PASSWORD`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, etc. +4. Assign a domain to the **`app`** service (Coolify fills `SERVICE_URL_APP` / `BETTER_AUTH_URL` automatically). +5. **Do not** override `S3_ENDPOINT` — the compose file sets `S3_ENDPOINT=http://garage:3900` on the shared network. +6. Redeploy. + +Alternative: [`docker-compose.yml`](../docker-compose.yml) works the same way; `docker-compose.coolify.yml` adds Coolify magic vars (`SERVICE_FQDN_APP`) and omits host port bindings for db/Garage. + +### Migrating from Application + external Postgres + Garage (or legacy MinIO) + +| Current | Action | +|---------|--------| +| beenvoice Application | Remove after Compose stack is live | +| Separate Postgres | Dump/restore into stack `db`, or keep external DB and delete the `db` service from the compose file | +| Garage / MinIO compose | Remove after data migrated (rclone) or re-point receipts (new bucket) | +| Env vars | Move `AUTH_SECRET`, Resend, Authentik, etc. to the Compose resource env | + +**Migrating from MinIO:** Garage uses port **3900** (not 9000) and Garage-format access keys (`GK…`). Update `S3_ENDPOINT`, `S3_REGION=garage`, and credentials. Receipt blobs in the old MinIO volume are not auto-migrated. + +--- + +## Compose file reference + +| File | Purpose | +|------|---------| +| [`docker-compose.coolify.yml`](../docker-compose.coolify.yml) | **Recommended** — full stack for one Coolify Compose resource | +| [`docker-compose.yml`](../docker-compose.yml) | Full stack (local/VPS); also valid on Coolify | +| [`docker-compose.coolify-garage.yml`](../docker-compose.coolify-garage.yml) | Garage only; pair with beenvoice Application (Path A or B above) | + +Do **not** add `networks: coolify: external: true` unless you know the exact external network name on your server. Coolify v4 uses **destinations**; network names are often UUID-based. Prefer the UI **Connect to Predefined Network** toggle over hard-coding `coolify` in compose. + +--- + +## Checklist (Application + separate Garage) + +- [ ] Garage stack redeployed with current `docker-compose.coolify-garage.yml` +- [ ] **Path A:** domain on port 3900 + `S3_ENDPOINT` = `SERVICE_URL_GARAGE_3900` + **or Path B:** Connect to Predefined Network on **both** resources + `S3_ENDPOINT=http://garage-:3900` +- [ ] `S3_ENDPOINT` is **not** `http://garage:3900`, **not** `localhost` +- [ ] `S3_ACCESS_KEY` / `S3_SECRET_KEY` match the Garage stack env +- [ ] `S3_BUCKET` exists (Garage `--default-bucket` creates `beenvoice-receipts` on first start) +- [ ] Redeployed beenvoice after env or network changes + +## Verify from the beenvoice container + +```bash +# Shell into beenvoice app container on the Coolify server +docker exec -it sh + +# Path A — public URL (403/404 on root is fine — confirms DNS + TLS) +wget -qO- "https://s3.yourdomain.com" || curl -sf "https://s3.yourdomain.com" + +# Path B — internal host from S3_ENDPOINT +wget -qO- "http://garage-:3900" || curl -sf "http://garage-:3900" +``` + +If this fails with "bad address" or timeout, fix networking / `S3_ENDPOINT` before debugging app code. On first S3 use, the app logs a hint if DNS fails or if `S3_ENDPOINT` still uses bare `garage` in production. diff --git a/apps/web/docs/README.md b/apps/web/docs/README.md new file mode 100644 index 0000000..490be42 --- /dev/null +++ b/apps/web/docs/README.md @@ -0,0 +1,36 @@ +# beenvoice-web documentation + +**Repository:** [git.soconnor.dev/soconnor/beenvoice-web](https://git.soconnor.dev/soconnor/beenvoice-web) + +## Core + +| Document | Description | +|----------|-------------| +| [ARCHITECTURE.md](./ARCHITECTURE.md) | Server stack, tRPC routers, schema, auth, MCP, Docker, mobile API contract | +| [../README.md](../README.md) | Install, scripts, deployment | +| [COOLIFY.md](./COOLIFY.md) | Coolify + Garage networking (`ENOTFOUND garage`) | + +## UI & product guides + +| Document | Description | +|----------|-------------| +| [forms-guide.md](./forms-guide.md) | Form patterns | +| [UI_UNIFORMITY_GUIDE.md](./UI_UNIFORMITY_GUIDE.md) | Visual consistency | +| [breadcrumbs-guide.md](./breadcrumbs-guide.md) | Navigation breadcrumbs | +| [data-table-responsive-guide.md](./data-table-responsive-guide.md) | Responsive tables | +| [data-table-improvements.md](./data-table-improvements.md) | Table enhancements | +| [RESPONSIVE_TABLE_EXAMPLES.md](./RESPONSIVE_TABLE_EXAMPLES.md) | Table examples | +| [email-features.md](./email-features.md) | Email composer / delivery | + +## Mobile + +| Document | Description | +|----------|-------------| +| [../../beenvoice-app/docs/ARCHITECTURE.md](../../beenvoice-app/docs/ARCHITECTURE.md) | Expo app architecture | +| [../../beenvoice-app/README.md](../../beenvoice-app/README.md) | Mobile setup | + +## Workspace + +| Document | Description | +|----------|-------------| +| [../../README.md](../../README.md) | Meta repo layout, full-stack quick start | diff --git a/apps/web/docs/RESPONSIVE_TABLE_EXAMPLES.md b/apps/web/docs/RESPONSIVE_TABLE_EXAMPLES.md new file mode 100644 index 0000000..abdbc4b --- /dev/null +++ b/apps/web/docs/RESPONSIVE_TABLE_EXAMPLES.md @@ -0,0 +1,138 @@ +# Responsive Table Examples + +This document shows how tables adapt across different screen sizes in the beenvoice application. + +## Mobile View (< 640px) + +### Invoices Table +- **Visible**: Invoice number, client name, amount, status, actions +- **Hidden**: Issue date, due date (shown on detail view) +- **Features**: Compact spacing, smaller buttons, simplified pagination + +### Clients Table +- **Visible**: Name with email, actions +- **Hidden**: Phone, address, created date +- **Icon**: Hidden on mobile to save space + +### Businesses Table +- **Visible**: Name with email, actions +- **Hidden**: Phone, address, tax ID, website +- **Icon**: Hidden on mobile to save space + +## Tablet View (640px - 1024px) + +### Invoices Table +- **Added**: Issue date column +- **Still Hidden**: Due date (less critical than issue date) +- **Features**: Search bar expands, column visibility toggle appears + +### Clients Table +- **Added**: Phone column, client icon +- **Still Hidden**: Address, created date +- **Features**: Better spacing, full search functionality + +### Businesses Table +- **Added**: Phone column, business icon +- **Still Hidden**: Address, tax ID +- **Features**: Website links become visible + +## Desktop View (> 1024px) + +### All Tables +- **Full Features**: All columns visible +- **Enhanced**: + - Full pagination controls with page size selector + - Column visibility toggle + - Advanced filters + - Comfortable spacing + - All metadata visible + +## Code Examples + +### Responsive Column Definition +```tsx +// Hide on mobile, show on tablet and up +{ + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + {row.original.phone || "—"} + ), +} + +// Hide on mobile and tablet, show on desktop +{ + id: "address", + header: "Address", + cell: ({ row }) => ( + {formatAddress(row.original)} + ), +} +``` + +### Responsive Cell Content +```tsx +// Icon hidden on mobile +
+
+ +
+
+

{client.name}

+

+ {client.email || "—"} +

+
+
+``` + +### Responsive Actions +```tsx +// Compact action buttons that work on all screen sizes +
+ + +
+``` + +## Filter Bar Behavior + +### Mobile +- Search input takes full width +- Filter dropdowns stack vertically +- Column visibility hidden +- Clear filters button visible when filters active + +### Tablet+ +- Search input limited to max-width +- Filter dropdowns in horizontal row +- Column visibility toggle appears +- All controls in single row + +## Pagination Behavior + +### Mobile +- Simplified page indicator (1/5 format) +- Compact button spacing +- Page size selector with smaller text + +### Desktop +- Full "Page 1 of 5" text +- Comfortable button spacing +- First/Last page buttons visible +- Entries count with detailed information + +## Best Practices + +1. **Priority Content**: Always show the most important data on mobile +2. **Progressive Enhancement**: Add columns as screen size increases +3. **Touch Targets**: Maintain 44px minimum touch targets on mobile +4. **Text Truncation**: Use `truncate` class for long text in narrow columns +5. **Icon Usage**: Hide decorative icons on mobile, keep functional ones +6. **Testing**: Always test at 375px (iPhone SE), 768px (iPad), and 1440px (Desktop) \ No newline at end of file diff --git a/apps/web/docs/UI_UNIFORMITY_GUIDE.md b/apps/web/docs/UI_UNIFORMITY_GUIDE.md new file mode 100644 index 0000000..5194361 --- /dev/null +++ b/apps/web/docs/UI_UNIFORMITY_GUIDE.md @@ -0,0 +1,324 @@ +# UI Uniformity Guide for beenvoice + +## Overview + +This guide documents the unified component system implemented across the beenvoice application to ensure consistent UI/UX patterns. The system follows a hierarchical approach where: + +1. **CSS Variables** (in `globals.css`) define the design tokens +2. **UI Components** (in `components/ui`) consume these variables +3. **Pages** use components with minimal additional styling + +## Design System Principles + +### 1. Variable-Based Theming +All colors, spacing, and other design tokens are defined as CSS variables in `globals.css`: +- Brand colors: `--brand-primary`, `--brand-secondary` +- Status colors: `--status-success`, `--status-warning`, `--status-error`, `--status-info` +- Semantic colors: `--background`, `--foreground`, `--muted`, etc. + +### 2. Component Composition +Complex UI patterns are built from smaller, reusable components rather than duplicating code. + +### 3. Minimal Page-Level Styling +Pages should primarily compose pre-built components and avoid custom Tailwind classes where possible. + +## Core Unified Components + +### Page Layout Components + +#### `PageContent` +Wraps page content with consistent spacing: +```tsx + + {/* Page sections */} + +``` + +#### `PageSection` +Groups related content with optional title and actions: +```tsx +Action} +> + {/* Section content */} + +``` + +#### `PageGrid` +Responsive grid layout with preset column options: +```tsx + + {/* Grid items */} + +``` + +### Data Display Components + +#### `DataTable` +Unified table component using @tanstack/react-table with floating card design: +```tsx +import { ColumnDef } from "@tanstack/react-table"; +import { DataTable, DataTableColumnHeader } from "~/components/ui/data-table"; +import { PageSection } from "~/components/ui/page-layout"; + +const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const name = row.getValue("name") as string; + return
{name}
; + } + }, + { + id: "actions", + cell: ({ row }) => { + const item = row.original; + return ( + + ); + } + } +]; + +const filterableColumns = [ + { + id: "status", + title: "Status", + options: [ + { label: "Active", value: "active" }, + { label: "Inactive", value: "inactive" } + ] + } +]; + +// Wrap in PageSection for title/description + + + +``` + +Features: +- **Floating Card Design**: Three separate cards for filter bar, table content, and pagination +- **Filter Bar Card**: Minimal padding (p-3) with global search and column filters +- **Table Content Card**: Clean borders with overflow handling +- **Pagination Card**: Compact controls with page size selector +- **Responsive Design**: Mobile-optimized with hidden columns on smaller screens +- **Tight Appearance**: Compact spacing with smaller action buttons +- **Sorting**: Visual indicators with proper arrow directions +- **Column Visibility**: Toggle columns (hidden on mobile) +- **Dark Mode**: Consistent styling across light/dark themes +- **Loading States**: DataTableSkeleton component with matching card structure + +#### `StatsCard` +Displays statistics with consistent styling: +```tsx + +``` + +#### `QuickActionCard` +Interactive cards for navigation or actions: +```tsx + + +
+ + +``` + +### Feedback Components + +#### `EmptyState` +Consistent empty state displays: +```tsx +} + title="No invoices yet" + description="Create your first invoice to get started" + action={} +/> +``` + +## Component Variants + +### Color Variants +Most components support these variants: +- `default` - Uses default theme colors +- `success` - Green color scheme for positive states +- `warning` - Orange/amber for warnings +- `error` - Red for errors or destructive actions +- `info` - Blue for informational content + +### Size Variants +- `sm` - Small size +- `default` - Normal size +- `lg` - Large size + +## Usage Examples + +### Standard Page Structure +```tsx +export default function ExamplePage() { + return ( + + + + + + + + + + + + + + + + ); +} +``` + +### Consistent Button Usage +```tsx +// Primary actions + + +// Secondary actions + + +// Destructive actions + + +// Icon-only actions + +``` + +## Styling Guidelines + +### Do's +- ✅ Use predefined color variables from globals.css +- ✅ Compose existing UI components +- ✅ Use semantic variant names (success, error, etc.) +- ✅ Follow the established spacing patterns +- ✅ Use the PageLayout components for structure + +### Don'ts +- ❌ Add custom colors directly in components +- ❌ Create one-off table or card implementations +- ❌ Override component styles with important flags +- ❌ Use arbitrary spacing values +- ❌ Mix different UI patterns on the same page + +## Migration Checklist + +When updating a page to use the unified system: + +1. Replace custom tables with `DataTable` using @tanstack/react-table ColumnDef +2. Replace statistics displays with `StatsCard` +3. Replace action cards with `QuickActionCard` +4. Wrap content in `PageContent` and `PageSection` +5. Use `PageGrid` for responsive layouts +6. Replace custom empty states with `EmptyState` +7. Update buttons to use the `brand` variant for primary actions +8. Remove page-specific color classes +9. Use `DataTableColumnHeader` for sortable column headers +10. Use `DataTableSkeleton` for loading states + +## Color System Reference + +### Brand Colors +- Primary: Green (`#16a34a` / `oklch(0.646 0.222 164.25)`) +- Secondary: Teal/cyan shades +- Gradients: Use `bg-brand-gradient` class + +### Status Colors +- Success: Green shades +- Warning: Amber/orange shades +- Error: Red shades +- Info: Blue shades + +### Semantic Colors +- Background: White/dark gray +- Foreground: Black/white text +- Muted: Gray shades for secondary content +- Border: Light gray borders + +## Component Documentation + +For detailed component APIs and props, refer to: +- `/src/components/ui/data-table.tsx` - TanStack Table-based data table with sorting, filtering, and pagination +- `/src/components/ui/stats-card.tsx` - Statistics display cards +- `/src/components/ui/quick-action-card.tsx` - Interactive action cards +- `/src/components/ui/page-layout.tsx` - Page structure components + +### DataTable Props +- `columns`: ColumnDef array from @tanstack/react-table +- `data`: Array of data to display +- `searchPlaceholder?`: Placeholder text for search input +- `showColumnVisibility?`: Show/hide column visibility toggle (default: true) +- `showPagination?`: Show/hide pagination controls (default: true) +- `showSearch?`: Show/hide search input (default: true) +- `pageSize?`: Number of items per page (default: 10) +- `filterableColumns?`: Array of column filters with options + +Note: `title` and `description` should be provided via the wrapping `PageSection` component for consistent spacing and typography. + +### Responsive Table Guidelines +- Use `hidden sm:flex` classes for icons in table cells +- Use `hidden md:inline` for less important columns on mobile +- Use `min-w-0` and `truncate` for text that might overflow +- Keep action buttons small with `h-8 w-8 p-0` sizing +- Test tables at all breakpoints (mobile, tablet, desktop) + +## Future Considerations + +1. **Form Components**: Create unified form field components +2. **Modal Patterns**: Standardize modal and dialog usage +3. **Loading States**: Create consistent skeleton loaders +4. **Animation**: Define standard transition patterns +5. **Icons**: Establish icon usage guidelines + +## Maintenance + +To maintain UI consistency: +1. Always check for existing components before creating new ones +2. Update this guide when adding new unified components +3. Review PRs for adherence to these patterns +4. Refactor pages that deviate from the system \ No newline at end of file diff --git a/apps/web/docs/breadcrumbs-guide.md b/apps/web/docs/breadcrumbs-guide.md new file mode 100644 index 0000000..63dfbc3 --- /dev/null +++ b/apps/web/docs/breadcrumbs-guide.md @@ -0,0 +1,198 @@ +# Dynamic Breadcrumbs Guide + +## Overview + +The breadcrumb system in beenvoice automatically generates navigation trails based on the current URL path. It features intelligent pluralization, proper capitalization, and dynamic resource name fetching. + +## Key Features + +### 1. Automatic Pluralization + +The breadcrumb system intelligently handles singular and plural forms: + +- **List pages** (e.g., `/dashboard/businesses`) → "Businesses" +- **Detail pages** (e.g., `/dashboard/businesses/[id]`) → "Business" +- **New pages** (e.g., `/dashboard/businesses/new`) → "Business" (singular context) + +### 2. Smart Capitalization + +All route segments are automatically capitalized: +- `businesses` → "Businesses" +- `clients` → "Clients" +- `invoices` → "Invoices" + +### 3. Dynamic Resource Names + +Instead of showing UUIDs, breadcrumbs fetch and display actual resource names: +- `/dashboard/clients/123e4567-e89b-12d3-a456-426614174000` → "Dashboard / Clients / John Doe" +- `/dashboard/invoices/987fcdeb-51a2-43f1-b321-123456789abc` → "Dashboard / Invoices / INV-2024-001" + +### 4. Context-Aware Labels + +Special pages are handled intelligently: +- **Edit pages**: Show the resource name instead of "Edit" as the last breadcrumb +- **New pages**: Show "New" as the last breadcrumb +- **Import/Export pages**: Show appropriate action labels + +## Implementation Details + +### Pluralization Rules + +The system uses a comprehensive pluralization utility (`src/lib/pluralize.ts`) that handles: + +```typescript +// Common business terms +business → businesses +client → clients +invoice → invoices +category → categories +company → companies + +// General rules +- Words ending in 's', 'ss', 'sh', 'ch', 'x', 'z' → add 'es' +- Words ending in consonant + 'y' → change to 'ies' +- Words ending in 'f' or 'fe' → change to 'ves' +- Default → add 's' +``` + +### Resource Fetching + +The breadcrumbs automatically detect resource IDs and fetch the appropriate data: + +```typescript +// Detects UUID patterns in the URL +const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + +// Fetches data based on resource type +- Clients: Shows client name +- Invoices: Shows invoice number or formatted date +- Businesses: Shows business name +``` + +### Loading States + +While fetching resource data, breadcrumbs show loading skeletons: +```tsx + +``` + +## Usage Examples + +### Basic List Page +**URL**: `/dashboard/clients` +**Breadcrumbs**: Dashboard / Clients + +### Resource Detail Page +**URL**: `/dashboard/clients/550e8400-e29b-41d4-a716-446655440000` +**Breadcrumbs**: Dashboard / Clients / Jane Smith + +### Resource Edit Page +**URL**: `/dashboard/businesses/550e8400-e29b-41d4-a716-446655440000/edit` +**Breadcrumbs**: Dashboard / Businesses / Acme Corp +*(Note: "Edit" is hidden when showing the resource name)* + +### New Resource Page +**URL**: `/dashboard/invoices/new` +**Breadcrumbs**: Dashboard / Invoices / New + +### Nested Resources +**URL**: `/dashboard/clients/550e8400-e29b-41d4-a716-446655440000/invoices` +**Breadcrumbs**: Dashboard / Clients / John Doe / Invoices + +## Customization + +### Adding New Resource Types + +To add a new resource type, update the pluralization rules: + +```typescript +// In src/lib/pluralize.ts +const PLURALIZATION_RULES = { + // ... existing rules + product: { singular: "Product", plural: "Products" }, + service: { singular: "Service", plural: "Services" }, +}; +``` + +### Custom Resource Labels + +For resources that need custom display logic, add to the breadcrumb component: + +```typescript +// For invoices, show invoice number instead of ID +if (prevSegment === "invoices") { + label = invoice.invoiceNumber || format(new Date(invoice.issueDate), "MMM dd, yyyy"); +} +``` + +### Special Segments + +Add new special segments to the `SPECIAL_SEGMENTS` object: + +```typescript +const SPECIAL_SEGMENTS = { + new: "New", + edit: "Edit", + import: "Import", + export: "Export", + duplicate: "Duplicate", + archive: "Archive", +}; +``` + +## Best Practices + +1. **Consistent Naming**: Use consistent URL patterns across your app + - List pages: `/dashboard/[resource]` + - Detail pages: `/dashboard/[resource]/[id]` + - Actions: `/dashboard/[resource]/[id]/[action]` + +2. **Resource Fetching**: Only fetch data when needed + - Check resource type before enabling queries + - Use proper loading states + +3. **Error Handling**: Handle cases where resources don't exist + - Show fallback text or maintain UUID display + - Don't break the breadcrumb trail + +4. **Performance**: Breadcrumb queries are lightweight + - Only fetch minimal data (id, name) + - Use React Query caching effectively + +## API Integration + +The breadcrumb component integrates with tRPC routers: + +```typescript +// Each resource router should have a getById method +getById: protectedProcedure + .input(z.object({ id: z.string() })) + .query(async ({ ctx, input }) => { + // Return resource with at least id and name/title + }) +``` + +## Accessibility + +- Breadcrumbs use semantic HTML with proper ARIA labels +- Each segment is a link except the current page +- Proper keyboard navigation support +- Screen reader friendly with role="navigation" + +## Responsive Design + +- Breadcrumbs wrap on smaller screens +- Font sizes adjust: `text-sm sm:text-base` +- Separators scale appropriately +- Loading skeletons match text size + +## Migration from Static Breadcrumbs + +If migrating from hardcoded breadcrumbs: + +1. Remove static breadcrumb definitions +2. Ensure URLs follow consistent patterns +3. Add getById methods to resource routers +4. Update imports to use `DashboardBreadcrumbs` + +The dynamic system will automatically generate appropriate breadcrumbs based on the URL structure. \ No newline at end of file diff --git a/apps/web/docs/data-table-improvements.md b/apps/web/docs/data-table-improvements.md new file mode 100644 index 0000000..41d6d4f --- /dev/null +++ b/apps/web/docs/data-table-improvements.md @@ -0,0 +1,154 @@ +# Data Table Improvements Summary + +## Overview + +The data table component has been significantly improved to address padding, scaling, and responsiveness issues. The tables now provide a cleaner, more compact appearance while maintaining excellent usability across all device sizes. + +## Key Improvements Made + +### 1. Tighter, More Consistent Padding + +**Before:** +- Inconsistent padding across different table sections +- Excessive vertical padding making tables feel loose +- Cards had default py-6 padding that was too spacious + +**After:** +- Table cells: `py-1.5` (mobile) / `py-2` (desktop) - reduced from `py-2.5` / `py-3` +- Table headers: `h-9` (mobile) / `h-10` (desktop) - reduced from `h-10` / `h-12` +- Filter/pagination cards: `py-2` with `px-3` horizontal padding +- Table card: `p-0` to wrap content tightly + +### 2. Improved Responsive Column Handling + +**Before:** +```tsx +// Cells would hide but headers remained visible +cell: ({ row }) => ( + {row.original.phone} +), +``` + +**After:** +```tsx +// Both header and cell hide together +cell: ({ row }) => row.original.phone || "—", +meta: { + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", +}, +``` + +### 3. Better Small Card Appearance + +- Filter card: Compact `py-2` padding with proper horizontal spacing +- Pagination card: Matching `py-2` padding for consistency +- Content aligned properly within smaller card boundaries +- Removed excessive gaps between elements +- Search box now has consistent padding without extra bottom spacing on mobile + +### 4. Responsive Font Sizing + +- Base text: `text-xs` on mobile, `text-sm` on desktop +- Consistent scaling across all table elements +- Better readability on small screens without wasting space + +## Visual Comparison + +### Table Density +- **Before**: ~60px per row with excessive padding +- **After**: ~40px per row with comfortable but efficient spacing + +### Card Heights +- **Filter Card**: Reduced from ~80px to ~56px +- **Pagination Card**: Reduced from ~72px to ~48px +- **Table Card**: Now wraps content exactly with no extra space +- **Pagination Layout**: Entry count and pagination controls now stay on the same line on mobile + +## Implementation Examples + +### Responsive Column Definition +```tsx +const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.name, + // Always visible + }, + { + accessorKey: "email", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.email, + meta: { + // Hidden on mobile, visible on tablets and up + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", + }, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => formatDate(row.getValue("createdAt")), + meta: { + // Only visible on large screens + headerClassName: "hidden lg:table-cell", + cellClassName: "hidden lg:table-cell", + }, + }, +]; +``` + +### Page Header Actions +Page headers now properly position action buttons to the right on all screen sizes: + +```tsx + + + +``` + +### Breakpoint Reference +- `sm`: 640px and up +- `md`: 768px and up +- `lg`: 1024px and up +- `xl`: 1280px and up + +## Benefits + +1. **More Data Visible**: Tighter spacing allows more rows to be visible without scrolling +2. **Professional Appearance**: Clean, compact design suitable for business applications +3. **Better Mobile UX**: Properly hidden columns prevent layout breaking +4. **Consistent Styling**: All table instances now follow the same spacing rules +5. **Performance**: CSS-only solution with no JavaScript overhead +6. **Improved Mobile Layout**: Pagination controls stay inline with entry count on mobile +7. **Consistent Header Actions**: Action buttons properly positioned to the right + +## Migration Checklist + +- [x] Update column definitions to use `meta` properties +- [x] Remove inline responsive classes from cell content +- [x] Test on actual mobile devices +- [x] Verify touch targets remain accessible (min 44x44px) +- [x] Check that critical data remains visible on small screens + +## Best Practices Going Forward + +1. **Column Priority**: Always keep the most important 2-3 columns visible on mobile +2. **Content Density**: Use the tighter spacing for data tables, looser spacing for content lists +3. **Responsive Testing**: Test at 320px, 768px, and 1024px minimum +4. **Accessibility**: Ensure interactive elements maintain proper touch targets despite tighter spacing \ No newline at end of file diff --git a/apps/web/docs/data-table-responsive-guide.md b/apps/web/docs/data-table-responsive-guide.md new file mode 100644 index 0000000..b8bc32f --- /dev/null +++ b/apps/web/docs/data-table-responsive-guide.md @@ -0,0 +1,246 @@ +# Data Table Responsive Design Guide + +## Overview + +The data table component has been updated to provide better responsive behavior, consistent padding, and proper scaling across different screen sizes. + +## Key Improvements + +### 1. Consistent Padding +- Uniform padding across all table elements +- Responsive padding that scales with screen size +- Cards now have consistent spacing (p-3 on mobile, p-4 on desktop) + +### 2. Proper Responsive Column Hiding +- Columns now properly hide both headers and cells on smaller screens +- Uses `meta` properties for clean column visibility control +- No more orphaned headers on mobile devices + +### 3. Better Scaling +- Font sizes adapt to screen size (text-xs on mobile, text-sm on desktop) +- Button sizes and spacing adjust appropriately +- Pagination controls are optimized for touch devices + +## Using Responsive Columns + +### Basic Column Definition + +```tsx +const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.name, + // Always visible on all screen sizes + }, + { + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.phone || "—", + meta: { + // Hidden on mobile, visible on md screens and up + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", + }, + }, + { + accessorKey: "address", + header: "Address", + cell: ({ row }) => formatAddress(row.original), + meta: { + // Hidden on mobile and tablet, visible on lg screens and up + headerClassName: "hidden lg:table-cell", + cellClassName: "hidden lg:table-cell", + }, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => formatDate(row.getValue("createdAt")), + meta: { + // Only visible on xl screens and up + headerClassName: "hidden xl:table-cell", + cellClassName: "hidden xl:table-cell", + }, + }, +]; +``` + +### Responsive Breakpoints + +- **Always visible**: No meta properties needed +- **md and up** (768px+): `hidden md:table-cell` +- **lg and up** (1024px+): `hidden lg:table-cell` +- **xl and up** (1280px+): `hidden xl:table-cell` + +## Complex Cell Content + +For cells with complex content that should partially hide on mobile: + +```tsx +{ + accessorKey: "client", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const client = row.original; + return ( +
+ {/* Icon hidden on mobile, shown on sm screens */} +
+ +
+
+

{client.name}

+ {/* Secondary info can be hidden on very small screens if needed */} +

+ {client.email || "—"} +

+
+
+ ); + }, +} +``` + +## Best Practices + +### 1. Priority-Based Column Hiding +- Always show the most important columns (e.g., name, status, primary action) +- Hide supplementary information first (e.g., dates, secondary details) +- Consider hiding decorative elements (icons) on mobile while keeping text + +### 2. Mobile-First Design +- Ensure at least 2-3 columns are visible on mobile +- Test on actual devices, not just browser dev tools +- Consider the minimum viable information for each row + +### 3. Touch-Friendly Actions +- Action buttons should be at least 44x44px on mobile +- Use appropriate spacing between interactive elements +- Consider grouping actions in a dropdown on mobile + +### 4. Performance +- The responsive system uses CSS classes, so there's no JavaScript overhead +- Column visibility is handled by Tailwind's responsive utilities +- No re-renders needed when resizing + +## Migration Guide + +If you have existing data tables, update them as follows: + +### Before: +```tsx +{ + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + {row.original.phone || "—"} + ), +} +``` + +### After: +```tsx +{ + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.phone || "—", + meta: { + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", + }, +} +``` + +## Common Patterns + +### Status Columns +Always visible, use color and icons to convey information efficiently: + +```tsx +{ + accessorKey: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => , +} +``` + +### Date Columns +Often hidden on mobile, show relative dates when space is limited: + +```tsx +{ + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const date = row.getValue("createdAt") as Date; + return ( + <> + {/* Full date on larger screens */} + {formatDate(date)} + {/* Relative date on mobile */} + {formatRelativeDate(date)} + + ); + }, +} +``` + +### Action Columns +Keep actions accessible but space-efficient: + +```tsx +{ + id: "actions", + cell: ({ row }) => { + const item = row.original; + return ( +
+ {/* Show individual buttons on larger screens */} +
+ + +
+ {/* Dropdown menu on mobile */} +
+ +
+
+ ); + }, +} +``` + +## Testing Checklist + +- [ ] Table is readable on 320px wide screens +- [ ] Headers and cells align properly at all breakpoints +- [ ] Touch targets are at least 44x44px on mobile +- [ ] Horizontal scrolling works smoothly when needed +- [ ] Critical information is always visible +- [ ] Loading states work correctly +- [ ] Empty states are responsive +- [ ] Pagination controls are touch-friendly + +## Accessibility Notes + +- Hidden columns are properly hidden from screen readers +- Table remains navigable with keyboard at all screen sizes +- Sort controls are accessible on mobile +- Focus indicators are visible on all interactive elements \ No newline at end of file diff --git a/apps/web/docs/email-features.md b/apps/web/docs/email-features.md new file mode 100644 index 0000000..785ee79 --- /dev/null +++ b/apps/web/docs/email-features.md @@ -0,0 +1,281 @@ +# Enhanced Email Sending Features + +## Overview + +The beenvoice application now includes a comprehensive email sending system with preview, rich text editing, and confirmation features. This enhancement provides a professional email experience for sending invoices to clients. + +## Features + +### 🎨 Rich Text Email Composer +- **Tiptap Editor Integration**: Professional rich text editing with formatting options +- **Text Formatting**: Bold, italic, strikethrough, and color options +- **Text Alignment**: Left, center, and right alignment +- **Lists**: Bullet points and numbered lists +- **Color Picker**: Choose from a variety of text colors +- **Real-time Preview**: See changes as you type + +### 👁️ Email Preview +- **Visual Preview**: See exactly how your email will appear to recipients +- **Invoice Summary**: Displays key invoice details (number, date, amount) +- **Attachment Notice**: Shows PDF attachment information +- **Professional Styling**: Clean, branded email template +- **Responsive Design**: Optimized for all screen sizes with proper text wrapping +- **Mobile-First**: Touch-friendly interface with proper spacing + +### ✅ Send Confirmation +- **Two-Step Process**: Compose ↔ Preview with Send Action +- **Action-Based Sending**: Send button available from sidebar and floating action bar +- **Status Updates**: Automatic status change from draft to sent +- **Error Handling**: Clear error messages with specific guidance +- **SSR Compatible**: Proper hydration handling for server-side rendering + +### 📄 Smart Templates +- **Auto-Generated Content**: Professional email templates with proper paragraph spacing +- **Time-Based Greetings**: Morning, afternoon, or evening greetings +- **Invoice Details**: Automatically includes invoice number, date, and amount +- **Business Branding**: Uses your business name and contact information +- **Immediate Loading**: Content appears instantly in the editor without requiring tab switching + +## Components + +### EmailComposer +**Location**: `src/components/forms/email-composer.tsx` + +A rich text editor component for composing emails with formatting options. + +**Props**: +- `subject`: Email subject line +- `onSubjectChange`: Callback for subject changes +- `content`: Email content (HTML) +- `onContentChange`: Callback for content changes +- `fromEmail`: Sender email address +- `toEmail`: Recipient email address + +### EmailPreview +**Location**: `src/components/forms/email-preview.tsx` + +Displays a visual preview of how the email will appear to recipients. + +**Props**: +- `subject`: Email subject line +- `fromEmail`: Sender email address +- `toEmail`: Recipient email address +- `content`: Email content (HTML) +- `invoice`: Invoice data for summary display + +### SendEmailDialog +**Location**: `src/components/forms/send-email-dialog.tsx` + +Main dialog component that combines composition, preview, and confirmation. + +**Props**: +- `invoiceId`: ID of the invoice to send +- `trigger`: React element that opens the dialog +- `invoice`: Invoice data +- `onEmailSent`: Callback when email is successfully sent + +### EnhancedSendInvoiceButton +**Location**: `src/components/forms/enhanced-send-invoice-button.tsx` + +Enhanced button component that opens the email dialog. + +**Props**: +- `invoiceId`: ID of the invoice to send +- `variant`: Button style variant +- `className`: Additional CSS classes +- `showResend`: Whether to show "Resend" text +- `size`: Button size + +## API Enhancements + +### Enhanced Email Router +**Location**: `src/server/api/routers/email.ts` + +The email API has been enhanced to support custom content and HTML emails. + +**New Parameters**: +- `customSubject`: Optional custom email subject +- `customContent`: Optional custom email content (HTML) +- `useHtml`: Boolean flag to send HTML email + +**Features**: +- HTML email support with plain text fallback +- Custom subject lines +- Rich HTML content +- Automatic PDF attachment +- BCC to business email +- Comprehensive error handling + +## Usage Examples + +### Basic Usage +```tsx +import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button"; + +// Replace existing send buttons + +``` + +### Custom Dialog +```tsx +import { SendEmailDialog } from "~/components/forms/send-email-dialog"; + +Send Custom Email} + onEmailSent={() => console.log("Email sent!")} +/> +``` + +### Standalone Components +```tsx +import { EmailComposer } from "~/components/forms/email-composer"; +import { EmailPreview } from "~/components/forms/email-preview"; + +// Use individual components for custom implementations + + + +``` + +## Technical Details + +### Dependencies +- **@tiptap/react**: Rich text editor framework +- **@tiptap/starter-kit**: Basic editor functionality +- **@tiptap/extension-text-style**: Text styling support +- **@tiptap/extension-color**: Color picker support +- **@tiptap/extension-text-align**: Text alignment options + +### Email Templates +The system generates professional HTML email templates with: +- Responsive design +- Brand colors (green theme) +- Invoice summary cards +- Proper typography +- Attachment indicators +- Footer branding + +### Error Handling +Comprehensive error handling for: +- Invalid email addresses +- Missing client information +- Resend API issues +- Network connectivity problems +- Domain verification issues +- Rate limiting + +## Usage in Application + +The enhanced email functionality is integrated throughout the application: +- Invoice view pages with enhanced send buttons +- Full-page email composition interface +- Professional email templates with invoice integration +- Comprehensive preview and confirmation workflow + +## Migration Guide + +### From Basic Send Button +Replace existing `SendInvoiceButton` components with `EnhancedSendInvoiceButton`: + +```tsx +// Before +import { SendInvoiceButton } from "../_components/send-invoice-button"; + + +// After +import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button"; + +``` + +### API Compatibility +The enhanced email API is backward compatible with existing implementations. New features are opt-in through additional parameters. + +## Security Considerations + +- **Input Sanitization**: All user input is validated and sanitized +- **Email Validation**: Comprehensive email format validation +- **Rate Limiting**: Built-in protection against spam +- **Domain Verification**: Resend domain verification required +- **Authentication**: All email operations require valid authentication + +## Performance + +- **SSR Optimization**: Proper server-side rendering with hydration safeguards +- **Efficient Loading**: Content initializes immediately without requiring user interaction +- **Optimized Rendering**: Efficient React component updates with proper state management +- **Caching**: Proper query caching for invoice data +- **Error Boundaries**: Graceful error handling without crashes +- **Responsive Design**: Optimized layouts for all screen sizes with text overflow prevention + +## Navigation + +### Send Email Page +Access the email interface by clicking "Send Invoice" on any invoice: +- `/dashboard/invoices/[id]/send` - Full-page email composition +- Two-tab interface: Compose ↔ Preview +- Send action available from sidebar and floating action bar +- Fully responsive design with proper text wrapping and overflow handling +- Professional layout with sidebar containing: + - Invoice summary (number, client, date, status) + - Email details (from, to, subject, attachment info) + - Context-aware action buttons +- Auto-filled message with proper HTML formatting and paragraph spacing +- Immediate content loading without requiring tab navigation + +## Fixes and Improvements + +Recent fixes and enhancements: +- **SSR Compatibility**: Fixed Tiptap hydration issues for reliable server-side rendering +- **Content Loading**: Improved email content initialization for immediate display +- **Responsive Design**: Enhanced text wrapping and overflow handling for all screen sizes +- **UI/UX**: Removed confirmation tab in favor of action-based sending approach +- **Performance**: Optimized state management for faster content loading + +## Future Enhancements + +Planned improvements include: +- Email templates library +- Scheduling email delivery +- Email tracking and read receipts +- Bulk email sending +- Custom email signatures +- Integration with email marketing tools + +## Support + +For issues or questions related to the email system: +1. Check the console for error messages +2. Verify Resend API configuration +3. Ensure client email addresses are valid +4. Review domain verification status +5. Check network connectivity + +## Changelog + +### Version 1.0.0 +- Initial release of enhanced email system +- Rich text editor integration +- Email preview functionality +- Send confirmation workflow +- HTML email support +- Professional templates +- Demo page implementation \ No newline at end of file diff --git a/apps/web/docs/forms-guide.md b/apps/web/docs/forms-guide.md new file mode 100644 index 0000000..8a0a6bd --- /dev/null +++ b/apps/web/docs/forms-guide.md @@ -0,0 +1,279 @@ +# Forms Improvement Guide + +## Overview + +The business and client creation/editing forms have been significantly improved with better organization, shared components, enhanced validation, and improved user experience. + +## Key Improvements + +### 1. Shared Components & Utilities + +#### Address Form Component (`src/components/ui/address-form.tsx`) +A reusable address form component that handles: +- Country-aware formatting (US ZIP codes, Canadian postal codes) +- State dropdown for US addresses, text input for other countries +- Popular countries listed first in country dropdown +- Automatic field adjustments based on country selection + +```tsx + +``` + +#### Form Constants & Utilities (`src/lib/form-constants.ts`) +Centralized location for: +- US states list with proper formatting +- All countries with ISO codes +- Popular countries for quick selection +- Format functions for phone, postal codes, tax IDs, and URLs +- Validation utilities and messages + +### 2. Enhanced Form Validation + +#### Real-time Validation +- Errors clear as soon as user starts typing +- Field-specific validation messages +- Visual feedback with red borders on invalid fields + +#### Smart Validation Rules +- Email: Proper email format checking +- Phone: US phone number format validation +- Address: Required fields only if any address field is filled +- URL: Automatic https:// prefix addition + +```typescript +// Example validation +if (formData.email && !isValidEmail(formData.email)) { + newErrors.email = VALIDATION_MESSAGES.email; +} +``` + +### 3. Better Form Organization + +#### Card-based Sections +Forms are now organized into logical sections using cards: +- **Basic Information**: Core fields like name, tax ID +- **Contact Information**: Email, phone, website +- **Address**: Complete address form with smart country handling +- **Settings**: Business-specific settings like default business flag + +#### Consistent Layout +- Maximum width container for better readability +- Responsive grid layouts that stack on mobile +- Proper spacing between sections +- Clear visual hierarchy + +### 4. Improved User Experience + +#### Loading States +- Skeleton loader while fetching data in edit mode +- Disabled form fields during submission +- Loading spinner in submit button + +#### Unsaved Changes Warning +```typescript +const handleCancel = () => { + if (isDirty) { + const confirmed = window.confirm( + "You have unsaved changes. Are you sure you want to leave?" + ); + if (!confirmed) return; + } + router.push("/dashboard/businesses"); +}; +``` + +#### Smart Field Formatting +- Phone numbers: Auto-format as (555) 123-4567 +- Tax ID: Auto-format as 12-3456789 +- Postal codes: Format based on country (US vs Canadian) +- Website URLs: Auto-add https:// if missing + +### 5. Responsive Design + +#### Mobile Optimizations +- Form sections stack vertically on small screens +- Touch-friendly input sizes +- Proper button positioning +- Readable font sizes + +#### Desktop Enhancements +- Two-column layouts for related fields +- Optimal reading width +- Side-by-side form actions + +### 6. Code Reusability + +#### Shared Between Business & Client Forms +- Address form component +- Validation logic +- Format functions +- Constants (states, countries) +- Error handling patterns + +#### TypeScript Interfaces +```typescript +interface FormData { + name: string; + email: string; + phone: string; + // ... other fields +} + +interface FormErrors { + name?: string; + email?: string; + // ... validation errors +} +``` + +## Usage Examples + +### Basic Form Implementation +```tsx +export function BusinessForm({ businessId, mode }: BusinessFormProps) { + const [formData, setFormData] = useState(initialFormData); + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isDirty, setIsDirty] = useState(false); + + // Handle input changes + const handleInputChange = (field: string, value: string | boolean) => { + setFormData((prev) => ({ ...prev, [field]: value })); + setIsDirty(true); + + // Clear error when user types + if (errors[field as keyof FormErrors]) { + setErrors((prev) => ({ ...prev, [field]: undefined })); + } + }; + + // Validate and submit + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) { + toast.error("Please correct the errors in the form"); + return; + } + + // Submit logic... + }; +} +``` + +### Field with Icon and Validation +```tsx +
+ +
+ + handleInputChange("email", e.target.value)} + placeholder={PLACEHOLDERS.email} + className={`pl-10 ${errors.email ? "border-destructive" : ""}`} + disabled={isSubmitting} + /> +
+ {errors.email && ( +

{errors.email}

+ )} +
+``` + +## Best Practices + +### 1. Form State Management +- Use controlled components for all inputs +- Track dirty state for unsaved changes warnings +- Clear errors when user corrects them +- Disable form during submission + +### 2. Validation Strategy +- Validate on submit, not on blur (less annoying) +- Clear errors immediately when user starts fixing them +- Show field-level errors below each input +- Use consistent error message format + +### 3. Accessibility +- Proper label associations with htmlFor +- Required field indicators +- Error messages linked to fields +- Keyboard navigation support +- Focus management + +### 4. Performance +- Memoize expensive computations +- Use debouncing for format functions if needed +- Lazy load country lists +- Optimize re-renders with proper state management + +## Migration Guide + +### From Old Forms +1. Replace inline state/country arrays with imported constants +2. Use `AddressForm` component instead of individual address fields +3. Apply format functions from `form-constants.ts` +4. Update validation to use shared utilities +5. Wrap sections in Card components +6. Add loading and dirty state tracking + +### Example Migration +```tsx +// Before +const US_STATES = [ + { value: "AL", label: "Alabama" }, + // ... duplicated in each form +]; + +// After +import { US_STATES, formatPhoneNumber } from "~/lib/form-constants"; +import { AddressForm } from "~/components/ui/address-form"; +``` + +## Future Enhancements + +### Planned Improvements +1. **Field-level permissions**: Disable fields based on user role +2. **Auto-save**: Save draft as user types +3. **Multi-step forms**: Break long forms into steps +4. **Conditional fields**: Show/hide fields based on other values +5. **Bulk operations**: Create multiple records at once +6. **Import from templates**: Pre-fill common business types + +### Extensibility +The form system is designed to be easily extended: +- Add new format functions to `form-constants.ts` +- Create additional shared form components +- Extend validation rules as needed +- Add new field types with consistent patterns + +## Troubleshooting + +### Common Issues + +1. **Validation not working**: Ensure field names match FormErrors interface +2. **Format function not applying**: Check that onChange uses the format function +3. **Country dropdown not searching**: Verify SearchableSelect has search enabled +4. **Address validation failing**: Check if country field affects validation rules + +### Debug Tips +- Use React DevTools to inspect form state +- Check console for validation errors +- Verify API responses match expected format +- Test with different country selections \ No newline at end of file diff --git a/apps/web/drizzle.config.ts b/apps/web/drizzle.config.ts new file mode 100644 index 0000000..a286a87 --- /dev/null +++ b/apps/web/drizzle.config.ts @@ -0,0 +1,23 @@ +import type { Config } from "drizzle-kit"; +import * as dotenv from "dotenv"; +// Load .env.local if it exists +dotenv.config({ path: ".env.local" }); +// Load .env if it exists (fallback) +dotenv.config({ path: ".env" }); + +// Use a relative import; path alias "~" may not resolve in CLI context +// import { env } from "./src/env.js"; + +if (!process.env.DATABASE_URL) { + throw new Error("DATABASE_URL is not set"); +} + +export default { + schema: "./src/server/db/schema.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL, + }, + tablesFilter: ["beenvoice_*"], +} satisfies Config; diff --git a/apps/web/drizzle/0000_glossy_magneto.sql b/apps/web/drizzle/0000_glossy_magneto.sql new file mode 100644 index 0000000..9921a06 --- /dev/null +++ b/apps/web/drizzle/0000_glossy_magneto.sql @@ -0,0 +1,166 @@ +CREATE TABLE "beenvoice_account" ( + "id" text PRIMARY KEY NOT NULL, + "userId" varchar(255) NOT NULL, + "accountId" varchar(255) NOT NULL, + "providerId" varchar(255) NOT NULL, + "accessToken" text, + "refreshToken" text, + "accessTokenExpiresAt" timestamp, + "refreshTokenExpiresAt" timestamp, + "scope" varchar(255), + "idToken" text, + "password" text, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "beenvoice_business" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "nickname" varchar(255), + "email" varchar(255), + "phone" varchar(50), + "addressLine1" varchar(255), + "addressLine2" varchar(255), + "city" varchar(100), + "state" varchar(50), + "postalCode" varchar(20), + "country" varchar(100), + "website" varchar(255), + "taxId" varchar(100), + "logoUrl" varchar(500), + "isDefault" boolean DEFAULT false, + "resendApiKey" varchar(255), + "resendDomain" varchar(255), + "emailFromName" varchar(255), + "createdById" varchar(255) NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp +); +--> statement-breakpoint +CREATE TABLE "beenvoice_client" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "email" varchar(255), + "phone" varchar(50), + "addressLine1" varchar(255), + "addressLine2" varchar(255), + "city" varchar(100), + "state" varchar(50), + "postalCode" varchar(20), + "country" varchar(100), + "defaultHourlyRate" real, + "createdById" varchar(255) NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp +); +--> statement-breakpoint +CREATE TABLE "beenvoice_invoice_item" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "invoiceId" varchar(255) NOT NULL, + "date" timestamp NOT NULL, + "description" varchar(500) NOT NULL, + "hours" real NOT NULL, + "rate" real NOT NULL, + "amount" real NOT NULL, + "position" integer DEFAULT 0 NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "beenvoice_invoice" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "invoiceNumber" varchar(100) NOT NULL, + "businessId" varchar(255), + "clientId" varchar(255) NOT NULL, + "issueDate" timestamp NOT NULL, + "dueDate" timestamp NOT NULL, + "status" varchar(50) DEFAULT 'draft' NOT NULL, + "totalAmount" real DEFAULT 0 NOT NULL, + "taxRate" real DEFAULT 0 NOT NULL, + "notes" varchar(1000), + "createdById" varchar(255) NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp +); +--> statement-breakpoint +CREATE TABLE "beenvoice_session" ( + "id" text PRIMARY KEY NOT NULL, + "userId" varchar(255) NOT NULL, + "token" varchar(255) NOT NULL, + "expiresAt" timestamp NOT NULL, + "ipAddress" text, + "userAgent" text, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "beenvoice_session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "beenvoice_sso_provider" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "providerId" varchar(255) NOT NULL, + "userId" varchar(255) NOT NULL, + "redirectURI" varchar(255) DEFAULT '' NOT NULL, + "oidcConfig" text, + "samlConfig" text, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "beenvoice_sso_provider_providerId_unique" UNIQUE("providerId") +); +--> statement-breakpoint +CREATE TABLE "beenvoice_user" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "email" varchar(255) NOT NULL, + "emailVerified" boolean DEFAULT false NOT NULL, + "image" varchar(255), + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL, + "password" varchar(255), + "resetToken" varchar(255), + "resetTokenExpiry" timestamp, + "prefersReducedMotion" boolean DEFAULT false NOT NULL, + "animationSpeedMultiplier" real DEFAULT 1 NOT NULL, + "colorTheme" varchar(50) DEFAULT 'slate' NOT NULL, + "customColor" varchar(50), + "theme" varchar(20) DEFAULT 'system' NOT NULL, + CONSTRAINT "beenvoice_user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "beenvoice_verification_token" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" varchar(255) NOT NULL, + "value" varchar(255) NOT NULL, + "expiresAt" timestamp NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "beenvoice_account" ADD CONSTRAINT "beenvoice_account_userId_beenvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_business" ADD CONSTRAINT "beenvoice_business_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_client" ADD CONSTRAINT "beenvoice_client_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_invoice_item" ADD CONSTRAINT "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk" FOREIGN KEY ("invoiceId") REFERENCES "public"."beenvoice_invoice"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_invoice" ADD CONSTRAINT "beenvoice_invoice_businessId_beenvoice_business_id_fk" FOREIGN KEY ("businessId") REFERENCES "public"."beenvoice_business"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_invoice" ADD CONSTRAINT "beenvoice_invoice_clientId_beenvoice_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."beenvoice_client"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_invoice" ADD CONSTRAINT "beenvoice_invoice_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_session" ADD CONSTRAINT "beenvoice_session_userId_beenvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_sso_provider" ADD CONSTRAINT "beenvoice_sso_provider_userId_beenvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "account_userId_idx" ON "beenvoice_account" USING btree ("userId");--> statement-breakpoint +CREATE INDEX "business_created_by_idx" ON "beenvoice_business" USING btree ("createdById");--> statement-breakpoint +CREATE INDEX "business_name_idx" ON "beenvoice_business" USING btree ("name");--> statement-breakpoint +CREATE INDEX "business_nickname_idx" ON "beenvoice_business" USING btree ("nickname");--> statement-breakpoint +CREATE INDEX "business_email_idx" ON "beenvoice_business" USING btree ("email");--> statement-breakpoint +CREATE INDEX "business_is_default_idx" ON "beenvoice_business" USING btree ("isDefault");--> statement-breakpoint +CREATE INDEX "client_created_by_idx" ON "beenvoice_client" USING btree ("createdById");--> statement-breakpoint +CREATE INDEX "client_name_idx" ON "beenvoice_client" USING btree ("name");--> statement-breakpoint +CREATE INDEX "client_email_idx" ON "beenvoice_client" USING btree ("email");--> statement-breakpoint +CREATE INDEX "invoice_item_invoice_id_idx" ON "beenvoice_invoice_item" USING btree ("invoiceId");--> statement-breakpoint +CREATE INDEX "invoice_item_date_idx" ON "beenvoice_invoice_item" USING btree ("date");--> statement-breakpoint +CREATE INDEX "invoice_item_position_idx" ON "beenvoice_invoice_item" USING btree ("position");--> statement-breakpoint +CREATE INDEX "invoice_business_id_idx" ON "beenvoice_invoice" USING btree ("businessId");--> statement-breakpoint +CREATE INDEX "invoice_client_id_idx" ON "beenvoice_invoice" USING btree ("clientId");--> statement-breakpoint +CREATE INDEX "invoice_created_by_idx" ON "beenvoice_invoice" USING btree ("createdById");--> statement-breakpoint +CREATE INDEX "invoice_number_idx" ON "beenvoice_invoice" USING btree ("invoiceNumber");--> statement-breakpoint +CREATE INDEX "invoice_status_idx" ON "beenvoice_invoice" USING btree ("status");--> statement-breakpoint +CREATE INDEX "session_userId_idx" ON "beenvoice_session" USING btree ("userId");--> statement-breakpoint +CREATE INDEX "sso_provider_user_id_idx" ON "beenvoice_sso_provider" USING btree ("userId");--> statement-breakpoint +CREATE INDEX "verification_token_identifier_idx" ON "beenvoice_verification_token" USING btree ("identifier"); \ No newline at end of file diff --git a/apps/web/drizzle/0001_supreme_the_enforcers.sql b/apps/web/drizzle/0001_supreme_the_enforcers.sql new file mode 100644 index 0000000..0873a66 --- /dev/null +++ b/apps/web/drizzle/0001_supreme_the_enforcers.sql @@ -0,0 +1,43 @@ +CREATE TABLE "beenvoice_expense" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "businessId" varchar(255), + "clientId" varchar(255), + "invoiceId" varchar(255), + "date" timestamp NOT NULL, + "description" varchar(500) NOT NULL, + "amount" real NOT NULL, + "currency" varchar(3) DEFAULT 'USD' NOT NULL, + "category" varchar(100), + "billable" boolean DEFAULT false NOT NULL, + "reimbursable" boolean DEFAULT false NOT NULL, + "notes" varchar(500), + "createdById" varchar(255) NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp +); +--> statement-breakpoint +CREATE TABLE "beenvoice_invoice_template" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "type" varchar(50) DEFAULT 'notes' NOT NULL, + "content" text NOT NULL, + "isDefault" boolean DEFAULT false NOT NULL, + "createdById" varchar(255) NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp +); +--> statement-breakpoint +ALTER TABLE "beenvoice_client" ADD COLUMN "currency" varchar(3) DEFAULT 'USD' NOT NULL;--> statement-breakpoint +ALTER TABLE "beenvoice_invoice" ADD COLUMN "currency" varchar(3) DEFAULT 'USD' NOT NULL;--> statement-breakpoint +ALTER TABLE "beenvoice_expense" ADD CONSTRAINT "beenvoice_expense_businessId_beenvoice_business_id_fk" FOREIGN KEY ("businessId") REFERENCES "public"."beenvoice_business"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_expense" ADD CONSTRAINT "beenvoice_expense_clientId_beenvoice_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."beenvoice_client"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_expense" ADD CONSTRAINT "beenvoice_expense_invoiceId_beenvoice_invoice_id_fk" FOREIGN KEY ("invoiceId") REFERENCES "public"."beenvoice_invoice"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_expense" ADD CONSTRAINT "beenvoice_expense_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "beenvoice_invoice_template" ADD CONSTRAINT "beenvoice_invoice_template_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "expense_created_by_idx" ON "beenvoice_expense" USING btree ("createdById");--> statement-breakpoint +CREATE INDEX "expense_client_id_idx" ON "beenvoice_expense" USING btree ("clientId");--> statement-breakpoint +CREATE INDEX "expense_invoice_id_idx" ON "beenvoice_expense" USING btree ("invoiceId");--> statement-breakpoint +CREATE INDEX "expense_date_idx" ON "beenvoice_expense" USING btree ("date");--> statement-breakpoint +CREATE INDEX "expense_billable_idx" ON "beenvoice_expense" USING btree ("billable");--> statement-breakpoint +CREATE INDEX "invoice_template_created_by_idx" ON "beenvoice_invoice_template" USING btree ("createdById");--> statement-breakpoint +CREATE INDEX "invoice_template_type_idx" ON "beenvoice_invoice_template" USING btree ("type"); \ No newline at end of file diff --git a/apps/web/drizzle/0002_tax_deductible.sql b/apps/web/drizzle/0002_tax_deductible.sql new file mode 100644 index 0000000..acc7c15 --- /dev/null +++ b/apps/web/drizzle/0002_tax_deductible.sql @@ -0,0 +1 @@ +ALTER TABLE "beenvoice_expense" ADD COLUMN "taxDeductible" boolean DEFAULT false NOT NULL; diff --git a/apps/web/drizzle/0003_appearance_preferences.sql b/apps/web/drizzle/0003_appearance_preferences.sql new file mode 100644 index 0000000..ce85917 --- /dev/null +++ b/apps/web/drizzle/0003_appearance_preferences.sql @@ -0,0 +1,2 @@ +ALTER TABLE "beenvoice_user" ADD COLUMN "interfaceTheme" varchar(50) DEFAULT 'beenvoice' NOT NULL; +ALTER TABLE "beenvoice_user" ADD COLUMN "fontPreference" varchar(50) DEFAULT 'brand' NOT NULL; diff --git a/apps/web/drizzle/0004_platform_appearance_controls.sql b/apps/web/drizzle/0004_platform_appearance_controls.sql new file mode 100644 index 0000000..d02a664 --- /dev/null +++ b/apps/web/drizzle/0004_platform_appearance_controls.sql @@ -0,0 +1,11 @@ +ALTER TABLE "beenvoice_user" +ADD COLUMN "bodyFontPreference" varchar(50) DEFAULT 'brand' NOT NULL; +--> statement-breakpoint +ALTER TABLE "beenvoice_user" +ADD COLUMN "headingFontPreference" varchar(50) DEFAULT 'brand' NOT NULL; +--> statement-breakpoint +ALTER TABLE "beenvoice_user" +ADD COLUMN "radiusPreference" varchar(20) DEFAULT 'xl' NOT NULL; +--> statement-breakpoint +ALTER TABLE "beenvoice_user" +ADD COLUMN "sidebarStyle" varchar(20) DEFAULT 'floating' NOT NULL; diff --git a/apps/web/drizzle/0005_platform_settings_and_roles.sql b/apps/web/drizzle/0005_platform_settings_and_roles.sql new file mode 100644 index 0000000..2c6089b --- /dev/null +++ b/apps/web/drizzle/0005_platform_settings_and_roles.sql @@ -0,0 +1,59 @@ +ALTER TABLE "beenvoice_user" +ADD COLUMN "role" varchar(20) DEFAULT 'user' NOT NULL; +--> statement-breakpoint +UPDATE "beenvoice_user" +SET "role" = 'admin' +WHERE "id" = ( + SELECT "id" + FROM "beenvoice_user" + ORDER BY "createdAt" ASC + LIMIT 1 +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "beenvoice_platform_setting" ( + "id" varchar(50) PRIMARY KEY DEFAULT 'global' NOT NULL, + "brandName" varchar(100) DEFAULT 'beenvoice' NOT NULL, + "brandTagline" varchar(255) DEFAULT 'Simple and efficient invoicing for freelancers and small businesses' NOT NULL, + "brandLogoText" varchar(100) DEFAULT 'beenvoice' NOT NULL, + "brandIcon" varchar(20) DEFAULT '$' NOT NULL, + "colorTheme" varchar(50) DEFAULT 'slate' NOT NULL, + "customColor" varchar(50), + "theme" varchar(20) DEFAULT 'system' NOT NULL, + "interfaceTheme" varchar(50) DEFAULT 'beenvoice' NOT NULL, + "bodyFontPreference" varchar(50) DEFAULT 'brand' NOT NULL, + "headingFontPreference" varchar(50) DEFAULT 'brand' NOT NULL, + "radiusPreference" varchar(20) DEFAULT 'xl' NOT NULL, + "sidebarStyle" varchar(20) DEFAULT 'floating' NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +INSERT INTO "beenvoice_platform_setting" ( + "id", + "brandName", + "brandTagline", + "brandLogoText", + "brandIcon", + "colorTheme", + "customColor", + "theme", + "interfaceTheme", + "bodyFontPreference", + "headingFontPreference", + "radiusPreference", + "sidebarStyle" +) VALUES ( + 'global', + 'beenvoice', + 'Simple and efficient invoicing for freelancers and small businesses', + 'beenvoice', + '$', + 'slate', + NULL, + 'system', + 'beenvoice', + 'brand', + 'brand', + 'xl', + 'floating' +) ON CONFLICT ("id") DO NOTHING; diff --git a/apps/web/drizzle/0006_pdf_generation_settings.sql b/apps/web/drizzle/0006_pdf_generation_settings.sql new file mode 100644 index 0000000..dcab66e --- /dev/null +++ b/apps/web/drizzle/0006_pdf_generation_settings.sql @@ -0,0 +1,14 @@ +ALTER TABLE "beenvoice_platform_setting" +ADD COLUMN "pdfTemplate" varchar(20) DEFAULT 'classic' NOT NULL; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" +ADD COLUMN "pdfAccentColor" varchar(50) DEFAULT '#111827' NOT NULL; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" +ADD COLUMN "pdfFooterText" varchar(120) DEFAULT 'Professional Invoicing' NOT NULL; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" +ADD COLUMN "pdfShowLogo" boolean DEFAULT true NOT NULL; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" +ADD COLUMN "pdfShowPageNumbers" boolean DEFAULT true NOT NULL; diff --git a/apps/web/drizzle/0007_invoice_email_message.sql b/apps/web/drizzle/0007_invoice_email_message.sql new file mode 100644 index 0000000..86d8beb --- /dev/null +++ b/apps/web/drizzle/0007_invoice_email_message.sql @@ -0,0 +1,2 @@ +ALTER TABLE "beenvoice_invoice" +ADD COLUMN "emailMessage" varchar(2000); diff --git a/apps/web/drizzle/0008_payments_recurring_public_links.sql b/apps/web/drizzle/0008_payments_recurring_public_links.sql new file mode 100644 index 0000000..cd325c2 --- /dev/null +++ b/apps/web/drizzle/0008_payments_recurring_public_links.sql @@ -0,0 +1,125 @@ +-- New columns on beenvoice_invoice +ALTER TABLE "beenvoice_invoice" ADD COLUMN IF NOT EXISTS "invoicePrefix" varchar(20) DEFAULT '#'; +--> statement-breakpoint +ALTER TABLE "beenvoice_invoice" ADD COLUMN IF NOT EXISTS "publicToken" varchar(255); +--> statement-breakpoint +ALTER TABLE "beenvoice_invoice" ADD COLUMN IF NOT EXISTS "lastReminderSentAt" timestamp; +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_invoice_publicToken_unique' + ) THEN + ALTER TABLE "beenvoice_invoice" ADD CONSTRAINT "beenvoice_invoice_publicToken_unique" UNIQUE("publicToken"); + END IF; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "invoice_public_token_idx" ON "beenvoice_invoice" USING btree ("publicToken"); +--> statement-breakpoint + +-- Partial payment tracking +CREATE TABLE IF NOT EXISTS "beenvoice_invoice_payment" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "invoiceId" varchar(255) NOT NULL, + "amount" real NOT NULL, + "currency" varchar(3) DEFAULT 'USD' NOT NULL, + "date" timestamp NOT NULL, + "method" varchar(50) DEFAULT 'other' NOT NULL, + "notes" varchar(500), + "createdById" varchar(255) NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_invoice_payment_invoiceId_beenvoice_invoice_id_fk' + ) THEN + ALTER TABLE "beenvoice_invoice_payment" ADD CONSTRAINT "beenvoice_invoice_payment_invoiceId_beenvoice_invoice_id_fk" FOREIGN KEY ("invoiceId") REFERENCES "public"."beenvoice_invoice"("id") ON DELETE cascade ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_invoice_payment_createdById_beenvoice_user_id_fk' + ) THEN + ALTER TABLE "beenvoice_invoice_payment" ADD CONSTRAINT "beenvoice_invoice_payment_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "invoice_payment_invoice_id_idx" ON "beenvoice_invoice_payment" USING btree ("invoiceId"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "invoice_payment_created_by_idx" ON "beenvoice_invoice_payment" USING btree ("createdById"); +--> statement-breakpoint + +-- Recurring invoices +CREATE TABLE IF NOT EXISTS "beenvoice_recurring_invoice" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "clientId" varchar(255) NOT NULL, + "businessId" varchar(255), + "schedule" varchar(20) DEFAULT 'monthly' NOT NULL, + "status" varchar(20) DEFAULT 'active' NOT NULL, + "invoicePrefix" varchar(20) DEFAULT '#', + "taxRate" real DEFAULT 0 NOT NULL, + "currency" varchar(3) DEFAULT 'USD' NOT NULL, + "notes" varchar(1000), + "emailMessage" varchar(2000), + "nextDueAt" timestamp NOT NULL, + "lastGeneratedAt" timestamp, + "createdById" varchar(255) NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp +); +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_recurring_invoice_clientId_beenvoice_client_id_fk' + ) THEN + ALTER TABLE "beenvoice_recurring_invoice" ADD CONSTRAINT "beenvoice_recurring_invoice_clientId_beenvoice_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."beenvoice_client"("id") ON DELETE no action ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_recurring_invoice_businessId_beenvoice_business_id_fk' + ) THEN + ALTER TABLE "beenvoice_recurring_invoice" ADD CONSTRAINT "beenvoice_recurring_invoice_businessId_beenvoice_business_id_fk" FOREIGN KEY ("businessId") REFERENCES "public"."beenvoice_business"("id") ON DELETE no action ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_recurring_invoice_createdById_beenvoice_user_id_fk' + ) THEN + ALTER TABLE "beenvoice_recurring_invoice" ADD CONSTRAINT "beenvoice_recurring_invoice_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "recurring_invoice_created_by_idx" ON "beenvoice_recurring_invoice" USING btree ("createdById"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "recurring_invoice_client_id_idx" ON "beenvoice_recurring_invoice" USING btree ("clientId"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "recurring_invoice_status_idx" ON "beenvoice_recurring_invoice" USING btree ("status"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "recurring_invoice_next_due_idx" ON "beenvoice_recurring_invoice" USING btree ("nextDueAt"); +--> statement-breakpoint + +-- Recurring invoice line items +CREATE TABLE IF NOT EXISTS "beenvoice_recurring_invoice_item" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "recurringInvoiceId" varchar(255) NOT NULL, + "description" varchar(500) NOT NULL, + "hours" real NOT NULL, + "rate" real NOT NULL, + "position" integer DEFAULT 0 NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_recurring_invoice_item_recurringInvoiceId_beenvoice_recurring_invoice_id_fk' + ) THEN + ALTER TABLE "beenvoice_recurring_invoice_item" ADD CONSTRAINT "beenvoice_recurring_invoice_item_recurringInvoiceId_beenvoice_recurring_invoice_id_fk" FOREIGN KEY ("recurringInvoiceId") REFERENCES "public"."beenvoice_recurring_invoice"("id") ON DELETE cascade ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "recurring_invoice_item_recurring_id_idx" ON "beenvoice_recurring_invoice_item" USING btree ("recurringInvoiceId"); diff --git a/apps/web/drizzle/0009_api_keys.sql b/apps/web/drizzle/0009_api_keys.sql new file mode 100644 index 0000000..48a2827 --- /dev/null +++ b/apps/web/drizzle/0009_api_keys.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS "beenvoice_api_key" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "name" varchar(100) NOT NULL, + "keyHash" varchar(64) NOT NULL, + "keyPrefix" varchar(16) NOT NULL, + "userId" varchar(255) NOT NULL, + "lastUsedAt" timestamp, + "expiresAt" timestamp, + "revokedAt" timestamp, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "beenvoice_api_key_keyHash_unique" UNIQUE("keyHash") +); +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_api_key_userId_beenvoice_user_id_fk' + ) THEN + ALTER TABLE "beenvoice_api_key" ADD CONSTRAINT "beenvoice_api_key_userId_beenvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."beenvoice_user"("id") ON DELETE cascade ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "api_key_hash_idx" ON "beenvoice_api_key" USING btree ("keyHash"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "api_key_user_id_idx" ON "beenvoice_api_key" USING btree ("userId"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "api_key_revoked_at_idx" ON "beenvoice_api_key" USING btree ("revokedAt"); diff --git a/apps/web/drizzle/0010_time_entries.sql b/apps/web/drizzle/0010_time_entries.sql new file mode 100644 index 0000000..cf73819 --- /dev/null +++ b/apps/web/drizzle/0010_time_entries.sql @@ -0,0 +1,37 @@ +CREATE TABLE IF NOT EXISTS "beenvoice_time_entry" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "description" varchar(500) DEFAULT '' NOT NULL, + "clientId" varchar(255), + "startedAt" timestamp NOT NULL, + "endedAt" timestamp, + "hours" real, + "rate" real, + "notes" varchar(500), + "createdById" varchar(255) NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp +); +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_time_entry_clientId_beenvoice_client_id_fk' + ) THEN + ALTER TABLE "beenvoice_time_entry" ADD CONSTRAINT "beenvoice_time_entry_clientId_beenvoice_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."beenvoice_client"("id") ON DELETE set null ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_time_entry_createdById_beenvoice_user_id_fk' + ) THEN + ALTER TABLE "beenvoice_time_entry" ADD CONSTRAINT "beenvoice_time_entry_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE cascade ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "time_entry_created_by_idx" ON "beenvoice_time_entry" USING btree ("createdById"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "time_entry_client_id_idx" ON "beenvoice_time_entry" USING btree ("clientId"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "time_entry_started_at_idx" ON "beenvoice_time_entry" USING btree ("startedAt"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "time_entry_ended_at_idx" ON "beenvoice_time_entry" USING btree ("endedAt"); diff --git a/apps/web/drizzle/0011_time_entry_invoice_id.sql b/apps/web/drizzle/0011_time_entry_invoice_id.sql new file mode 100644 index 0000000..1ab47b5 --- /dev/null +++ b/apps/web/drizzle/0011_time_entry_invoice_id.sql @@ -0,0 +1,11 @@ +ALTER TABLE "beenvoice_time_entry" ADD COLUMN IF NOT EXISTS "invoiceId" varchar(255); +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_time_entry_invoiceId_beenvoice_invoice_id_fk' + ) THEN + ALTER TABLE "beenvoice_time_entry" ADD CONSTRAINT "beenvoice_time_entry_invoiceId_beenvoice_invoice_id_fk" FOREIGN KEY ("invoiceId") REFERENCES "public"."beenvoice_invoice"("id") ON DELETE set null ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "time_entry_invoice_id_idx" ON "beenvoice_time_entry" USING btree ("invoiceId"); diff --git a/apps/web/drizzle/0012_verification_token_value_text.sql b/apps/web/drizzle/0012_verification_token_value_text.sql new file mode 100644 index 0000000..2cf46ec --- /dev/null +++ b/apps/web/drizzle/0012_verification_token_value_text.sql @@ -0,0 +1 @@ +ALTER TABLE "beenvoice_verification_token" ALTER COLUMN "value" TYPE text; diff --git a/apps/web/drizzle/0013_invoice_public_token_expiry.sql b/apps/web/drizzle/0013_invoice_public_token_expiry.sql new file mode 100644 index 0000000..480fc6b --- /dev/null +++ b/apps/web/drizzle/0013_invoice_public_token_expiry.sql @@ -0,0 +1 @@ +ALTER TABLE "beenvoice_invoice" ADD COLUMN IF NOT EXISTS "publicTokenExpiresAt" timestamp; diff --git a/apps/web/drizzle/0014_seed_demo_account.sql b/apps/web/drizzle/0014_seed_demo_account.sql new file mode 100644 index 0000000..88f4558 --- /dev/null +++ b/apps/web/drizzle/0014_seed_demo_account.sql @@ -0,0 +1,172 @@ +-- App Store review demo account: demo@example.com / demo123 +DO $$ +DECLARE + demo_user_id varchar(255) := 'a0000000-0000-4000-8000-000000000001'; + demo_account_id text := 'a0000000-0000-4000-8000-000000000010'; + demo_business_id varchar(255) := 'a0000000-0000-4000-8000-000000000020'; + demo_client_acme varchar(255) := 'a0000000-0000-4000-8000-000000000030'; + demo_client_bright varchar(255) := 'a0000000-0000-4000-8000-000000000031'; + demo_invoice_draft varchar(255) := 'a0000000-0000-4000-8000-000000000040'; + demo_invoice_sent varchar(255) := 'a0000000-0000-4000-8000-000000000041'; + demo_invoice_paid varchar(255) := 'a0000000-0000-4000-8000-000000000042'; + demo_password_hash text := '$2b$12$90U31okgkhOwSQD5RDqHwO0QpcC.pkKsqKb1IPnHfKUZm/2A9hzs6'; +BEGIN + IF EXISTS (SELECT 1 FROM "beenvoice_user" WHERE "email" = 'demo@example.com') THEN + RETURN; + END IF; + + INSERT INTO "beenvoice_user" ( + "id", "name", "email", "emailVerified", "password", "role" + ) VALUES ( + demo_user_id, + 'Demo User', + 'demo@example.com', + true, + demo_password_hash, + 'user' + ); + + INSERT INTO "beenvoice_account" ( + "id", "userId", "accountId", "providerId", "password" + ) VALUES ( + demo_account_id, + demo_user_id, + demo_user_id, + 'credential', + demo_password_hash + ); + + INSERT INTO "beenvoice_business" ( + "id", "name", "nickname", "email", "phone", "addressLine1", "city", "state", + "postalCode", "country", "isDefault", "createdById" + ) VALUES ( + demo_business_id, + 'Demo Studio LLC', + 'Demo Studio', + 'hello@demostudio.example', + '(555) 010-2000', + '100 Market Street', + 'San Francisco', + 'CA', + '94105', + 'United States', + true, + demo_user_id + ); + + INSERT INTO "beenvoice_client" ( + "id", "name", "email", "phone", "defaultHourlyRate", "currency", "createdById" + ) VALUES + ( + demo_client_acme, + 'Acme Corporation', + 'billing@acme.example', + '(555) 010-3001', + 150, + 'USD', + demo_user_id + ), + ( + demo_client_bright, + 'Bright Labs', + 'ap@brightlabs.example', + '(555) 010-3002', + 125, + 'USD', + demo_user_id + ); + + INSERT INTO "beenvoice_invoice" ( + "id", "invoiceNumber", "invoicePrefix", "businessId", "clientId", + "issueDate", "dueDate", "status", "totalAmount", "taxRate", "notes", "currency", "createdById" + ) VALUES + ( + demo_invoice_draft, + 'INV-DEMO-001', + '#', + demo_business_id, + demo_client_acme, + NOW() - INTERVAL '5 days', + NOW() + INTERVAL '25 days', + 'draft', + 1500, + 0, + 'Website redesign — phase 1', + 'USD', + demo_user_id + ), + ( + demo_invoice_sent, + 'INV-DEMO-002', + '#', + demo_business_id, + demo_client_bright, + NOW() - INTERVAL '20 days', + NOW() - INTERVAL '5 days', + 'sent', + 2500, + 0, + 'Mobile app consulting', + 'USD', + demo_user_id + ), + ( + demo_invoice_paid, + 'INV-DEMO-003', + '#', + demo_business_id, + demo_client_acme, + NOW() - INTERVAL '45 days', + NOW() - INTERVAL '15 days', + 'paid', + 3200, + 0, + 'API integration project', + 'USD', + demo_user_id + ); + + INSERT INTO "beenvoice_invoice_item" ( + "id", "invoiceId", "date", "description", "hours", "rate", "amount", "position" + ) VALUES + ( + 'a0000000-0000-4000-8000-000000000050', + demo_invoice_draft, + NOW() - INTERVAL '5 days', + 'UX wireframes and design system', + 10, + 150, + 1500, + 0 + ), + ( + 'a0000000-0000-4000-8000-000000000051', + demo_invoice_sent, + NOW() - INTERVAL '20 days', + 'Sprint planning and implementation', + 20, + 125, + 2500, + 0 + ), + ( + 'a0000000-0000-4000-8000-000000000052', + demo_invoice_paid, + NOW() - INTERVAL '45 days', + 'Backend API work', + 16, + 150, + 2400, + 0 + ), + ( + 'a0000000-0000-4000-8000-000000000053', + demo_invoice_paid, + NOW() - INTERVAL '44 days', + 'Deployment and documentation', + 5.33, + 150, + 800, + 1 + ); +END $$; diff --git a/apps/web/drizzle/0015_invoice_send_reminder_at.sql b/apps/web/drizzle/0015_invoice_send_reminder_at.sql new file mode 100644 index 0000000..653765e --- /dev/null +++ b/apps/web/drizzle/0015_invoice_send_reminder_at.sql @@ -0,0 +1 @@ +ALTER TABLE "beenvoice_invoice" ADD COLUMN IF NOT EXISTS "sendReminderAt" timestamp; diff --git a/apps/web/drizzle/0016_fix_send_reminder_at_column.sql b/apps/web/drizzle/0016_fix_send_reminder_at_column.sql new file mode 100644 index 0000000..2374415 --- /dev/null +++ b/apps/web/drizzle/0016_fix_send_reminder_at_column.sql @@ -0,0 +1,3 @@ +-- 0015 may have been recorded before the column name was corrected (send_reminder_at vs sendReminderAt). +ALTER TABLE "beenvoice_invoice" DROP COLUMN IF EXISTS "send_reminder_at"; +ALTER TABLE "beenvoice_invoice" ADD COLUMN IF NOT EXISTS "sendReminderAt" timestamp; diff --git a/apps/web/drizzle/0017_drop_theme_engine_columns.sql b/apps/web/drizzle/0017_drop_theme_engine_columns.sql new file mode 100644 index 0000000..598425c --- /dev/null +++ b/apps/web/drizzle/0017_drop_theme_engine_columns.sql @@ -0,0 +1,39 @@ +ALTER TABLE "beenvoice_user" DROP COLUMN IF EXISTS "colorTheme"; +--> statement-breakpoint +ALTER TABLE "beenvoice_user" DROP COLUMN IF EXISTS "customColor"; +--> statement-breakpoint +ALTER TABLE "beenvoice_user" DROP COLUMN IF EXISTS "interfaceTheme"; +--> statement-breakpoint +ALTER TABLE "beenvoice_user" DROP COLUMN IF EXISTS "fontPreference"; +--> statement-breakpoint +ALTER TABLE "beenvoice_user" DROP COLUMN IF EXISTS "bodyFontPreference"; +--> statement-breakpoint +ALTER TABLE "beenvoice_user" DROP COLUMN IF EXISTS "headingFontPreference"; +--> statement-breakpoint +ALTER TABLE "beenvoice_user" DROP COLUMN IF EXISTS "radiusPreference"; +--> statement-breakpoint +ALTER TABLE "beenvoice_user" DROP COLUMN IF EXISTS "sidebarStyle"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "brandName"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "brandTagline"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "brandLogoText"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "brandIcon"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "colorTheme"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "customColor"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "theme"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "interfaceTheme"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "bodyFontPreference"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "headingFontPreference"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "radiusPreference"; +--> statement-breakpoint +ALTER TABLE "beenvoice_platform_setting" DROP COLUMN IF EXISTS "sidebarStyle"; diff --git a/apps/web/drizzle/0018_user_onboarding.sql b/apps/web/drizzle/0018_user_onboarding.sql new file mode 100644 index 0000000..284e192 --- /dev/null +++ b/apps/web/drizzle/0018_user_onboarding.sql @@ -0,0 +1,11 @@ +ALTER TABLE "beenvoice_user" ADD COLUMN IF NOT EXISTS "onboardingCompletedAt" timestamp; + +-- Users who already have a business are treated as onboarded +UPDATE "beenvoice_user" u +SET "onboardingCompletedAt" = COALESCE(u."onboardingCompletedAt", NOW()) +WHERE u."onboardingCompletedAt" IS NULL + AND EXISTS ( + SELECT 1 + FROM "beenvoice_business" b + WHERE b."createdById" = u."id" + ); diff --git a/apps/web/drizzle/0019_pdf_font_family.sql b/apps/web/drizzle/0019_pdf_font_family.sql new file mode 100644 index 0000000..f004473 --- /dev/null +++ b/apps/web/drizzle/0019_pdf_font_family.sql @@ -0,0 +1,2 @@ +ALTER TABLE "beenvoice_platform_setting" +ADD COLUMN "pdfFontFamily" varchar(20) DEFAULT 'sans' NOT NULL; diff --git a/apps/web/drizzle/0020_pdf_numeric_font_family.sql b/apps/web/drizzle/0020_pdf_numeric_font_family.sql new file mode 100644 index 0000000..dd251c7 --- /dev/null +++ b/apps/web/drizzle/0020_pdf_numeric_font_family.sql @@ -0,0 +1,2 @@ +ALTER TABLE "beenvoice_platform_setting" +ADD COLUMN "pdfNumericFontFamily" varchar(20) DEFAULT 'mono' NOT NULL; diff --git a/apps/web/drizzle/0021_audit_log.sql b/apps/web/drizzle/0021_audit_log.sql new file mode 100644 index 0000000..bae1a9c --- /dev/null +++ b/apps/web/drizzle/0021_audit_log.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS "beenvoice_audit_log" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "actorUserId" varchar(255) NOT NULL, + "action" varchar(100) NOT NULL, + "targetType" varchar(50) NOT NULL, + "targetId" varchar(255), + "metadata" jsonb, + "createdAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "beenvoice_audit_log" +ADD CONSTRAINT "beenvoice_audit_log_actorUserId_beenvoice_user_id_fk" +FOREIGN KEY ("actorUserId") REFERENCES "public"."beenvoice_user"("id") +ON DELETE NO ACTION ON UPDATE NO ACTION; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "audit_log_actor_user_id_idx" ON "beenvoice_audit_log" USING btree ("actorUserId"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "audit_log_action_idx" ON "beenvoice_audit_log" USING btree ("action"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "audit_log_created_at_idx" ON "beenvoice_audit_log" USING btree ("createdAt"); diff --git a/apps/web/drizzle/0022_expense_business_receipts.sql b/apps/web/drizzle/0022_expense_business_receipts.sql new file mode 100644 index 0000000..af1bcd2 --- /dev/null +++ b/apps/web/drizzle/0022_expense_business_receipts.sql @@ -0,0 +1,43 @@ +CREATE INDEX IF NOT EXISTS "expense_business_id_idx" ON "beenvoice_expense" USING btree ("businessId"); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "beenvoice_expense_receipt" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "expenseId" varchar(255) NOT NULL, + "storageKey" varchar(500) NOT NULL, + "originalFilename" varchar(255) NOT NULL, + "mimeType" varchar(100) NOT NULL, + "sizeBytes" integer NOT NULL, + "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +ALTER TABLE "beenvoice_expense_receipt" +ADD CONSTRAINT "beenvoice_expense_receipt_expenseId_beenvoice_expense_id_fk" +FOREIGN KEY ("expenseId") REFERENCES "public"."beenvoice_expense"("id") +ON DELETE CASCADE ON UPDATE NO ACTION; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "expense_receipt_expense_id_idx" ON "beenvoice_expense_receipt" USING btree ("expenseId"); +--> statement-breakpoint +UPDATE "beenvoice_expense" e +SET "businessId" = i."businessId" +FROM "beenvoice_invoice" i +WHERE e."invoiceId" = i.id + AND e."businessId" IS NULL + AND i."businessId" IS NOT NULL; +--> statement-breakpoint +UPDATE "beenvoice_expense" e +SET "businessId" = sub.business_id +FROM ( + SELECT + e2.id AS expense_id, + ( + SELECT b2.id + FROM "beenvoice_business" b2 + WHERE b2."createdById" = e2."createdById" + ORDER BY b2."isDefault" DESC, b2."createdAt" DESC + LIMIT 1 + ) AS business_id + FROM "beenvoice_expense" e2 + WHERE e2."businessId" IS NULL +) sub +WHERE e.id = sub.expense_id + AND sub.business_id IS NOT NULL; diff --git a/apps/web/drizzle/0023_invoice_item_time_entry.sql b/apps/web/drizzle/0023_invoice_item_time_entry.sql new file mode 100644 index 0000000..8a488e3 --- /dev/null +++ b/apps/web/drizzle/0023_invoice_item_time_entry.sql @@ -0,0 +1,11 @@ +ALTER TABLE "beenvoice_invoice_item" ADD COLUMN IF NOT EXISTS "timeEntryId" varchar(255); +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'beenvoice_invoice_item_timeEntryId_beenvoice_time_entry_id_fk' + ) THEN + ALTER TABLE "beenvoice_invoice_item" ADD CONSTRAINT "beenvoice_invoice_item_timeEntryId_beenvoice_time_entry_id_fk" FOREIGN KEY ("timeEntryId") REFERENCES "public"."beenvoice_time_entry"("id") ON DELETE set null ON UPDATE no action; + END IF; +END $$; +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "invoice_item_time_entry_id_idx" ON "beenvoice_invoice_item" USING btree ("timeEntryId") WHERE "timeEntryId" is not null; diff --git a/apps/web/drizzle/0024_business_logo_upload.sql b/apps/web/drizzle/0024_business_logo_upload.sql new file mode 100644 index 0000000..6cd85bf --- /dev/null +++ b/apps/web/drizzle/0024_business_logo_upload.sql @@ -0,0 +1,3 @@ +ALTER TABLE "beenvoice_business" ADD COLUMN IF NOT EXISTS "logoStorageKey" varchar(500); +--> statement-breakpoint +ALTER TABLE "beenvoice_business" ADD COLUMN IF NOT EXISTS "logoMimeType" varchar(100); diff --git a/apps/web/drizzle/0025_business_hide_name_with_logo.sql b/apps/web/drizzle/0025_business_hide_name_with_logo.sql new file mode 100644 index 0000000..623692b --- /dev/null +++ b/apps/web/drizzle/0025_business_hide_name_with_logo.sql @@ -0,0 +1 @@ +ALTER TABLE "beenvoice_business" ADD COLUMN IF NOT EXISTS "hideNameWithLogo" boolean DEFAULT false NOT NULL; diff --git a/apps/web/drizzle/0026_business_hide_name_with_logo_fix.sql b/apps/web/drizzle/0026_business_hide_name_with_logo_fix.sql new file mode 100644 index 0000000..c664069 --- /dev/null +++ b/apps/web/drizzle/0026_business_hide_name_with_logo_fix.sql @@ -0,0 +1,8 @@ +-- 0025 was silently skipped on databases that already had a later migration +-- timestamp recorded (from a since-removed feature's migrations). Re-apply +-- idempotently so both that DB and any fresh install end up with the column. +ALTER TABLE "beenvoice_business" ADD COLUMN IF NOT EXISTS "hideNameWithLogo" boolean DEFAULT false NOT NULL; +--> statement-breakpoint +-- Drop the orphaned table from the business-document-vault feature that was +-- removed earlier this session (its migrations ran on this DB before removal). +DROP TABLE IF EXISTS "beenvoice_business_document" CASCADE; diff --git a/apps/web/drizzle/0027_disable_public_demo_password.sql b/apps/web/drizzle/0027_disable_public_demo_password.sql new file mode 100644 index 0000000..7d9c63b --- /dev/null +++ b/apps/web/drizzle/0027_disable_public_demo_password.sql @@ -0,0 +1,17 @@ +-- The original App Review credential was committed publicly in migration 0014. +-- Rotate it to an unknown value and invalidate its sessions. Use +-- `bun run demo:provision` with a private DEMO_ACCOUNT_PASSWORD when review +-- access is needed. +UPDATE "beenvoice_user" +SET "password" = '$2b$12$GyM6.bLv2.sZMsWytNz1L.j7pLwc79a55Nww6bSLQJ9OJarqY9oZW', + "updatedAt" = NOW() +WHERE "id" = 'a0000000-0000-4000-8000-000000000001'; + +UPDATE "beenvoice_account" +SET "password" = '$2b$12$GyM6.bLv2.sZMsWytNz1L.j7pLwc79a55Nww6bSLQJ9OJarqY9oZW', + "updatedAt" = NOW() +WHERE "userId" = 'a0000000-0000-4000-8000-000000000001' + AND "providerId" = 'credential'; + +DELETE FROM "beenvoice_session" +WHERE "userId" = 'a0000000-0000-4000-8000-000000000001'; diff --git a/apps/web/drizzle/0028_enable_public_demo_password.sql b/apps/web/drizzle/0028_enable_public_demo_password.sql new file mode 100644 index 0000000..98296f1 --- /dev/null +++ b/apps/web/drizzle/0028_enable_public_demo_password.sql @@ -0,0 +1,16 @@ +-- Restore the public App Store review credential for the seeded demo account. +-- Password: demo123 +UPDATE "beenvoice_user" +SET "password" = '$2b$12$90U31okgkhOwSQD5RDqHwO0QpcC.pkKsqKb1IPnHfKUZm/2A9hzs6', + "updatedAt" = NOW() +WHERE "id" = 'a0000000-0000-4000-8000-000000000001' + AND "email" = 'demo@example.com'; + +UPDATE "beenvoice_account" +SET "password" = '$2b$12$90U31okgkhOwSQD5RDqHwO0QpcC.pkKsqKb1IPnHfKUZm/2A9hzs6', + "updatedAt" = NOW() +WHERE "userId" = 'a0000000-0000-4000-8000-000000000001' + AND "providerId" = 'credential'; + +DELETE FROM "beenvoice_session" +WHERE "userId" = 'a0000000-0000-4000-8000-000000000001'; diff --git a/apps/web/drizzle/meta/0000_snapshot.json b/apps/web/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..54ec008 --- /dev/null +++ b/apps/web/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1259 @@ +{ + "id": "f6c70548-143c-48a3-a0c5-85873eaaa326", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.beenvoice_account": { + "name": "beenvoice_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_account_userId_beenvoice_user_id_fk": { + "name": "beenvoice_account_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_account", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_business": { + "name": "beenvoice_business", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "nickname": { + "name": "nickname", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "addressLine1": { + "name": "addressLine1", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "addressLine2": { + "name": "addressLine2", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "postalCode": { + "name": "postalCode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "taxId": { + "name": "taxId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "logoUrl": { + "name": "logoUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "resendApiKey": { + "name": "resendApiKey", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resendDomain": { + "name": "resendDomain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "emailFromName": { + "name": "emailFromName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "business_created_by_idx": { + "name": "business_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_name_idx": { + "name": "business_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_nickname_idx": { + "name": "business_nickname_idx", + "columns": [ + { + "expression": "nickname", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_email_idx": { + "name": "business_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_is_default_idx": { + "name": "business_is_default_idx", + "columns": [ + { + "expression": "isDefault", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_business_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_business_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_business", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_client": { + "name": "beenvoice_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "addressLine1": { + "name": "addressLine1", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "addressLine2": { + "name": "addressLine2", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "postalCode": { + "name": "postalCode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "defaultHourlyRate": { + "name": "defaultHourlyRate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "client_created_by_idx": { + "name": "client_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_name_idx": { + "name": "client_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_email_idx": { + "name": "client_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_client_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_client_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_client", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice_item": { + "name": "beenvoice_invoice_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "invoiceId": { + "name": "invoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "hours": { + "name": "hours", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "invoice_item_invoice_id_idx": { + "name": "invoice_item_invoice_id_idx", + "columns": [ + { + "expression": "invoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_item_date_idx": { + "name": "invoice_item_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_item_position_idx": { + "name": "invoice_item_position_idx", + "columns": [ + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk": { + "name": "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk", + "tableFrom": "beenvoice_invoice_item", + "tableTo": "beenvoice_invoice", + "columnsFrom": [ + "invoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice": { + "name": "beenvoice_invoice", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "businessId": { + "name": "businessId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "issueDate": { + "name": "issueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "dueDate": { + "name": "dueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "totalAmount": { + "name": "totalAmount", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "taxRate": { + "name": "taxRate", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invoice_business_id_idx": { + "name": "invoice_business_id_idx", + "columns": [ + { + "expression": "businessId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_client_id_idx": { + "name": "invoice_client_id_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_created_by_idx": { + "name": "invoice_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_number_idx": { + "name": "invoice_number_idx", + "columns": [ + { + "expression": "invoiceNumber", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_status_idx": { + "name": "invoice_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_businessId_beenvoice_business_id_fk": { + "name": "beenvoice_invoice_businessId_beenvoice_business_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_business", + "columnsFrom": [ + "businessId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_invoice_clientId_beenvoice_client_id_fk": { + "name": "beenvoice_invoice_clientId_beenvoice_client_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_invoice_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_invoice_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_session": { + "name": "beenvoice_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_session_userId_beenvoice_user_id_fk": { + "name": "beenvoice_session_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_session", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_session_token_unique": { + "name": "beenvoice_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_sso_provider": { + "name": "beenvoice_sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "redirectURI": { + "name": "redirectURI", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidcConfig": { + "name": "oidcConfig", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "samlConfig": { + "name": "samlConfig", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_sso_provider_userId_beenvoice_user_id_fk": { + "name": "beenvoice_sso_provider_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_sso_provider", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_sso_provider_providerId_unique": { + "name": "beenvoice_sso_provider_providerId_unique", + "nullsNotDistinct": false, + "columns": [ + "providerId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_user": { + "name": "beenvoice_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resetToken": { + "name": "resetToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resetTokenExpiry": { + "name": "resetTokenExpiry", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "prefersReducedMotion": { + "name": "prefersReducedMotion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "animationSpeedMultiplier": { + "name": "animationSpeedMultiplier", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "colorTheme": { + "name": "colorTheme", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'slate'" + }, + "customColor": { + "name": "customColor", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'system'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_user_email_unique": { + "name": "beenvoice_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_verification_token": { + "name": "beenvoice_verification_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_token_identifier_idx": { + "name": "verification_token_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/web/drizzle/meta/0001_snapshot.json b/apps/web/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..b88fdc2 --- /dev/null +++ b/apps/web/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1618 @@ +{ + "id": "7dc54995-f82a-4650-a7d6-9e7e4db678ee", + "prevId": "f6c70548-143c-48a3-a0c5-85873eaaa326", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.beenvoice_account": { + "name": "beenvoice_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_account_userId_beenvoice_user_id_fk": { + "name": "beenvoice_account_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_account", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_business": { + "name": "beenvoice_business", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "nickname": { + "name": "nickname", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "addressLine1": { + "name": "addressLine1", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "addressLine2": { + "name": "addressLine2", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "postalCode": { + "name": "postalCode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "taxId": { + "name": "taxId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "logoUrl": { + "name": "logoUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "resendApiKey": { + "name": "resendApiKey", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resendDomain": { + "name": "resendDomain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "emailFromName": { + "name": "emailFromName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "business_created_by_idx": { + "name": "business_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_name_idx": { + "name": "business_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_nickname_idx": { + "name": "business_nickname_idx", + "columns": [ + { + "expression": "nickname", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_email_idx": { + "name": "business_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_is_default_idx": { + "name": "business_is_default_idx", + "columns": [ + { + "expression": "isDefault", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_business_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_business_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_business", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_client": { + "name": "beenvoice_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "addressLine1": { + "name": "addressLine1", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "addressLine2": { + "name": "addressLine2", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "postalCode": { + "name": "postalCode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "defaultHourlyRate": { + "name": "defaultHourlyRate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "client_created_by_idx": { + "name": "client_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_name_idx": { + "name": "client_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_email_idx": { + "name": "client_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_client_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_client_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_client", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_expense": { + "name": "beenvoice_expense", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "businessId": { + "name": "businessId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "category": { + "name": "category", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "billable": { + "name": "billable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reimbursable": { + "name": "reimbursable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "expense_created_by_idx": { + "name": "expense_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_client_id_idx": { + "name": "expense_client_id_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_invoice_id_idx": { + "name": "expense_invoice_id_idx", + "columns": [ + { + "expression": "invoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_date_idx": { + "name": "expense_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_billable_idx": { + "name": "expense_billable_idx", + "columns": [ + { + "expression": "billable", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_expense_businessId_beenvoice_business_id_fk": { + "name": "beenvoice_expense_businessId_beenvoice_business_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_business", + "columnsFrom": [ + "businessId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_expense_clientId_beenvoice_client_id_fk": { + "name": "beenvoice_expense_clientId_beenvoice_client_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_expense_invoiceId_beenvoice_invoice_id_fk": { + "name": "beenvoice_expense_invoiceId_beenvoice_invoice_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_invoice", + "columnsFrom": [ + "invoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "beenvoice_expense_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_expense_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice_item": { + "name": "beenvoice_invoice_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "invoiceId": { + "name": "invoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "hours": { + "name": "hours", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "invoice_item_invoice_id_idx": { + "name": "invoice_item_invoice_id_idx", + "columns": [ + { + "expression": "invoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_item_date_idx": { + "name": "invoice_item_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_item_position_idx": { + "name": "invoice_item_position_idx", + "columns": [ + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk": { + "name": "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk", + "tableFrom": "beenvoice_invoice_item", + "tableTo": "beenvoice_invoice", + "columnsFrom": [ + "invoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice_template": { + "name": "beenvoice_invoice_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'notes'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invoice_template_created_by_idx": { + "name": "invoice_template_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_template_type_idx": { + "name": "invoice_template_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_template_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_invoice_template_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_invoice_template", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice": { + "name": "beenvoice_invoice", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "businessId": { + "name": "businessId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "issueDate": { + "name": "issueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "dueDate": { + "name": "dueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "totalAmount": { + "name": "totalAmount", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "taxRate": { + "name": "taxRate", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invoice_business_id_idx": { + "name": "invoice_business_id_idx", + "columns": [ + { + "expression": "businessId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_client_id_idx": { + "name": "invoice_client_id_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_created_by_idx": { + "name": "invoice_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_number_idx": { + "name": "invoice_number_idx", + "columns": [ + { + "expression": "invoiceNumber", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_status_idx": { + "name": "invoice_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_businessId_beenvoice_business_id_fk": { + "name": "beenvoice_invoice_businessId_beenvoice_business_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_business", + "columnsFrom": [ + "businessId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_invoice_clientId_beenvoice_client_id_fk": { + "name": "beenvoice_invoice_clientId_beenvoice_client_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_invoice_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_invoice_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_session": { + "name": "beenvoice_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_session_userId_beenvoice_user_id_fk": { + "name": "beenvoice_session_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_session", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_session_token_unique": { + "name": "beenvoice_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_sso_provider": { + "name": "beenvoice_sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "redirectURI": { + "name": "redirectURI", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidcConfig": { + "name": "oidcConfig", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "samlConfig": { + "name": "samlConfig", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_sso_provider_userId_beenvoice_user_id_fk": { + "name": "beenvoice_sso_provider_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_sso_provider", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_sso_provider_providerId_unique": { + "name": "beenvoice_sso_provider_providerId_unique", + "nullsNotDistinct": false, + "columns": [ + "providerId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_user": { + "name": "beenvoice_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resetToken": { + "name": "resetToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resetTokenExpiry": { + "name": "resetTokenExpiry", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "prefersReducedMotion": { + "name": "prefersReducedMotion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "animationSpeedMultiplier": { + "name": "animationSpeedMultiplier", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "colorTheme": { + "name": "colorTheme", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'slate'" + }, + "customColor": { + "name": "customColor", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'system'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_user_email_unique": { + "name": "beenvoice_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_verification_token": { + "name": "beenvoice_verification_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_token_identifier_idx": { + "name": "verification_token_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/web/drizzle/meta/0008_snapshot.json b/apps/web/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..7817f57 --- /dev/null +++ b/apps/web/drizzle/meta/0008_snapshot.json @@ -0,0 +1,2305 @@ +{ + "id": "4a78b572-415c-416f-a4c9-46fa08b5f939", + "prevId": "7dc54995-f82a-4650-a7d6-9e7e4db678ee", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.beenvoice_account": { + "name": "beenvoice_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_account_userId_beenvoice_user_id_fk": { + "name": "beenvoice_account_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_account", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_business": { + "name": "beenvoice_business", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "nickname": { + "name": "nickname", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "addressLine1": { + "name": "addressLine1", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "addressLine2": { + "name": "addressLine2", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "postalCode": { + "name": "postalCode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "taxId": { + "name": "taxId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "logoUrl": { + "name": "logoUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "resendApiKey": { + "name": "resendApiKey", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resendDomain": { + "name": "resendDomain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "emailFromName": { + "name": "emailFromName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "business_created_by_idx": { + "name": "business_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_name_idx": { + "name": "business_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_nickname_idx": { + "name": "business_nickname_idx", + "columns": [ + { + "expression": "nickname", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_email_idx": { + "name": "business_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_is_default_idx": { + "name": "business_is_default_idx", + "columns": [ + { + "expression": "isDefault", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_business_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_business_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_business", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_client": { + "name": "beenvoice_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "addressLine1": { + "name": "addressLine1", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "addressLine2": { + "name": "addressLine2", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "postalCode": { + "name": "postalCode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "defaultHourlyRate": { + "name": "defaultHourlyRate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "client_created_by_idx": { + "name": "client_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_name_idx": { + "name": "client_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_email_idx": { + "name": "client_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_client_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_client_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_client", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_expense": { + "name": "beenvoice_expense", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "businessId": { + "name": "businessId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "category": { + "name": "category", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "billable": { + "name": "billable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reimbursable": { + "name": "reimbursable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "taxDeductible": { + "name": "taxDeductible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "expense_created_by_idx": { + "name": "expense_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_client_id_idx": { + "name": "expense_client_id_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_invoice_id_idx": { + "name": "expense_invoice_id_idx", + "columns": [ + { + "expression": "invoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_date_idx": { + "name": "expense_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_billable_idx": { + "name": "expense_billable_idx", + "columns": [ + { + "expression": "billable", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_expense_businessId_beenvoice_business_id_fk": { + "name": "beenvoice_expense_businessId_beenvoice_business_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_business", + "columnsFrom": [ + "businessId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_expense_clientId_beenvoice_client_id_fk": { + "name": "beenvoice_expense_clientId_beenvoice_client_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_expense_invoiceId_beenvoice_invoice_id_fk": { + "name": "beenvoice_expense_invoiceId_beenvoice_invoice_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_invoice", + "columnsFrom": [ + "invoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "beenvoice_expense_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_expense_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice_item": { + "name": "beenvoice_invoice_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "invoiceId": { + "name": "invoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "hours": { + "name": "hours", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "invoice_item_invoice_id_idx": { + "name": "invoice_item_invoice_id_idx", + "columns": [ + { + "expression": "invoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_item_date_idx": { + "name": "invoice_item_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_item_position_idx": { + "name": "invoice_item_position_idx", + "columns": [ + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk": { + "name": "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk", + "tableFrom": "beenvoice_invoice_item", + "tableTo": "beenvoice_invoice", + "columnsFrom": [ + "invoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice_payment": { + "name": "beenvoice_invoice_payment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "invoiceId": { + "name": "invoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "notes": { + "name": "notes", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "invoice_payment_invoice_id_idx": { + "name": "invoice_payment_invoice_id_idx", + "columns": [ + { + "expression": "invoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_payment_created_by_idx": { + "name": "invoice_payment_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_payment_invoiceId_beenvoice_invoice_id_fk": { + "name": "beenvoice_invoice_payment_invoiceId_beenvoice_invoice_id_fk", + "tableFrom": "beenvoice_invoice_payment", + "tableTo": "beenvoice_invoice", + "columnsFrom": [ + "invoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "beenvoice_invoice_payment_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_invoice_payment_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_invoice_payment", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice_template": { + "name": "beenvoice_invoice_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'notes'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invoice_template_created_by_idx": { + "name": "invoice_template_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_template_type_idx": { + "name": "invoice_template_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_template_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_invoice_template_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_invoice_template", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice": { + "name": "beenvoice_invoice", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "invoicePrefix": { + "name": "invoicePrefix", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'#'" + }, + "businessId": { + "name": "businessId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "issueDate": { + "name": "issueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "dueDate": { + "name": "dueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "totalAmount": { + "name": "totalAmount", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "taxRate": { + "name": "taxRate", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "emailMessage": { + "name": "emailMessage", + "type": "varchar(2000)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "publicToken": { + "name": "publicToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "lastReminderSentAt": { + "name": "lastReminderSentAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invoice_business_id_idx": { + "name": "invoice_business_id_idx", + "columns": [ + { + "expression": "businessId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_client_id_idx": { + "name": "invoice_client_id_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_created_by_idx": { + "name": "invoice_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_number_idx": { + "name": "invoice_number_idx", + "columns": [ + { + "expression": "invoiceNumber", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_status_idx": { + "name": "invoice_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_public_token_idx": { + "name": "invoice_public_token_idx", + "columns": [ + { + "expression": "publicToken", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_businessId_beenvoice_business_id_fk": { + "name": "beenvoice_invoice_businessId_beenvoice_business_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_business", + "columnsFrom": [ + "businessId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_invoice_clientId_beenvoice_client_id_fk": { + "name": "beenvoice_invoice_clientId_beenvoice_client_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_invoice_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_invoice_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_invoice_publicToken_unique": { + "name": "beenvoice_invoice_publicToken_unique", + "nullsNotDistinct": false, + "columns": [ + "publicToken" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_platform_setting": { + "name": "beenvoice_platform_setting", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(50)", + "primaryKey": true, + "notNull": true, + "default": "'global'" + }, + "brandName": { + "name": "brandName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "default": "'beenvoice'" + }, + "brandTagline": { + "name": "brandTagline", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'Simple and efficient invoicing for freelancers and small businesses'" + }, + "brandLogoText": { + "name": "brandLogoText", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "default": "'beenvoice'" + }, + "brandIcon": { + "name": "brandIcon", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'$'" + }, + "colorTheme": { + "name": "colorTheme", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'slate'" + }, + "customColor": { + "name": "customColor", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "interfaceTheme": { + "name": "interfaceTheme", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'beenvoice'" + }, + "bodyFontPreference": { + "name": "bodyFontPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'brand'" + }, + "headingFontPreference": { + "name": "headingFontPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'brand'" + }, + "radiusPreference": { + "name": "radiusPreference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'xl'" + }, + "sidebarStyle": { + "name": "sidebarStyle", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'floating'" + }, + "pdfTemplate": { + "name": "pdfTemplate", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'classic'" + }, + "pdfAccentColor": { + "name": "pdfAccentColor", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'#111827'" + }, + "pdfFooterText": { + "name": "pdfFooterText", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true, + "default": "'Professional Invoicing'" + }, + "pdfShowLogo": { + "name": "pdfShowLogo", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "pdfShowPageNumbers": { + "name": "pdfShowPageNumbers", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_recurring_invoice_item": { + "name": "beenvoice_recurring_invoice_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "recurringInvoiceId": { + "name": "recurringInvoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "hours": { + "name": "hours", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "recurring_invoice_item_recurring_id_idx": { + "name": "recurring_invoice_item_recurring_id_idx", + "columns": [ + { + "expression": "recurringInvoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_recurring_invoice_item_recurringInvoiceId_beenvoice_recurring_invoice_id_fk": { + "name": "beenvoice_recurring_invoice_item_recurringInvoiceId_beenvoice_recurring_invoice_id_fk", + "tableFrom": "beenvoice_recurring_invoice_item", + "tableTo": "beenvoice_recurring_invoice", + "columnsFrom": [ + "recurringInvoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_recurring_invoice": { + "name": "beenvoice_recurring_invoice", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "businessId": { + "name": "businessId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'monthly'" + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "invoicePrefix": { + "name": "invoicePrefix", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'#'" + }, + "taxRate": { + "name": "taxRate", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "notes": { + "name": "notes", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "emailMessage": { + "name": "emailMessage", + "type": "varchar(2000)", + "primaryKey": false, + "notNull": false + }, + "nextDueAt": { + "name": "nextDueAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "lastGeneratedAt": { + "name": "lastGeneratedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "recurring_invoice_created_by_idx": { + "name": "recurring_invoice_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recurring_invoice_client_id_idx": { + "name": "recurring_invoice_client_id_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recurring_invoice_status_idx": { + "name": "recurring_invoice_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recurring_invoice_next_due_idx": { + "name": "recurring_invoice_next_due_idx", + "columns": [ + { + "expression": "nextDueAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_recurring_invoice_clientId_beenvoice_client_id_fk": { + "name": "beenvoice_recurring_invoice_clientId_beenvoice_client_id_fk", + "tableFrom": "beenvoice_recurring_invoice", + "tableTo": "beenvoice_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_recurring_invoice_businessId_beenvoice_business_id_fk": { + "name": "beenvoice_recurring_invoice_businessId_beenvoice_business_id_fk", + "tableFrom": "beenvoice_recurring_invoice", + "tableTo": "beenvoice_business", + "columnsFrom": [ + "businessId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_recurring_invoice_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_recurring_invoice_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_recurring_invoice", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_session": { + "name": "beenvoice_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_session_userId_beenvoice_user_id_fk": { + "name": "beenvoice_session_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_session", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_session_token_unique": { + "name": "beenvoice_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_sso_provider": { + "name": "beenvoice_sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "redirectURI": { + "name": "redirectURI", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidcConfig": { + "name": "oidcConfig", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "samlConfig": { + "name": "samlConfig", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_sso_provider_userId_beenvoice_user_id_fk": { + "name": "beenvoice_sso_provider_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_sso_provider", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_sso_provider_providerId_unique": { + "name": "beenvoice_sso_provider_providerId_unique", + "nullsNotDistinct": false, + "columns": [ + "providerId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_user": { + "name": "beenvoice_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resetToken": { + "name": "resetToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resetTokenExpiry": { + "name": "resetTokenExpiry", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "prefersReducedMotion": { + "name": "prefersReducedMotion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "animationSpeedMultiplier": { + "name": "animationSpeedMultiplier", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "colorTheme": { + "name": "colorTheme", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'slate'" + }, + "customColor": { + "name": "customColor", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "interfaceTheme": { + "name": "interfaceTheme", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'beenvoice'" + }, + "fontPreference": { + "name": "fontPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'brand'" + }, + "bodyFontPreference": { + "name": "bodyFontPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'brand'" + }, + "headingFontPreference": { + "name": "headingFontPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'brand'" + }, + "radiusPreference": { + "name": "radiusPreference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'xl'" + }, + "sidebarStyle": { + "name": "sidebarStyle", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'floating'" + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'user'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_user_email_unique": { + "name": "beenvoice_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_verification_token": { + "name": "beenvoice_verification_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_token_identifier_idx": { + "name": "verification_token_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/web/drizzle/meta/0016_snapshot.json b/apps/web/drizzle/meta/0016_snapshot.json new file mode 100644 index 0000000..21398d8 --- /dev/null +++ b/apps/web/drizzle/meta/0016_snapshot.json @@ -0,0 +1,2664 @@ +{ + "id": "6e79910c-7892-4fca-9aaf-6f6c210f3970", + "prevId": "4a78b572-415c-416f-a4c9-46fa08b5f939", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.beenvoice_account": { + "name": "beenvoice_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_account_userId_beenvoice_user_id_fk": { + "name": "beenvoice_account_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_account", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_api_key": { + "name": "beenvoice_api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "keyHash": { + "name": "keyHash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "keyPrefix": { + "name": "keyPrefix", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revokedAt": { + "name": "revokedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_key_hash_idx": { + "name": "api_key_hash_idx", + "columns": [ + { + "expression": "keyHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_id_idx": { + "name": "api_key_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_revoked_at_idx": { + "name": "api_key_revoked_at_idx", + "columns": [ + { + "expression": "revokedAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_api_key_userId_beenvoice_user_id_fk": { + "name": "beenvoice_api_key_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_api_key", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_api_key_keyHash_unique": { + "name": "beenvoice_api_key_keyHash_unique", + "nullsNotDistinct": false, + "columns": [ + "keyHash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_business": { + "name": "beenvoice_business", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "nickname": { + "name": "nickname", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "addressLine1": { + "name": "addressLine1", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "addressLine2": { + "name": "addressLine2", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "postalCode": { + "name": "postalCode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "taxId": { + "name": "taxId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "logoUrl": { + "name": "logoUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "resendApiKey": { + "name": "resendApiKey", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resendDomain": { + "name": "resendDomain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "emailFromName": { + "name": "emailFromName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "business_created_by_idx": { + "name": "business_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_name_idx": { + "name": "business_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_nickname_idx": { + "name": "business_nickname_idx", + "columns": [ + { + "expression": "nickname", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_email_idx": { + "name": "business_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "business_is_default_idx": { + "name": "business_is_default_idx", + "columns": [ + { + "expression": "isDefault", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_business_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_business_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_business", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_client": { + "name": "beenvoice_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "addressLine1": { + "name": "addressLine1", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "addressLine2": { + "name": "addressLine2", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "postalCode": { + "name": "postalCode", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "defaultHourlyRate": { + "name": "defaultHourlyRate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "client_created_by_idx": { + "name": "client_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_name_idx": { + "name": "client_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_email_idx": { + "name": "client_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_client_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_client_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_client", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_expense": { + "name": "beenvoice_expense", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "businessId": { + "name": "businessId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "category": { + "name": "category", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "billable": { + "name": "billable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reimbursable": { + "name": "reimbursable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "taxDeductible": { + "name": "taxDeductible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "expense_created_by_idx": { + "name": "expense_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_client_id_idx": { + "name": "expense_client_id_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_invoice_id_idx": { + "name": "expense_invoice_id_idx", + "columns": [ + { + "expression": "invoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_date_idx": { + "name": "expense_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expense_billable_idx": { + "name": "expense_billable_idx", + "columns": [ + { + "expression": "billable", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_expense_businessId_beenvoice_business_id_fk": { + "name": "beenvoice_expense_businessId_beenvoice_business_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_business", + "columnsFrom": [ + "businessId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_expense_clientId_beenvoice_client_id_fk": { + "name": "beenvoice_expense_clientId_beenvoice_client_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_expense_invoiceId_beenvoice_invoice_id_fk": { + "name": "beenvoice_expense_invoiceId_beenvoice_invoice_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_invoice", + "columnsFrom": [ + "invoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "beenvoice_expense_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_expense_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_expense", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice_item": { + "name": "beenvoice_invoice_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "invoiceId": { + "name": "invoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "hours": { + "name": "hours", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "invoice_item_invoice_id_idx": { + "name": "invoice_item_invoice_id_idx", + "columns": [ + { + "expression": "invoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_item_date_idx": { + "name": "invoice_item_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_item_position_idx": { + "name": "invoice_item_position_idx", + "columns": [ + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk": { + "name": "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk", + "tableFrom": "beenvoice_invoice_item", + "tableTo": "beenvoice_invoice", + "columnsFrom": [ + "invoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice_payment": { + "name": "beenvoice_invoice_payment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "invoiceId": { + "name": "invoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "notes": { + "name": "notes", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "invoice_payment_invoice_id_idx": { + "name": "invoice_payment_invoice_id_idx", + "columns": [ + { + "expression": "invoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_payment_created_by_idx": { + "name": "invoice_payment_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_payment_invoiceId_beenvoice_invoice_id_fk": { + "name": "beenvoice_invoice_payment_invoiceId_beenvoice_invoice_id_fk", + "tableFrom": "beenvoice_invoice_payment", + "tableTo": "beenvoice_invoice", + "columnsFrom": [ + "invoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "beenvoice_invoice_payment_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_invoice_payment_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_invoice_payment", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice_template": { + "name": "beenvoice_invoice_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'notes'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invoice_template_created_by_idx": { + "name": "invoice_template_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_template_type_idx": { + "name": "invoice_template_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_template_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_invoice_template_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_invoice_template", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_invoice": { + "name": "beenvoice_invoice", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "invoicePrefix": { + "name": "invoicePrefix", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'#'" + }, + "businessId": { + "name": "businessId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "issueDate": { + "name": "issueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "dueDate": { + "name": "dueDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "totalAmount": { + "name": "totalAmount", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "taxRate": { + "name": "taxRate", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "emailMessage": { + "name": "emailMessage", + "type": "varchar(2000)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "publicToken": { + "name": "publicToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "publicTokenExpiresAt": { + "name": "publicTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastReminderSentAt": { + "name": "lastReminderSentAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sendReminderAt": { + "name": "sendReminderAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invoice_business_id_idx": { + "name": "invoice_business_id_idx", + "columns": [ + { + "expression": "businessId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_client_id_idx": { + "name": "invoice_client_id_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_created_by_idx": { + "name": "invoice_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_number_idx": { + "name": "invoice_number_idx", + "columns": [ + { + "expression": "invoiceNumber", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_status_idx": { + "name": "invoice_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invoice_public_token_idx": { + "name": "invoice_public_token_idx", + "columns": [ + { + "expression": "publicToken", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_invoice_businessId_beenvoice_business_id_fk": { + "name": "beenvoice_invoice_businessId_beenvoice_business_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_business", + "columnsFrom": [ + "businessId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_invoice_clientId_beenvoice_client_id_fk": { + "name": "beenvoice_invoice_clientId_beenvoice_client_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_invoice_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_invoice_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_invoice", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_invoice_publicToken_unique": { + "name": "beenvoice_invoice_publicToken_unique", + "nullsNotDistinct": false, + "columns": [ + "publicToken" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_platform_setting": { + "name": "beenvoice_platform_setting", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(50)", + "primaryKey": true, + "notNull": true, + "default": "'global'" + }, + "brandName": { + "name": "brandName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "default": "'beenvoice'" + }, + "brandTagline": { + "name": "brandTagline", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'Simple and efficient invoicing for freelancers and small businesses'" + }, + "brandLogoText": { + "name": "brandLogoText", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "default": "'beenvoice'" + }, + "brandIcon": { + "name": "brandIcon", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'$'" + }, + "colorTheme": { + "name": "colorTheme", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'slate'" + }, + "customColor": { + "name": "customColor", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "interfaceTheme": { + "name": "interfaceTheme", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'beenvoice'" + }, + "bodyFontPreference": { + "name": "bodyFontPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'brand'" + }, + "headingFontPreference": { + "name": "headingFontPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'brand'" + }, + "radiusPreference": { + "name": "radiusPreference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'xl'" + }, + "sidebarStyle": { + "name": "sidebarStyle", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'floating'" + }, + "pdfTemplate": { + "name": "pdfTemplate", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'classic'" + }, + "pdfAccentColor": { + "name": "pdfAccentColor", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'#111827'" + }, + "pdfFooterText": { + "name": "pdfFooterText", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true, + "default": "'Professional Invoicing'" + }, + "pdfShowLogo": { + "name": "pdfShowLogo", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "pdfShowPageNumbers": { + "name": "pdfShowPageNumbers", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_recurring_invoice_item": { + "name": "beenvoice_recurring_invoice_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "recurringInvoiceId": { + "name": "recurringInvoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "hours": { + "name": "hours", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "recurring_invoice_item_recurring_id_idx": { + "name": "recurring_invoice_item_recurring_id_idx", + "columns": [ + { + "expression": "recurringInvoiceId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_recurring_invoice_item_recurringInvoiceId_beenvoice_recurring_invoice_id_fk": { + "name": "beenvoice_recurring_invoice_item_recurringInvoiceId_beenvoice_recurring_invoice_id_fk", + "tableFrom": "beenvoice_recurring_invoice_item", + "tableTo": "beenvoice_recurring_invoice", + "columnsFrom": [ + "recurringInvoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_recurring_invoice": { + "name": "beenvoice_recurring_invoice", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "businessId": { + "name": "businessId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'monthly'" + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "invoicePrefix": { + "name": "invoicePrefix", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'#'" + }, + "taxRate": { + "name": "taxRate", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "notes": { + "name": "notes", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "emailMessage": { + "name": "emailMessage", + "type": "varchar(2000)", + "primaryKey": false, + "notNull": false + }, + "nextDueAt": { + "name": "nextDueAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "lastGeneratedAt": { + "name": "lastGeneratedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "recurring_invoice_created_by_idx": { + "name": "recurring_invoice_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recurring_invoice_client_id_idx": { + "name": "recurring_invoice_client_id_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recurring_invoice_status_idx": { + "name": "recurring_invoice_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recurring_invoice_next_due_idx": { + "name": "recurring_invoice_next_due_idx", + "columns": [ + { + "expression": "nextDueAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_recurring_invoice_clientId_beenvoice_client_id_fk": { + "name": "beenvoice_recurring_invoice_clientId_beenvoice_client_id_fk", + "tableFrom": "beenvoice_recurring_invoice", + "tableTo": "beenvoice_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_recurring_invoice_businessId_beenvoice_business_id_fk": { + "name": "beenvoice_recurring_invoice_businessId_beenvoice_business_id_fk", + "tableFrom": "beenvoice_recurring_invoice", + "tableTo": "beenvoice_business", + "columnsFrom": [ + "businessId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "beenvoice_recurring_invoice_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_recurring_invoice_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_recurring_invoice", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_session": { + "name": "beenvoice_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_session_userId_beenvoice_user_id_fk": { + "name": "beenvoice_session_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_session", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_session_token_unique": { + "name": "beenvoice_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_sso_provider": { + "name": "beenvoice_sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "redirectURI": { + "name": "redirectURI", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidcConfig": { + "name": "oidcConfig", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "samlConfig": { + "name": "samlConfig", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_sso_provider_userId_beenvoice_user_id_fk": { + "name": "beenvoice_sso_provider_userId_beenvoice_user_id_fk", + "tableFrom": "beenvoice_sso_provider", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_sso_provider_providerId_unique": { + "name": "beenvoice_sso_provider_providerId_unique", + "nullsNotDistinct": false, + "columns": [ + "providerId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_time_entry": { + "name": "beenvoice_time_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "clientId": { + "name": "clientId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "startedAt": { + "name": "startedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "endedAt": { + "name": "endedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hours": { + "name": "hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rate": { + "name": "rate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "createdById": { + "name": "createdById", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "time_entry_created_by_idx": { + "name": "time_entry_created_by_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "time_entry_client_id_idx": { + "name": "time_entry_client_id_idx", + "columns": [ + { + "expression": "clientId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "time_entry_started_at_idx": { + "name": "time_entry_started_at_idx", + "columns": [ + { + "expression": "startedAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "time_entry_ended_at_idx": { + "name": "time_entry_ended_at_idx", + "columns": [ + { + "expression": "endedAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "time_entry_one_running_per_user_idx": { + "name": "time_entry_one_running_per_user_idx", + "columns": [ + { + "expression": "createdById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"beenvoice_time_entry\".\"endedAt\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beenvoice_time_entry_clientId_beenvoice_client_id_fk": { + "name": "beenvoice_time_entry_clientId_beenvoice_client_id_fk", + "tableFrom": "beenvoice_time_entry", + "tableTo": "beenvoice_client", + "columnsFrom": [ + "clientId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "beenvoice_time_entry_invoiceId_beenvoice_invoice_id_fk": { + "name": "beenvoice_time_entry_invoiceId_beenvoice_invoice_id_fk", + "tableFrom": "beenvoice_time_entry", + "tableTo": "beenvoice_invoice", + "columnsFrom": [ + "invoiceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "beenvoice_time_entry_createdById_beenvoice_user_id_fk": { + "name": "beenvoice_time_entry_createdById_beenvoice_user_id_fk", + "tableFrom": "beenvoice_time_entry", + "tableTo": "beenvoice_user", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_user": { + "name": "beenvoice_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resetToken": { + "name": "resetToken", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "resetTokenExpiry": { + "name": "resetTokenExpiry", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "prefersReducedMotion": { + "name": "prefersReducedMotion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "animationSpeedMultiplier": { + "name": "animationSpeedMultiplier", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "colorTheme": { + "name": "colorTheme", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'slate'" + }, + "customColor": { + "name": "customColor", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "interfaceTheme": { + "name": "interfaceTheme", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'beenvoice'" + }, + "fontPreference": { + "name": "fontPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'brand'" + }, + "bodyFontPreference": { + "name": "bodyFontPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'brand'" + }, + "headingFontPreference": { + "name": "headingFontPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'brand'" + }, + "radiusPreference": { + "name": "radiusPreference", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'xl'" + }, + "sidebarStyle": { + "name": "sidebarStyle", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'floating'" + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'user'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "beenvoice_user_email_unique": { + "name": "beenvoice_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beenvoice_verification_token": { + "name": "beenvoice_verification_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_token_identifier_idx": { + "name": "verification_token_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/web/drizzle/meta/_journal.json b/apps/web/drizzle/meta/_journal.json new file mode 100644 index 0000000..3539d1f --- /dev/null +++ b/apps/web/drizzle/meta/_journal.json @@ -0,0 +1,209 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1775354242672, + "tag": "0000_glossy_magneto", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1775356013998, + "tag": "0001_supreme_the_enforcers", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1775400000000, + "tag": "0002_tax_deductible", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1775600000000, + "tag": "0003_appearance_preferences", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1777336000000, + "tag": "0004_platform_appearance_controls", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1777337000000, + "tag": "0005_platform_settings_and_roles", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1777338000000, + "tag": "0006_pdf_generation_settings", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1777339000000, + "tag": "0007_invoice_email_message", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1747526400000, + "tag": "0008_payments_recurring_public_links", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1780617600000, + "tag": "0009_api_keys", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1780704000000, + "tag": "0010_time_entries", + "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1749254400000, + "tag": "0011_time_entry_invoice_id", + "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1749340800000, + "tag": "0012_verification_token_value_text", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1781194385000, + "tag": "0013_invoice_public_token_expiry", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1781300000000, + "tag": "0014_seed_demo_account", + "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1781400000000, + "tag": "0015_invoice_send_reminder_at", + "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1781500000000, + "tag": "0016_fix_send_reminder_at_column", + "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1781600000000, + "tag": "0017_drop_theme_engine_columns", + "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1781700000000, + "tag": "0018_user_onboarding", + "breakpoints": true + }, + { + "idx": 19, + "version": "7", + "when": 1781800000000, + "tag": "0019_pdf_font_family", + "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1781900000000, + "tag": "0020_pdf_numeric_font_family", + "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1782000000000, + "tag": "0021_audit_log", + "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1782100000000, + "tag": "0022_expense_business_receipts", + "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1782200000000, + "tag": "0023_invoice_item_time_entry", + "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1783700000000, + "tag": "0024_business_logo_upload", + "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1783800000000, + "tag": "0025_business_hide_name_with_logo", + "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1784000000000, + "tag": "0026_business_hide_name_with_logo_fix", + "breakpoints": true + }, + { + "idx": 27, + "version": "7", + "when": 1786740000000, + "tag": "0027_disable_public_demo_password", + "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1786766968000, + "tag": "0028_enable_public_demo_password", + "breakpoints": true + } + ] +} diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js new file mode 100644 index 0000000..e03c85c --- /dev/null +++ b/apps/web/eslint.config.js @@ -0,0 +1,57 @@ +import nextCoreWebVitals from "eslint-config-next/core-web-vitals"; +import tseslint from "typescript-eslint"; +// @ts-ignore -- no types for this plugin +import drizzle from "eslint-plugin-drizzle"; + +export default tseslint.config( + { + ignores: [".next", "scripts/**"], + }, + ...nextCoreWebVitals, + { + files: ["**/*.ts", "**/*.tsx"], + plugins: { + drizzle, + }, + extends: [ + ...tseslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + ], + rules: { + "@typescript-eslint/array-type": "off", + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/consistent-type-imports": [ + "warn", + { prefer: "type-imports", fixStyle: "inline-type-imports" }, + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_" }, + ], + "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-misused-promises": [ + "error", + { checksVoidReturn: { attributes: false } }, + ], + "drizzle/enforce-delete-with-where": [ + "error", + { drizzleObjectName: ["db", "ctx.db"] }, + ], + "drizzle/enforce-update-with-where": [ + "error", + { drizzleObjectName: ["db", "ctx.db"] }, + ], + }, + }, + { + linterOptions: { + reportUnusedDisableDirectives: true, + }, + languageOptions: { + parserOptions: { + projectService: true, + }, + }, + }, +); diff --git a/apps/web/next.config.js b/apps/web/next.config.js new file mode 100644 index 0000000..eeef5bf --- /dev/null +++ b/apps/web/next.config.js @@ -0,0 +1,28 @@ +/** + * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful + * for Docker builds. + */ +import "./src/env.js"; + +const isDockerBuild = process.env.DOCKER_BUILD === "1"; +const disableReactCompiler = process.env.DISABLE_REACT_COMPILER === "1"; + +/** @type {import("next").NextConfig} */ +const config = { + // React Compiler is helpful in dev/prod but adds compile-time memory pressure in Docker builds. + reactCompiler: !disableReactCompiler, + productionBrowserSourceMaps: false, + serverExternalPackages: ["pg", "better-auth"], + experimental: { + webpackMemoryOptimizations: true, + }, + // Skip duplicate typecheck during Docker `next build` to lower peak memory. + // Lint separately via `bun run lint` / `bun run check`. + ...(isDockerBuild + ? { + typescript: { ignoreBuildErrors: true }, + } + : {}), +}; + +export default config; diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..36ec6a7 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,130 @@ +{ + "name": "beenvoice", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "next build", + "check": "eslint . && tsc --noEmit", + "db:generate": "drizzle-kit generate", + "db:migrate": "bun src/server/db/migrate.ts", + "db:verify-journal": "bun scripts/verify-drizzle-journal.ts", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio", + "db:clone": "./scripts/clone-local.sh", + "demo:provision": "bun scripts/provision-demo-account.ts", + "docker:up": "colima start && docker compose -f docker-compose.dev.yml up -d", + "docker:down": "docker compose -f docker-compose.dev.yml down && colima stop", + "docker:dev:down": "docker compose -f docker-compose.dev.yml down && colima stop", + "docker:deploy": "./scripts/docker-deploy.sh", + "deploy": "drizzle-kit push && next build", + "dev": "next dev --turbo", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", + "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "preview": "next build && next start", + "start": "next start", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.1075.0", + "@better-auth/expo": "^1.6.19", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@fontsource-variable/playfair-display": "^5.2.8", + "@radix-ui/react-alert-dialog": "^1.1.16", + "@radix-ui/react-avatar": "^1.1.12", + "@radix-ui/react-checkbox": "^1.3.4", + "@radix-ui/react-collapsible": "^1.1.13", + "@radix-ui/react-dialog": "^1.1.16", + "@radix-ui/react-dropdown-menu": "^2.1.17", + "@radix-ui/react-label": "^2.1.9", + "@radix-ui/react-navigation-menu": "^1.2.15", + "@radix-ui/react-popover": "^1.1.16", + "@radix-ui/react-progress": "^1.1.9", + "@radix-ui/react-select": "^2.3.0", + "@radix-ui/react-separator": "^1.1.9", + "@radix-ui/react-slot": "^1.2.5", + "@radix-ui/react-switch": "^1.3.0", + "@radix-ui/react-tabs": "^1.1.14", + "@radix-ui/react-tooltip": "^1.2.9", + "@react-pdf/renderer": "^4.5.1", + "@t3-oss/env-nextjs": "^0.12.0", + "@tanstack/react-query": "^5.101.0", + "@tanstack/react-table": "^8.21.3", + "@tiptap/extension-color": "^3.13.0", + "@tiptap/extension-list-item": "^3.13.0", + "@tiptap/extension-text-align": "^3.13.0", + "@tiptap/extension-text-style": "^3.13.0", + "@tiptap/react": "^3.13.0", + "@tiptap/starter-kit": "^3.13.0", + "@trpc/client": "^11.17.0", + "@trpc/react-query": "^11.17.0", + "@trpc/server": "^11.17.0", + "bcryptjs": "^3.0.3", + "better-auth": "^1.6.16", + "chrono-node": "^2.9.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.4.0", + "dotenv": "^17.4.2", + "drizzle-orm": "^0.45.2", + "file-saver": "^2.0.5", + "framer-motion": "^12.40.0", + "fuse.js": "^7.4.2", + "lucide-react": "^0.525.0", + "next": "^16.2.12", + "pg": "8.21.0", + "react": "^19.2.8", + "react-colorful": "^5.7.0", + "react-day-picker": "^9.12.0", + "react-dom": "^19.2.8", + "react-dropzone": "^14.3.8", + "recharts": "^3.8.1", + "resend": "^4.8.0", + "server-only": "^0.0.1", + "sharp": "^0.35.3", + "sonner": "^2.0.7", + "superjson": "^2.2.6", + "tailwind-merge": "^3.6.0", + "trpc": "^0.11.3", + "zod": "^3.25.76" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.3.0", + "@types/bcryptjs": "^2.4.6", + "@types/file-saver": "^2.0.7", + "@types/node": "^20.19.26", + "@types/pg": "^8.20.0", + "@types/raf": "^3.4.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "babel-plugin-react-compiler": "^1.0.0", + "baseline-browser-mapping": "^2.10.34", + "drizzle-kit": "^0.31.10", + "eslint": "^9.39.1", + "eslint-config-next": "^16.2.7", + "eslint-plugin-drizzle": "^0.2.3", + "postcss": "^8.5.15", + "prettier": "3.8.3", + "prettier-plugin-tailwindcss": "^0.6.14", + "tailwindcss": "^4.3.0", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.60.1" + }, + "ct3aMetadata": { + "initVersion": "7.39.3" + }, + "trustedDependencies": [ + "@tailwindcss/oxide", + "core-js", + "esbuild", + "sharp", + "unrs-resolver" + ] +} diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/apps/web/postcss.config.js @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/apps/web/prettier.config.js b/apps/web/prettier.config.js new file mode 100644 index 0000000..b2d59b4 --- /dev/null +++ b/apps/web/prettier.config.js @@ -0,0 +1,6 @@ +/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ +const config = { + plugins: ["prettier-plugin-tailwindcss"], +}; + +export default config; diff --git a/apps/web/public/beenvoice-logo.png b/apps/web/public/beenvoice-logo.png new file mode 100644 index 0000000..4d63519 Binary files /dev/null and b/apps/web/public/beenvoice-logo.png differ diff --git a/apps/web/public/beenvoice-logo.svg b/apps/web/public/beenvoice-logo.svg new file mode 100644 index 0000000..8c0afdd --- /dev/null +++ b/apps/web/public/beenvoice-logo.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/beenvoice.afdesign b/apps/web/public/beenvoice.afdesign new file mode 100644 index 0000000..94d7617 Binary files /dev/null and b/apps/web/public/beenvoice.afdesign differ diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico new file mode 100644 index 0000000..df25543 Binary files /dev/null and b/apps/web/public/favicon.ico differ diff --git a/apps/web/public/fonts/frutiger/Frutiger.ttf b/apps/web/public/fonts/frutiger/Frutiger.ttf new file mode 100644 index 0000000..67021b4 Binary files /dev/null and b/apps/web/public/fonts/frutiger/Frutiger.ttf differ diff --git a/apps/web/public/fonts/frutiger/Frutiger_bold.ttf b/apps/web/public/fonts/frutiger/Frutiger_bold.ttf new file mode 100644 index 0000000..a6d4fa0 Binary files /dev/null and b/apps/web/public/fonts/frutiger/Frutiger_bold.ttf differ diff --git a/apps/web/public/fonts/geist/mono/GeistMono-VariableFont_wght.ttf b/apps/web/public/fonts/geist/mono/GeistMono-VariableFont_wght.ttf new file mode 100644 index 0000000..f86f195 Binary files /dev/null and b/apps/web/public/fonts/geist/mono/GeistMono-VariableFont_wght.ttf differ diff --git a/apps/web/public/fonts/geist/sans/Geist-VariableFont_wght.ttf b/apps/web/public/fonts/geist/sans/Geist-VariableFont_wght.ttf new file mode 100644 index 0000000..ad6f2c5 Binary files /dev/null and b/apps/web/public/fonts/geist/sans/Geist-VariableFont_wght.ttf differ diff --git a/apps/web/scripts/clone-local.sh b/apps/web/scripts/clone-local.sh new file mode 100755 index 0000000..788d4d4 --- /dev/null +++ b/apps/web/scripts/clone-local.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Function to read a variable from a specific env file +read_env_var() { + local file="$1" + local var="$2" + if [ -f "$file" ]; then + grep "^$var=" "$file" | cut -d '=' -f2- | tr -d '"' | tr -d "'" + fi +} + +# 1. Get Production URL +# Priority: Argument > .env.production > .env +PROD_DB_URL="$1" + +if [ -z "$PROD_DB_URL" ]; then + echo "Checking .env.production for DATABASE_URL..." + PROD_DB_URL=$(read_env_var ".env.production" "DATABASE_URL") +fi + +if [ -z "$PROD_DB_URL" ]; then + echo "Checking .env for PROD_DATABASE_URL..." + PROD_DB_URL=$(read_env_var ".env" "PROD_DATABASE_URL") +fi + +if [ -z "$PROD_DB_URL" ]; then + echo "Error: Could not find production database URL." + echo "Please provide it as an argument, or set DATABASE_URL in .env.production, or PROD_DATABASE_URL in .env" + echo "Usage: $0 " + exit 1 +fi + +# 2. Get Target URL +# Priority: .env.local > .env +TARGET_DB_URL=$(read_env_var ".env.local" "DATABASE_URL") +if [ -z "$TARGET_DB_URL" ]; then TARGET_DB_URL=$(read_env_var ".env" "DATABASE_URL"); fi + +if [ -z "$TARGET_DB_URL" ]; then + echo "Error: Could not find target DATABASE_URL in .env.local or .env" + exit 1 +fi + +echo "Configuration:" +echo " Source: $PROD_DB_URL" +echo " Target: $TARGET_DB_URL" +echo +echo "⚠️ WARNING: This will OVERWRITE the target database at the above URL." +echo "This is a one-time migration script." +read -p "Are you sure you want to continue? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 1 +fi + +echo "Cloning database..." + +# Use local pg_dump and psql directly +# This assumes pg_dump and psql are installed on the host machine +pg_dump "$PROD_DB_URL" \ + --clean --if-exists \ + --no-owner --no-privileges \ + --format=plain \ + | psql "$TARGET_DB_URL" + +if [ $? -eq 0 ]; then + echo "✅ Database cloned successfully!" +else + echo "❌ Database clone failed." + exit 1 +fi diff --git a/apps/web/scripts/docker-deploy.sh b/apps/web/scripts/docker-deploy.sh new file mode 100755 index 0000000..1bf89bf --- /dev/null +++ b/apps/web/scripts/docker-deploy.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Production deploy helper for docker-compose.yml (not docker-compose.dev.yml). +# Rebuilds the app image from the current working tree, then starts/restarts services +# (app, db, garage). Receipt storage uses in-stack Garage unless S3_* are +# overridden in .env. Garage S3 API: localhost:${GARAGE_API_PORT:-3900}. +# +# Plain `docker compose up -d` reuses the local image tag and does NOT pick up +# changes from `git pull`. Always pass --build or use this script after pulling. + +cd "$(dirname "$0")/.." + +if [[ -f .env ]]; then + set -a + # shellcheck disable=SC1091 + source .env + set +a +fi + +if [[ -z "${BEENVOICE_IMAGE:-}" ]] && command -v git >/dev/null 2>&1; then + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + BEENVOICE_IMAGE="beenvoice:$(git rev-parse --short HEAD)" + export BEENVOICE_IMAGE + fi +fi + +BEENVOICE_IMAGE="${BEENVOICE_IMAGE:-beenvoice:local}" +export BEENVOICE_IMAGE + +echo "Deploying ${BEENVOICE_IMAGE} (docker compose up -d --build)..." +exec docker compose up -d --build "$@" diff --git a/apps/web/scripts/provision-demo-account.ts b/apps/web/scripts/provision-demo-account.ts new file mode 100644 index 0000000..7b03cb7 --- /dev/null +++ b/apps/web/scripts/provision-demo-account.ts @@ -0,0 +1,61 @@ +import "dotenv/config"; + +import bcrypt from "bcryptjs"; +import { Pool } from "pg"; + +const DEMO_USER_ID = "a0000000-0000-4000-8000-000000000001"; +const password = process.env.DEMO_ACCOUNT_PASSWORD?.trim(); +const databaseUrl = process.env.DATABASE_URL?.trim(); + +if (!databaseUrl) { + throw new Error("DATABASE_URL is required"); +} + +if (!password || password.length < 12) { + throw new Error("DEMO_ACCOUNT_PASSWORD must be at least 12 characters"); +} + +const pool = new Pool({ connectionString: databaseUrl, ssl: false }); + +try { + const passwordHash = await bcrypt.hash(password, 12); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + const userResult = await client.query( + `UPDATE "beenvoice_user" + SET "password" = $1, "updatedAt" = NOW() + WHERE "id" = $2`, + [passwordHash, DEMO_USER_ID], + ); + const accountResult = await client.query( + `UPDATE "beenvoice_account" + SET "password" = $1, "updatedAt" = NOW() + WHERE "userId" = $2 AND "providerId" = 'credential'`, + [passwordHash, DEMO_USER_ID], + ); + + if (userResult.rowCount !== 1 || accountResult.rowCount !== 1) { + throw new Error( + "Demo account is missing. Apply database migrations before provisioning it.", + ); + } + + await client.query(`DELETE FROM "beenvoice_session" WHERE "userId" = $1`, [ + DEMO_USER_ID, + ]); + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + + console.log( + "Demo review account provisioned; previous sessions were invalidated.", + ); +} finally { + await pool.end(); +} diff --git a/apps/web/scripts/setup-env.sh b/apps/web/scripts/setup-env.sh new file mode 100644 index 0000000..469f965 --- /dev/null +++ b/apps/web/scripts/setup-env.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." &>/dev/null && pwd)" +cd "${PROJECT_ROOT}" + +echo "[setup-env] Project root: ${PROJECT_ROOT}" + +ENV_EXAMPLE_FILE="${PROJECT_ROOT}/env.example" +ENV_FILE="${PROJECT_ROOT}/.env" + +FORCE=${FORCE:-false} + +if [[ ! -f "${ENV_EXAMPLE_FILE}" ]]; then + echo "[setup-env] ERROR: env.example not found at ${ENV_EXAMPLE_FILE}" >&2 + exit 1 +fi + +if [[ -f "${ENV_FILE}" && "${FORCE}" != "true" ]]; then + echo "[setup-env] .env already exists. Set FORCE=true to overwrite. Skipping." + exit 0 +fi + +echo "[setup-env] Generating secrets for .env" + +GEN_AUTH_SECRET=$(openssl rand -hex 32 2>/dev/null || cat /proc/sys/kernel/random/uuid) +GEN_DB_PASSWORD=$(openssl rand -hex 16 2>/dev/null || cat /proc/sys/kernel/random/uuid) + +TMP_FILE=$(mktemp) + +sed \ + -e "s/^AUTH_SECRET=__GENERATE__/AUTH_SECRET=${GEN_AUTH_SECRET}/" \ + -e "s/^POSTGRES_PASSWORD=__GENERATE__/POSTGRES_PASSWORD=${GEN_DB_PASSWORD}/" \ + "${ENV_EXAMPLE_FILE}" > "${TMP_FILE}" + +mv "${TMP_FILE}" "${ENV_FILE}" + +echo "[setup-env] Wrote ${ENV_FILE} with generated AUTH_SECRET and POSTGRES_PASSWORD" +echo "[setup-env] You can edit ${ENV_FILE} to adjust PORT, RESEND_* and other values." + +exit 0 + +#!/usr/bin/env bash + +set -euo pipefail + +# Resolve project root (directory containing this script's parent) +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." &>/dev/null && pwd)" +cd "${PROJECT_ROOT}" + +echo "[setup-env] Project root: ${PROJECT_ROOT}" + +ENV_EXAMPLE_FILE="${PROJECT_ROOT}/env.example" +ENV_FILE="${PROJECT_ROOT}/.env" + +FORCE=${FORCE:-false} + +if [[ ! -f "${ENV_EXAMPLE_FILE}" ]]; then + echo "[setup-env] ERROR: env.example not found at ${ENV_EXAMPLE_FILE}" >&2 + exit 1 +fi + +if [[ -f "${ENV_FILE}" && "${FORCE}" != "true" ]]; then + echo "[setup-env] .env already exists. Set FORCE=true to overwrite. Skipping." + exit 0 +fi + +echo "[setup-env] Generating secrets for .env" + +# Generate secrets +GEN_AUTH_SECRET=$(openssl rand -hex 32 2>/dev/null || cat /proc/sys/kernel/random/uuid) +GEN_DB_PASSWORD=$(openssl rand -hex 16 2>/dev/null || cat /proc/sys/kernel/random/uuid) + +TMP_FILE=$(mktemp) + +# Perform replacements +sed \ + -e "s/^AUTH_SECRET=__GENERATE__/AUTH_SECRET=${GEN_AUTH_SECRET}/" \ + -e "s/^POSTGRES_PASSWORD=__GENERATE__/POSTGRES_PASSWORD=${GEN_DB_PASSWORD}/" \ + "${ENV_EXAMPLE_FILE}" > "${TMP_FILE}" + +mv "${TMP_FILE}" "${ENV_FILE}" + +echo "[setup-env] Wrote ${ENV_FILE} with generated AUTH_SECRET and POSTGRES_PASSWORD" +echo "[setup-env] You can edit ${ENV_FILE} to adjust PORT, RESEND_* and other values." + +exit 0 + + diff --git a/apps/web/scripts/verify-drizzle-journal.ts b/apps/web/scripts/verify-drizzle-journal.ts new file mode 100644 index 0000000..ef1912a --- /dev/null +++ b/apps/web/scripts/verify-drizzle-journal.ts @@ -0,0 +1,85 @@ +/** + * Ensures every drizzle/*.sql migration file has a matching entry in meta/_journal.json. + * Run: bun scripts/verify-drizzle-journal.ts + */ +import { readdirSync, readFileSync } from "fs"; +import path from "path"; + +const drizzleDir = path.resolve(import.meta.dir, "../drizzle"); +const journalPath = path.join(drizzleDir, "meta/_journal.json"); + +const journal = JSON.parse(readFileSync(journalPath, "utf8")) as { + entries: Array<{ idx: number; tag: string; when: number }>; +}; + +const sqlTags = readdirSync(drizzleDir) + .filter((name) => /^\d+_.+\.sql$/.test(name)) + .map((name) => name.replace(/\.sql$/, "")) + .sort(); + +const journalTags = journal.entries.map((entry) => entry.tag).sort(); + +const missingFromJournal = sqlTags.filter((tag) => !journalTags.includes(tag)); +const missingSql = journalTags.filter((tag) => !sqlTags.includes(tag)); + +const idxSequence = journal.entries.map((entry) => entry.idx); +const expectedIdx = journal.entries.map((_, i) => i); +const badIdx = idxSequence.some((idx, i) => idx !== expectedIdx[i]); + +// drizzle's migrator gates on the single highest `when` already recorded in +// the target DB — it doesn't check hashes per-migration. If `when` values +// ever go non-increasing (e.g. a migration got deleted/renumbered after +// being applied to some environment), later entries can silently be skipped +// forever even though drizzle reports success. Keep this strictly increasing. +// +// 0008 and 0011 are known pre-existing exceptions from before this check +// existed (see git log on those files) — both already guard every statement +// with IF NOT EXISTS specifically because of this, so a skip is harmless. +// Don't add new exceptions here; fix the timestamp instead. +const KNOWN_NON_MONOTONIC_TAGS = new Set([ + "0008_payments_recurring_public_links", + "0011_time_entry_invoice_id", +]); +const nonMonotonic = journal.entries.some( + (entry, i) => + i > 0 && + entry.when <= journal.entries[i - 1]!.when && + !KNOWN_NON_MONOTONIC_TAGS.has(entry.tag), +); + +let failed = false; + +if (missingFromJournal.length > 0) { + console.error("[verify-drizzle-journal] SQL files missing from journal:"); + for (const tag of missingFromJournal) console.error(` - ${tag}`); + failed = true; +} + +if (missingSql.length > 0) { + console.error("[verify-drizzle-journal] Journal entries without SQL files:"); + for (const tag of missingSql) console.error(` - ${tag}`); + failed = true; +} + +if (badIdx) { + console.error("[verify-drizzle-journal] Journal idx values are not sequential from 0"); + failed = true; +} + +if (nonMonotonic) { + console.error( + "[verify-drizzle-journal] Journal `when` timestamps are not strictly increasing. " + + "drizzle-orm's migrator only compares against the single highest `when` already " + + "applied in the target DB, so a lower or equal value here can cause migrations to " + + "be silently skipped on databases that already ran a later timestamp.", + ); + failed = true; +} + +if (failed) { + process.exit(1); +} + +console.log( + `[verify-drizzle-journal] OK — ${sqlTags.length} migrations match journal entries`, +); diff --git a/apps/web/src/app/(legal)/layout.tsx b/apps/web/src/app/(legal)/layout.tsx new file mode 100644 index 0000000..b9f6005 --- /dev/null +++ b/apps/web/src/app/(legal)/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from "next"; + +import { MarketingProviders } from "~/components/providers/marketing-providers"; +import { brand } from "~/lib/branding"; + +export const metadata: Metadata = { + title: { + template: `%s | ${brand.name}`, + default: `Legal | ${brand.name}`, + }, +}; + +export default function LegalLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/apps/web/src/app/(legal)/privacy/page.tsx b/apps/web/src/app/(legal)/privacy/page.tsx new file mode 100644 index 0000000..024c3a5 --- /dev/null +++ b/apps/web/src/app/(legal)/privacy/page.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from "next"; + +import { PrivacyPolicyContent } from "~/components/legal/privacy-policy-content"; +import { LegalPageShell } from "~/components/legal/legal-page-shell"; +import { brand } from "~/lib/branding"; + +export const metadata: Metadata = { + title: `Privacy Policy | ${brand.name}`, + description: `How ${brand.name} collects, uses, and protects your data.`, +}; + +export default function PrivacyPolicyPage() { + return ( + + + + ); +} diff --git a/apps/web/src/app/(legal)/terms/page.tsx b/apps/web/src/app/(legal)/terms/page.tsx new file mode 100644 index 0000000..0cb133d --- /dev/null +++ b/apps/web/src/app/(legal)/terms/page.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from "next"; + +import { LegalPageShell } from "~/components/legal/legal-page-shell"; +import { TermsOfServiceContent } from "~/components/legal/terms-of-service-content"; +import { brand } from "~/lib/branding"; + +export const metadata: Metadata = { + title: `Terms of Service | ${brand.name}`, + description: `Terms governing your use of the ${brand.name} platform.`, +}; + +export default function TermsOfServicePage() { + return ( + + + + ); +} diff --git a/apps/web/src/app/(marketing)/layout.tsx b/apps/web/src/app/(marketing)/layout.tsx new file mode 100644 index 0000000..18ceb12 --- /dev/null +++ b/apps/web/src/app/(marketing)/layout.tsx @@ -0,0 +1,9 @@ +import { MarketingProviders } from "~/components/providers/marketing-providers"; + +export default function MarketingLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/apps/web/src/app/(marketing)/page.tsx b/apps/web/src/app/(marketing)/page.tsx new file mode 100644 index 0000000..4b8e5fa --- /dev/null +++ b/apps/web/src/app/(marketing)/page.tsx @@ -0,0 +1,14 @@ +import { LandingPage } from "~/components/marketing/landing-page"; +import { env } from "~/env"; + +export const dynamic = "force-dynamic"; + +export default function HomePage() { + const allowRegistration = env.DISABLE_SIGNUPS !== true; + + return ( +
+ +
+ ); +} diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..2fe2da0 --- /dev/null +++ b/apps/web/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { toNextJsHandler } from "better-auth/next-js"; +import { auth } from "~/lib/auth"; + +export const { GET, POST } = toNextJsHandler(auth); diff --git a/apps/web/src/app/api/auth/capabilities/route.ts b/apps/web/src/app/api/auth/capabilities/route.ts new file mode 100644 index 0000000..20e06c6 --- /dev/null +++ b/apps/web/src/app/api/auth/capabilities/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from "next/server"; + +import { env } from "~/env"; + +export function GET() { + return NextResponse.json({ + authentik: env.NEXT_PUBLIC_AUTHENTIK_ENABLED === true, + signupsDisabled: env.DISABLE_SIGNUPS === true, + }); +} diff --git a/apps/web/src/app/api/auth/forgot-password/route.ts b/apps/web/src/app/api/auth/forgot-password/route.ts new file mode 100644 index 0000000..88bb317 --- /dev/null +++ b/apps/web/src/app/api/auth/forgot-password/route.ts @@ -0,0 +1,73 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { eq } from "drizzle-orm"; +import { db } from "~/server/db"; +import { users } from "~/server/db/schema"; +import { sendPasswordResetForUser } from "~/lib/password-reset"; +import { rateLimitKey, requireRateLimit } from "~/lib/rate-limit"; + +export async function POST(request: NextRequest) { + try { + const { email } = (await request.json()) as { email: string }; + + if (!email || typeof email !== "string") { + return NextResponse.json({ error: "Email is required" }, { status: 400 }); + } + + const normalizedEmail = email.toLowerCase().trim(); + const ipRateLimit = requireRateLimit(rateLimitKey(request, "auth:forgot"), { + windowMs: 60 * 60 * 1000, + max: 10, + }); + if (ipRateLimit) return ipRateLimit; + + const emailRateLimit = requireRateLimit( + rateLimitKey(request, "auth:forgot-email", normalizedEmail), + { + windowMs: 60 * 60 * 1000, + max: 3, + }, + ); + if (emailRateLimit) return emailRateLimit; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(normalizedEmail)) { + return NextResponse.json( + { error: "Invalid email format" }, + { status: 400 }, + ); + } + + const user = await db.query.users.findFirst({ + where: eq(users.email, normalizedEmail), + columns: { id: true }, + }); + + if (!user) { + return NextResponse.json( + { + success: true, + message: + "If an account with that email exists, password reset instructions have been sent.", + }, + { status: 200 }, + ); + } + + await sendPasswordResetForUser(user.id); + + return NextResponse.json( + { + success: true, + message: + "If an account with that email exists, password reset instructions have been sent.", + }, + { status: 200 }, + ); + } catch (error) { + console.error("Password reset error:", error); + return NextResponse.json( + { error: "An error occurred while processing your request" }, + { status: 500 }, + ); + } +} diff --git a/apps/web/src/app/api/auth/register/route.ts b/apps/web/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..268ae00 --- /dev/null +++ b/apps/web/src/app/api/auth/register/route.ts @@ -0,0 +1,196 @@ +import bcrypt from "bcryptjs"; +import { eq } from "drizzle-orm"; +import { type NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { auth } from "~/lib/auth"; +import { getDatabaseSetupErrorMessage } from "~/lib/db-errors"; +import { resolveNewUserRole } from "~/lib/first-admin"; +import { rateLimitKey, requireRateLimit } from "~/lib/rate-limit"; +import { env } from "~/env"; +import { db } from "~/server/db"; +import { accounts, users } from "~/server/db/schema"; + +const registerSchema = z + .object({ + firstName: z.string().trim().min(1, "First name is required"), + lastName: z.string().trim().min(1, "Last name is required"), + name: z.string().trim().optional(), + email: z.string().email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + }) + .transform((data) => { + if (data.name?.length) { + const parts = data.name.trim().split(/\s+/); + const firstName = parts[0] ?? ""; + const lastName = parts.slice(1).join(" ") || firstName; + return { + firstName, + lastName, + email: data.email, + password: data.password, + }; + } + + return { + firstName: data.firstName, + lastName: data.lastName, + email: data.email, + password: data.password, + }; + }); + +const fieldLabels: Record = { + firstName: "First name", + lastName: "Last name", + name: "Name", + email: "Email address", + password: "Password", +}; + +function formatRegisterError(error: z.ZodError): string { + const issue = error.issues[0] ?? error.errors[0]; + if (!issue) return "Please check the registration form"; + + const field = issue.path[0]; + const label = + typeof field === "string" ? (fieldLabels[field] ?? field) : "Field"; + + if ( + issue.code === "invalid_type" && + "received" in issue && + issue.received === "undefined" + ) { + return `${label} is required`; + } + + if (issue.message && issue.message !== "Required") { + return issue.message; + } + + return `${label} is required`; +} + +export async function POST(request: NextRequest) { + try { + const rateLimit = requireRateLimit(rateLimitKey(request, "auth:register"), { + windowMs: 60 * 60 * 1000, + max: 5, + }); + if (rateLimit) return rateLimit; + + if (env.DISABLE_SIGNUPS === true) { + return NextResponse.json( + { error: "New account registration is currently disabled" }, + { status: 403 }, + ); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json( + { error: "Invalid request body. Please try again." }, + { status: 400 }, + ); + } + + if (!body || typeof body !== "object") { + return NextResponse.json( + { error: "Registration details are required" }, + { status: 400 }, + ); + } + + const parsed = registerSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: formatRegisterError(parsed.error) }, + { status: 400 }, + ); + } + + const { firstName, lastName, email, password } = parsed.data; + const normalizedEmail = email.toLowerCase(); + + const emailRateLimit = requireRateLimit( + rateLimitKey(request, "auth:register-email", normalizedEmail), + { + windowMs: 60 * 60 * 1000, + max: 3, + }, + ); + if (emailRateLimit) return emailRateLimit; + + const existingUser = await db.query.users.findFirst({ + where: eq(users.email, normalizedEmail), + }); + + if (existingUser) { + return NextResponse.json( + { error: "Registration failed. Please check the form or sign in." }, + { status: 400 }, + ); + } + + const hashedPassword = await bcrypt.hash(password, 12); + + await db.transaction(async (tx) => { + const role = await resolveNewUserRole(tx); + + const [user] = await tx + .insert(users) + .values({ + name: `${firstName} ${lastName}`, + email: normalizedEmail, + password: hashedPassword, + role, + }) + .returning({ id: users.id }); + + if (!user) { + throw new Error("Failed to create user"); + } + + await tx.insert(accounts).values({ + userId: user.id, + accountId: user.id, + providerId: "credential", + password: hashedPassword, + }); + }); + + try { + await auth.api.signInEmail({ + body: { + email: normalizedEmail, + password, + }, + headers: request.headers, + }); + } catch (signInError) { + console.error("Post-register sign-in failed:", signInError); + return NextResponse.json( + { message: "User created successfully", signInRequired: true }, + { status: 201 }, + ); + } + + return NextResponse.json( + { message: "User created successfully" }, + { status: 201 }, + ); + } catch (error) { + console.error("Registration error:", error); + + const databaseSetupError = getDatabaseSetupErrorMessage(error); + if (databaseSetupError) { + return NextResponse.json({ error: databaseSetupError }, { status: 503 }); + } + + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/apps/web/src/app/api/auth/reset-password/route.ts b/apps/web/src/app/api/auth/reset-password/route.ts new file mode 100644 index 0000000..25c5606 --- /dev/null +++ b/apps/web/src/app/api/auth/reset-password/route.ts @@ -0,0 +1,121 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { eq, and, gt } from "drizzle-orm"; +import bcrypt from "bcryptjs"; +import { hashPasswordResetToken } from "~/lib/reset-token"; +import { revokeUserSessions } from "~/lib/session-security"; +import { rateLimitKey, requireRateLimit } from "~/lib/rate-limit"; +import { db } from "~/server/db"; +import { accounts, users } from "~/server/db/schema"; + +export async function POST(request: NextRequest) { + try { + const ipRateLimit = requireRateLimit(rateLimitKey(request, "auth:reset"), { + windowMs: 60 * 1000, + max: 10, + }); + if (ipRateLimit) return ipRateLimit; + + const { token, password } = (await request.json()) as { + token: string; + password: string; + }; + + if (!token || typeof token !== "string") { + return NextResponse.json({ error: "Token is required" }, { status: 400 }); + } + + if (!password || typeof password !== "string") { + return NextResponse.json( + { error: "Password is required" }, + { status: 400 }, + ); + } + + if (password.length < 8) { + return NextResponse.json( + { error: "Password must be at least 8 characters long" }, + { status: 400 }, + ); + } + + const tokenRateLimit = requireRateLimit( + rateLimitKey(request, "auth:reset-token", token), + { + windowMs: 60 * 60 * 1000, + max: 5, + }, + ); + if (tokenRateLimit) return tokenRateLimit; + + const tokenHash = hashPasswordResetToken(token); + + // Find user with valid reset token that hasn't expired + const user = await db.query.users.findFirst({ + where: and( + eq(users.resetToken, tokenHash), + gt(users.resetTokenExpiry, new Date()), + ), + }); + + if (!user) { + return NextResponse.json( + { error: "Invalid or expired token" }, + { status: 400 }, + ); + } + + // Hash the new password + const hashedPassword = await bcrypt.hash(password, 12); + + await db.transaction(async (tx) => { + await tx + .update(users) + .set({ + password: hashedPassword, + resetToken: null, + resetTokenExpiry: null, + }) + .where(eq(users.id, user.id)); + + const credentialAccount = await tx.query.accounts.findFirst({ + where: and( + eq(accounts.userId, user.id), + eq(accounts.providerId, "credential"), + ), + }); + + if (credentialAccount) { + await tx + .update(accounts) + .set({ + password: hashedPassword, + updatedAt: new Date(), + }) + .where(eq(accounts.id, credentialAccount.id)); + } else { + await tx.insert(accounts).values({ + userId: user.id, + accountId: user.id, + providerId: "credential", + password: hashedPassword, + }); + } + }); + + await revokeUserSessions(user.id); + + return NextResponse.json( + { + success: true, + message: "Password has been reset successfully", + }, + { status: 200 }, + ); + } catch (error) { + console.error("Password reset error:", error); + return NextResponse.json( + { error: "An error occurred while resetting your password" }, + { status: 500 }, + ); + } +} diff --git a/apps/web/src/app/api/auth/validate-reset-token/route.ts b/apps/web/src/app/api/auth/validate-reset-token/route.ts new file mode 100644 index 0000000..4afbc4d --- /dev/null +++ b/apps/web/src/app/api/auth/validate-reset-token/route.ts @@ -0,0 +1,56 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { eq, and, gt } from "drizzle-orm"; +import { hashPasswordResetToken } from "~/lib/reset-token"; +import { rateLimitKey, requireRateLimit } from "~/lib/rate-limit"; +import { db } from "~/server/db"; +import { users } from "~/server/db/schema"; + +export async function POST(request: NextRequest) { + try { + const ipRateLimit = requireRateLimit(rateLimitKey(request, "auth:validate-reset"), { + windowMs: 60 * 1000, + max: 20, + }); + if (ipRateLimit) return ipRateLimit; + + const { token } = (await request.json()) as { token: string }; + + if (!token || typeof token !== "string") { + return NextResponse.json({ error: "Token is required" }, { status: 400 }); + } + + const tokenRateLimit = requireRateLimit( + rateLimitKey(request, "auth:validate-reset-token", token), + { + windowMs: 60 * 60 * 1000, + max: 5, + }, + ); + if (tokenRateLimit) return tokenRateLimit; + + const tokenHash = hashPasswordResetToken(token); + + // Find user with valid reset token that hasn't expired + const user = await db.query.users.findFirst({ + where: and( + eq(users.resetToken, tokenHash), + gt(users.resetTokenExpiry, new Date()), + ), + }); + + if (!user) { + return NextResponse.json( + { error: "Invalid or expired token" }, + { status: 400 }, + ); + } + + return NextResponse.json({ valid: true }, { status: 200 }); + } catch (error) { + console.error("Token validation error:", error); + return NextResponse.json( + { error: "An error occurred while validating the token" }, + { status: 500 }, + ); + } +} diff --git a/apps/web/src/app/api/business-logo/[businessId]/route.ts b/apps/web/src/app/api/business-logo/[businessId]/route.ts new file mode 100644 index 0000000..dcb0798 --- /dev/null +++ b/apps/web/src/app/api/business-logo/[businessId]/route.ts @@ -0,0 +1,78 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { eq } from "drizzle-orm"; +import { getObject } from "~/lib/object-storage"; +import { db } from "~/server/db"; +import { businesses } from "~/server/db/schema"; + +export const runtime = "nodejs"; + +const RASTERIZABLE_MIME_TYPES = new Set(["image/svg+xml", "image/webp"]); + +// Intentionally unauthenticated: a business logo must be viewable on public, +// token-based invoice pages without a session. Business IDs are random +// UUIDs, so this only serves images to callers who already know the ID. +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ businessId: string }> }, +) { + const { businessId } = await params; + const business = await db.query.businesses.findFirst({ + where: eq(businesses.id, businessId), + columns: { logoStorageKey: true, logoMimeType: true }, + }); + + if (!business?.logoStorageKey || !business.logoMimeType) { + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + + // @react-pdf/renderer's Image component only decodes PNG/JPEG, so PDF + // generation requests a rasterized copy of SVG/WebP logos via this param. + const wantsPng = + new URL(req.url).searchParams.get("format") === "png" && + RASTERIZABLE_MIME_TYPES.has(business.logoMimeType); + + try { + const body = await getObject(business.logoStorageKey); + + if (wantsPng) { + const { default: sharp } = await import("sharp"); + const isSvg = business.logoMimeType === "image/svg+xml"; + // SVG is vector: rasterize at a high density so the PNG stays crisp at + // the size it's actually displayed (PDF header, up to ~2.2in wide). + // withoutEnlargement only makes sense for the WebP (already-raster) + // case — for SVG it would cap us at whatever tiny canvas the source's + // intrinsic viewBox implies, even though the vector has no such limit. + const png = await sharp(body, isSvg ? { density: 600 } : undefined) + .resize({ + width: 1024, + height: 1024, + fit: "inside", + withoutEnlargement: !isSvg, + }) + .png() + .toBuffer(); + return new NextResponse(new Uint8Array(png), { + headers: { + "Content-Type": "image/png", + "Cache-Control": "public, max-age=300, must-revalidate", + "X-Content-Type-Options": "nosniff", + }, + }); + } + + return new NextResponse(new Uint8Array(body), { + headers: { + "Content-Type": business.logoMimeType, + "Cache-Control": "public, max-age=300, must-revalidate", + "X-Content-Type-Options": "nosniff", + }, + }); + } catch (error) { + console.error("[business-logo] Failed to serve logo", { + backendError: error, + businessId, + wantsPng, + }); + return NextResponse.json({ error: "Logo not found" }, { status: 404 }); + } +} diff --git a/apps/web/src/app/api/cron/generate-recurring/route.ts b/apps/web/src/app/api/cron/generate-recurring/route.ts new file mode 100644 index 0000000..ea95527 --- /dev/null +++ b/apps/web/src/app/api/cron/generate-recurring/route.ts @@ -0,0 +1,23 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { env } from "~/env"; +import { db } from "~/server/db"; +import { generateDueRecurringInvoices } from "~/server/api/routers/recurring-invoices"; + +export async function POST(req: NextRequest) { + const authHeader = req.headers.get("authorization"); + const secret = env.CRON_SECRET; + + if (!secret) { + return NextResponse.json( + { error: "Cron secret is not configured" }, + { status: 500 }, + ); + } + + if (authHeader !== `Bearer ${secret}`) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const generated = await generateDueRecurringInvoices(db); + return NextResponse.json({ generated }); +} diff --git a/apps/web/src/app/api/i/[token]/pdf/route.ts b/apps/web/src/app/api/i/[token]/pdf/route.ts new file mode 100644 index 0000000..bde6160 --- /dev/null +++ b/apps/web/src/app/api/i/[token]/pdf/route.ts @@ -0,0 +1,95 @@ +import { NextResponse } from "next/server"; +import { eq } from "drizzle-orm"; +import { db } from "~/server/db"; +import { invoices, platformSettings } from "~/server/db/schema"; +import { generateInvoicePDFBlob } from "~/lib/pdf-export"; + +export const runtime = "nodejs"; + +export async function GET( + request: Request, + { params }: { params: Promise<{ token: string }> }, +) { + const { token } = await params; + + const invoice = await db.query.invoices.findFirst({ + where: eq(invoices.publicToken, token), + with: { + client: true, + // Explicit allowlist: token-based public route — never fetch + // secret fields (resendApiKey, resendDomain) for an unauthenticated request. + business: { + columns: { + id: true, + name: true, + nickname: true, + email: true, + phone: true, + addressLine1: true, + addressLine2: true, + city: true, + state: true, + postalCode: true, + country: true, + website: true, + taxId: true, + logoStorageKey: true, + logoMimeType: true, + hideNameWithLogo: true, + }, + }, + items: { + orderBy: (i, { asc }) => [ + asc(i.date), + asc(i.position), + asc(i.createdAt), + ], + }, + }, + }); + + if (!invoice) { + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + + if (invoice.publicTokenExpiresAt && new Date(invoice.publicTokenExpiresAt) < new Date()) { + return NextResponse.json({ error: "This link has expired" }, { status: 410 }); + } + + const settings = await db.query.platformSettings.findFirst({ + where: eq(platformSettings.id, "global"), + }); + + const pdfBlob = await generateInvoicePDFBlob( + invoice, + { + pdfTemplate: settings?.pdfTemplate as "classic" | "minimal" | undefined, + pdfAccentColor: settings?.pdfAccentColor, + pdfFontFamily: settings?.pdfFontFamily as + | "sans" + | "serif" + | "mono" + | undefined, + pdfNumericFontFamily: settings?.pdfNumericFontFamily as + | "sans" + | "serif" + | "mono" + | undefined, + pdfFooterText: settings?.pdfFooterText, + pdfShowLogo: settings?.pdfShowLogo, + pdfShowPageNumbers: settings?.pdfShowPageNumbers, + }, + { logoBaseUrl: new URL(request.url).origin }, + ); + + const buffer = await pdfBlob.arrayBuffer(); + const filename = `invoice-${invoice.invoiceNumber}.pdf`; + + return new Response(buffer, { + headers: { + "Content-Type": "application/pdf", + "Content-Disposition": `inline; filename="${filename}"`, + "Cache-Control": "private, no-store", + }, + }); +} diff --git a/apps/web/src/app/api/mcp/route.ts b/apps/web/src/app/api/mcp/route.ts new file mode 100644 index 0000000..f5f9063 --- /dev/null +++ b/apps/web/src/app/api/mcp/route.ts @@ -0,0 +1,1122 @@ +import { TRPCError } from "@trpc/server"; +import { z, type ZodType } from "zod"; + +import { createCaller } from "~/server/api/root"; +import { createTRPCContext } from "~/server/api/trpc"; +import { getAppUrl } from "~/lib/app-url"; + +export const runtime = "nodejs"; + +type JsonRpcId = string | number | null; +type ToolResult = { + content: Array<{ type: "text"; text: string }>; +}; +type McpCaller = ReturnType; + +const dateString = z.string().min(1); +const emptyableString = z.string().optional().or(z.literal("")); +const invoiceStatus = z.enum(["draft", "sent", "paid"]); +const paymentMethod = z.enum([ + "cash", + "check", + "bank_transfer", + "credit_card", + "paypal", + "other", +]); + +const invoiceItemSchema = z.object({ + date: dateString, + description: z.string().min(1), + hours: z.number().min(0), + rate: z.number().min(0), +}); + +const clientCreateSchema = z.object({ + name: z.string().min(1).max(255), + email: z.string().email().optional().or(z.literal("")), + phone: z.string().max(50).optional().or(z.literal("")), + addressLine1: z.string().max(255).optional().or(z.literal("")), + addressLine2: z.string().max(255).optional().or(z.literal("")), + city: z.string().max(100).optional().or(z.literal("")), + state: z.string().max(50).optional().or(z.literal("")), + postalCode: z.string().max(20).optional().or(z.literal("")), + country: z.string().max(100).optional().or(z.literal("")), + defaultHourlyRate: z.number().min(0).optional(), + currency: z.string().length(3).optional(), +}); + +const businessCreateSchema = z.object({ + name: z.string().min(1).max(255), + nickname: emptyableString, + email: z.string().email().optional().or(z.literal("")), + phone: emptyableString, + addressLine1: emptyableString, + addressLine2: emptyableString, + city: emptyableString, + state: emptyableString, + postalCode: emptyableString, + country: emptyableString, + website: z.string().url().optional().or(z.literal("")), + taxId: emptyableString, + logoUrl: emptyableString, + isDefault: z.boolean().default(false), +}); + +const invoiceCreateSchema = z.object({ + invoiceNumber: z.string().min(1), + invoicePrefix: z.string().optional(), + businessId: emptyableString, + clientId: z.string().min(1), + issueDate: dateString, + dueDate: dateString, + status: invoiceStatus.default("draft"), + notes: emptyableString, + emailMessage: emptyableString, + taxRate: z.number().min(0).max(100).default(0), + currency: z.string().length(3).default("USD"), + items: z.array(invoiceItemSchema).min(1), +}); + +const invoiceUpdateSchema = invoiceCreateSchema.partial().extend({ + id: z.string(), +}); + +const expenseCreateSchema = z.object({ + date: dateString, + description: z.string().min(1), + amount: z.number().min(0), + currency: z.string().length(3).default("USD"), + category: z.string().optional().or(z.literal("")), + billable: z.boolean().default(false), + reimbursable: z.boolean().default(false), + taxDeductible: z.boolean().default(false), + notes: z.string().optional().or(z.literal("")), + clientId: z.string().optional().or(z.literal("")), + businessId: z.string().optional().or(z.literal("")), + invoiceId: z.string().optional().or(z.literal("")), +}); + +const expenseUpdateSchema = expenseCreateSchema.partial().extend({ id: z.string() }); + +const recurringItemSchema = z.object({ + description: z.string().min(1), + hours: z.number().min(0), + rate: z.number().min(0), + position: z.number().int().default(0), +}); + +const recurringCreateSchema = z.object({ + name: z.string().min(1).max(255), + clientId: z.string().min(1), + businessId: z.string().optional().or(z.literal("")), + schedule: z.enum(["weekly", "biweekly", "monthly", "quarterly", "yearly"]), + invoicePrefix: z.string().optional().default("#"), + taxRate: z.number().min(0).max(100).default(0), + currency: z.string().length(3).default("USD"), + notes: z.string().optional().or(z.literal("")), + emailMessage: z.string().optional().or(z.literal("")), + items: z.array(recurringItemSchema).min(1), +}); + +const recurringUpdateSchema = recurringCreateSchema.extend({ id: z.string() }); + +const jsonSchemas = { + empty: { type: "object", properties: {}, additionalProperties: false }, + id: { + type: "object", + properties: { id: { type: "string" } }, + required: ["id"], + additionalProperties: false, + }, + invoiceId: { + type: "object", + properties: { invoiceId: { type: "string" } }, + required: ["invoiceId"], + additionalProperties: false, + }, + invoiceStatus: { + type: "object", + properties: { + id: { type: "string" }, + status: { type: "string", enum: ["draft", "sent", "paid"] }, + }, + required: ["id", "status"], + additionalProperties: false, + }, + paymentCreate: { + type: "object", + properties: { + invoiceId: { type: "string" }, + amount: { type: "number", exclusiveMinimum: 0 }, + date: { type: "string", format: "date-time" }, + method: { + type: "string", + enum: ["cash", "check", "bank_transfer", "credit_card", "paypal", "other"], + }, + notes: { type: "string", maxLength: 500 }, + }, + required: ["invoiceId", "amount", "date"], + additionalProperties: false, + }, + clientCreate: { + type: "object", + properties: { + name: { type: "string", minLength: 1, maxLength: 255 }, + email: { type: "string" }, + phone: { type: "string", maxLength: 50 }, + addressLine1: { type: "string", maxLength: 255 }, + addressLine2: { type: "string", maxLength: 255 }, + city: { type: "string", maxLength: 100 }, + state: { type: "string", maxLength: 50 }, + postalCode: { type: "string", maxLength: 20 }, + country: { type: "string", maxLength: 100 }, + defaultHourlyRate: { type: "number", minimum: 0 }, + currency: { type: "string", minLength: 3, maxLength: 3 }, + }, + required: ["name"], + additionalProperties: false, + }, + invoiceCreate: { + type: "object", + properties: { + invoiceNumber: { type: "string", minLength: 1 }, + invoicePrefix: { type: "string" }, + businessId: { type: "string" }, + clientId: { type: "string", minLength: 1 }, + issueDate: { type: "string", format: "date-time" }, + dueDate: { type: "string", format: "date-time" }, + status: { type: "string", enum: ["draft", "sent", "paid"] }, + notes: { type: "string" }, + emailMessage: { type: "string" }, + taxRate: { type: "number", minimum: 0, maximum: 100 }, + currency: { type: "string", minLength: 3, maxLength: 3 }, + items: { + type: "array", + minItems: 1, + items: { + type: "object", + properties: { + date: { type: "string", format: "date-time" }, + description: { type: "string", minLength: 1 }, + hours: { type: "number", minimum: 0 }, + rate: { type: "number", minimum: 0 }, + }, + required: ["date", "description", "hours", "rate"], + additionalProperties: false, + }, + }, + }, + required: ["invoiceNumber", "clientId", "issueDate", "dueDate", "items"], + additionalProperties: false, + }, + businessCreate: { + type: "object", + properties: { + name: { type: "string", minLength: 1, maxLength: 255 }, + nickname: { type: "string", maxLength: 255 }, + email: { type: "string" }, + phone: { type: "string" }, + addressLine1: { type: "string" }, + addressLine2: { type: "string" }, + city: { type: "string" }, + state: { type: "string" }, + postalCode: { type: "string" }, + country: { type: "string" }, + website: { type: "string" }, + taxId: { type: "string" }, + logoUrl: { type: "string" }, + isDefault: { type: "boolean" }, + }, + required: ["name"], + additionalProperties: false, + }, + expenseCreate: { + type: "object", + properties: { + date: { type: "string", format: "date-time" }, + description: { type: "string", minLength: 1 }, + amount: { type: "number", minimum: 0 }, + currency: { type: "string", minLength: 3, maxLength: 3 }, + category: { type: "string", enum: ["Travel", "Meals & Entertainment", "Software & Subscriptions", "Hardware & Equipment", "Office Supplies", "Marketing", "Professional Services", "Utilities", "Other"] }, + billable: { type: "boolean" }, + reimbursable: { type: "boolean" }, + taxDeductible: { type: "boolean" }, + notes: { type: "string", maxLength: 500 }, + clientId: { type: "string" }, + businessId: { type: "string" }, + invoiceId: { type: "string" }, + }, + required: ["date", "description", "amount"], + additionalProperties: false, + }, + recurringItem: { + type: "object", + properties: { + description: { type: "string", minLength: 1 }, + hours: { type: "number", minimum: 0 }, + rate: { type: "number", minimum: 0 }, + position: { type: "integer" }, + }, + required: ["description", "hours", "rate"], + additionalProperties: false, + }, + recurringCreate: { + type: "object", + properties: { + name: { type: "string", minLength: 1, maxLength: 255 }, + clientId: { type: "string", minLength: 1 }, + businessId: { type: "string" }, + schedule: { type: "string", enum: ["weekly", "biweekly", "monthly", "quarterly", "yearly"] }, + invoicePrefix: { type: "string" }, + taxRate: { type: "number", minimum: 0, maximum: 100 }, + currency: { type: "string", minLength: 3, maxLength: 3 }, + notes: { type: "string" }, + emailMessage: { type: "string" }, + items: { + type: "array", + minItems: 1, + items: { + type: "object", + properties: { + description: { type: "string", minLength: 1 }, + hours: { type: "number", minimum: 0 }, + rate: { type: "number", minimum: 0 }, + position: { type: "integer" }, + }, + required: ["description", "hours", "rate"], + additionalProperties: false, + }, + }, + }, + required: ["name", "clientId", "schedule", "items"], + additionalProperties: false, + }, + invoiceSend: { + type: "object", + properties: { + invoiceId: { type: "string" }, + customSubject: { type: "string" }, + customMessage: { type: "string" }, + ccEmails: { type: "string", description: "Comma-separated CC email addresses" }, + bccEmails: { type: "string", description: "Comma-separated BCC email addresses" }, + }, + required: ["invoiceId"], + additionalProperties: false, + }, + bulkIds: { + type: "object", + properties: { ids: { type: "array", items: { type: "string" }, minItems: 1 } }, + required: ["ids"], + additionalProperties: false, + }, + bulkStatus: { + type: "object", + properties: { + ids: { type: "array", items: { type: "string" }, minItems: 1 }, + status: { type: "string", enum: ["draft", "sent", "paid"] }, + }, + required: ["ids", "status"], + additionalProperties: false, + }, +} as const; + +type ToolDefinition = { + description: string; + inputSchema: Record; + schema: ZodType; + handler: (input: unknown, caller: McpCaller) => Promise; +}; + +function defineTool(tool: { + description: string; + inputSchema: Record; + schema: ZodType; + handler: (input: TInput, caller: McpCaller) => Promise; +}): ToolDefinition { + return { + description: tool.description, + inputSchema: tool.inputSchema, + schema: tool.schema, + handler: async (input, caller) => tool.handler(input as TInput, caller), + }; +} + +function parseDate(value: string, fieldName: string) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `${fieldName} must be a valid ISO date string`, + }); + } + return date; +} + +function parseInvoiceItems(items: z.infer[]) { + return items.map((item) => ({ + ...item, + date: parseDate(item.date, "item.date"), + })); +} + +function textResult(data: unknown): ToolResult { + return { + content: [{ type: "text", text: JSON.stringify(data ?? null, null, 2) }], + }; +} + +const tools = { + invoices_list: defineTool({ + description: "List invoices for the authenticated user. Optionally filter by status ('draft', 'sent', or 'paid') and/or clientId.", + inputSchema: { + type: "object", + properties: { + status: { type: "string", enum: ["draft", "sent", "paid"], description: "Filter by invoice status" }, + clientId: { type: "string", description: "Filter by client ID" }, + }, + additionalProperties: false, + }, + schema: z.object({ + status: z.enum(["draft", "sent", "paid"]).optional(), + clientId: z.string().optional(), + }).optional().default({}), + handler: async (input, caller) => caller.invoices.getAll(input ?? {}), + }), + invoices_get: defineTool({ + description: "Get one invoice by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.invoices.getById(input), + }), + invoices_create: defineTool({ + description: "Create an invoice with line items.", + inputSchema: jsonSchemas.invoiceCreate, + schema: invoiceCreateSchema, + handler: async (input, caller) => + caller.invoices.create({ + ...input, + issueDate: parseDate(input.issueDate, "issueDate"), + dueDate: parseDate(input.dueDate, "dueDate"), + items: parseInvoiceItems(input.items), + }), + }), + invoices_update: defineTool({ + description: "Update invoice fields and optionally replace line items.", + inputSchema: { + ...jsonSchemas.invoiceCreate, + required: ["id"], + properties: { + id: { type: "string" }, + ...jsonSchemas.invoiceCreate.properties, + }, + }, + schema: invoiceUpdateSchema, + handler: async (input, caller) => + caller.invoices.update({ + ...input, + issueDate: input.issueDate + ? parseDate(input.issueDate, "issueDate") + : undefined, + dueDate: input.dueDate ? parseDate(input.dueDate, "dueDate") : undefined, + items: input.items ? parseInvoiceItems(input.items) : undefined, + }), + }), + invoices_update_status: defineTool({ + description: "Update an invoice status to draft, sent, or paid.", + inputSchema: jsonSchemas.invoiceStatus, + schema: z.object({ id: z.string(), status: invoiceStatus }), + handler: async (input, caller) => caller.invoices.updateStatus(input), + }), + invoices_delete: defineTool({ + description: "Delete an invoice by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.invoices.delete(input), + }), + payments_list_for_invoice: defineTool({ + description: "List payments recorded for an invoice.", + inputSchema: jsonSchemas.invoiceId, + schema: z.object({ invoiceId: z.string() }), + handler: async (input, caller) => caller.payments.getByInvoice(input), + }), + payments_create: defineTool({ + description: "Record a payment for an invoice.", + inputSchema: jsonSchemas.paymentCreate, + schema: z.object({ + invoiceId: z.string(), + amount: z.number().positive(), + date: dateString, + method: paymentMethod.default("other"), + notes: z.string().max(500).optional(), + }), + handler: async (input, caller) => + caller.payments.create({ + ...input, + date: parseDate(input.date, "date"), + }), + }), + payments_delete: defineTool({ + description: "Delete a payment by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.payments.delete(input), + }), + clients_list: defineTool({ + description: "List clients for the authenticated beenvoice user.", + inputSchema: jsonSchemas.empty, + schema: z.object({}).optional().default({}), + handler: async (_input, caller) => caller.clients.getAll(), + }), + clients_get: defineTool({ + description: "Get one client by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.clients.getById(input), + }), + clients_create: defineTool({ + description: "Create a client.", + inputSchema: jsonSchemas.clientCreate, + schema: clientCreateSchema, + handler: async (input, caller) => caller.clients.create(input), + }), + clients_update: defineTool({ + description: "Update a client.", + inputSchema: { + ...jsonSchemas.clientCreate, + required: ["id"], + properties: { id: { type: "string" }, ...jsonSchemas.clientCreate.properties }, + }, + schema: clientCreateSchema.partial().extend({ id: z.string() }), + handler: async (input, caller) => caller.clients.update(input), + }), + clients_delete: defineTool({ + description: "Delete a client by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.clients.delete(input), + }), + businesses_list: defineTool({ + description: "List businesses for the authenticated beenvoice user.", + inputSchema: jsonSchemas.empty, + schema: z.object({}).optional().default({}), + handler: async (_input, caller) => caller.businesses.getAll(), + }), + businesses_get: defineTool({ + description: "Get one business by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.businesses.getById(input), + }), + businesses_get_default: defineTool({ + description: "Get the user's default business.", + inputSchema: jsonSchemas.empty, + schema: z.object({}).optional().default({}), + handler: async (_input, caller) => caller.businesses.getDefault(), + }), + businesses_create: defineTool({ + description: "Create a business profile.", + inputSchema: jsonSchemas.businessCreate, + schema: businessCreateSchema, + handler: async (input, caller) => caller.businesses.create(input), + }), + businesses_update: defineTool({ + description: "Update a business profile. All business fields should be provided.", + inputSchema: { + ...jsonSchemas.businessCreate, + required: ["id", "name"], + properties: { + id: { type: "string" }, + ...jsonSchemas.businessCreate.properties, + }, + }, + schema: businessCreateSchema.extend({ id: z.string() }), + handler: async (input, caller) => caller.businesses.update(input), + }), + businesses_set_default: defineTool({ + description: "Set a business as the default for new invoices.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.businesses.setDefault(input), + }), + businesses_delete: defineTool({ + description: "Delete a business by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.businesses.delete(input), + }), + time_clock_in: defineTool({ + description: + "Start a time clock entry for the authenticated user. Fails if a timer is already running. Use invoiceId to link directly to a specific invoice (the time will be added to that invoice on clock-out). Use startedAt to backdate the start time (e.g. if you forgot to clock in earlier). Cannot be in the future.", + inputSchema: { + type: "object", + properties: { + description: { type: "string", maxLength: 500 }, + clientId: { type: "string" }, + invoiceId: { type: "string", description: "Link this timer to a specific invoice. On clock-out, time is added directly to this invoice." }, + rate: { type: "number", minimum: 0 }, + startedAt: { type: "string", format: "date-time", description: "Optional backdated start time (ISO 8601). Defaults to now." }, + }, + additionalProperties: false, + }, + schema: z.object({ + description: z.string().max(500).default(""), + clientId: z.string().optional().or(z.literal("")), + invoiceId: z.string().optional(), + rate: z.number().min(0).optional(), + startedAt: z.string().optional(), + }), + handler: async (input, caller) => + caller.timeEntries.clockIn({ + ...input, + startedAt: input.startedAt ? parseDate(input.startedAt, "startedAt") : undefined, + }), + }), + time_clock_out: defineTool({ + description: + "Stop the currently running timer for the authenticated user. Returns the completed time entry with computed hours. If the entry was linked to a specific invoice (via invoiceId at clock-in), the time is added directly to that invoice. Otherwise, if the entry has a client, a line item is automatically added to their latest open invoice. The invoice is returned in the 'invoice' field.", + inputSchema: { + type: "object", + properties: { + description: { type: "string", maxLength: 500 }, + }, + additionalProperties: false, + }, + schema: z.object({ + description: z.string().max(500).optional(), + }), + handler: async (input, caller) => caller.timeEntries.clockOut(input), + }), + time_get_running: defineTool({ + description: "Get the currently running timer, if any. Returns null if no timer is running.", + inputSchema: { type: "object", properties: {}, additionalProperties: false }, + schema: z.object({}).optional().default({}), + handler: async (_input, caller) => caller.timeEntries.getRunning(), + }), + time_entries_list: defineTool({ + description: "List completed time entries for the authenticated user.", + inputSchema: { + type: "object", + properties: { + clientId: { type: "string" }, + from: { type: "string", format: "date-time" }, + to: { type: "string", format: "date-time" }, + }, + additionalProperties: false, + }, + schema: z.object({ + clientId: z.string().optional(), + from: z.string().optional(), + to: z.string().optional(), + }), + handler: async (input, caller) => + caller.timeEntries.getAll({ + clientId: input.clientId, + from: input.from ? parseDate(input.from, "from") : undefined, + to: input.to ? parseDate(input.to, "to") : undefined, + }), + }), + time_entries_create: defineTool({ + description: + "Create a manual time entry (for backdating or importing existing records). Hours are auto-computed from startedAt/endedAt if not provided.", + inputSchema: { + type: "object", + properties: { + description: { type: "string", maxLength: 500 }, + clientId: { type: "string" }, + startedAt: { type: "string", format: "date-time" }, + endedAt: { type: "string", format: "date-time" }, + hours: { type: "number", minimum: 0 }, + rate: { type: "number", minimum: 0 }, + notes: { type: "string", maxLength: 500 }, + }, + required: ["startedAt"], + additionalProperties: false, + }, + schema: z.object({ + description: z.string().max(500).default(""), + clientId: z.string().optional().or(z.literal("")), + startedAt: dateString, + endedAt: dateString.optional(), + hours: z.number().min(0).optional(), + rate: z.number().min(0).optional(), + notes: z.string().max(500).optional(), + }), + handler: async (input, caller) => + caller.timeEntries.create({ + ...input, + startedAt: parseDate(input.startedAt, "startedAt"), + endedAt: input.endedAt ? parseDate(input.endedAt, "endedAt") : undefined, + }), + }), + time_entries_update: defineTool({ + description: "Update an existing time entry by ID. All fields are optional except id.", + inputSchema: { + type: "object", + properties: { + id: { type: "string" }, + description: { type: "string", maxLength: 500 }, + clientId: { type: "string" }, + startedAt: { type: "string", format: "date-time" }, + endedAt: { type: "string", format: "date-time" }, + hours: { type: "number", minimum: 0 }, + rate: { type: "number", minimum: 0 }, + notes: { type: "string", maxLength: 500 }, + }, + required: ["id"], + additionalProperties: false, + }, + schema: z.object({ + id: z.string(), + description: z.string().max(500).optional(), + clientId: z.string().optional().or(z.literal("")), + startedAt: dateString.optional(), + endedAt: dateString.optional(), + hours: z.number().min(0).optional(), + rate: z.number().min(0).optional(), + notes: z.string().max(500).optional().or(z.literal("")), + }), + handler: async (input, caller) => + caller.timeEntries.update({ + ...input, + startedAt: input.startedAt ? parseDate(input.startedAt, "startedAt") : undefined, + endedAt: input.endedAt ? parseDate(input.endedAt, "endedAt") : undefined, + }), + }), + time_entries_delete: defineTool({ + description: "Delete a time entry by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.timeEntries.delete(input), + }), + time_entries_get_summary: defineTool({ + description: + "Get total hours, total earnings, and entry count for the authenticated user, optionally filtered by date range.", + inputSchema: { + type: "object", + properties: { + from: { type: "string", format: "date-time" }, + to: { type: "string", format: "date-time" }, + }, + additionalProperties: false, + }, + schema: z.object({ + from: dateString.optional(), + to: dateString.optional(), + }), + handler: async (input, caller) => + caller.timeEntries.getSummary({ + from: input.from ? parseDate(input.from, "from") : undefined, + to: input.to ? parseDate(input.to, "to") : undefined, + }), + }), + // ── Expenses ──────────────────────────────────────────────────────────────── + expenses_list: defineTool({ + description: "List all expenses for the authenticated user, ordered by date descending.", + inputSchema: jsonSchemas.empty, + schema: z.object({}).optional().default({}), + handler: async (_input, caller) => caller.expenses.getAll(), + }), + expenses_get: defineTool({ + description: "Get a single expense by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.expenses.getById(input), + }), + expenses_create: defineTool({ + description: "Create an expense. Category must be one of the allowed values. Set billable=true if this will be charged to a client, taxDeductible=true for tax purposes.", + inputSchema: jsonSchemas.expenseCreate, + schema: expenseCreateSchema, + handler: async (input, caller) => + caller.expenses.create({ + ...input, + date: parseDate(input.date, "date"), + }), + }), + expenses_update: defineTool({ + description: "Update an existing expense by ID. All fields are optional except id.", + inputSchema: { + ...jsonSchemas.expenseCreate, + required: ["id"], + properties: { id: { type: "string" }, ...jsonSchemas.expenseCreate.properties }, + }, + schema: expenseUpdateSchema, + handler: async (input, caller) => + caller.expenses.update({ + ...input, + date: input.date ? parseDate(input.date, "date") : undefined, + }), + }), + expenses_delete: defineTool({ + description: "Delete an expense by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.expenses.delete(input), + }), + + // ── Recurring Invoices ─────────────────────────────────────────────────────── + recurring_list: defineTool({ + description: "List all recurring invoice templates for the authenticated user, ordered by next due date.", + inputSchema: jsonSchemas.empty, + schema: z.object({}).optional().default({}), + handler: async (_input, caller) => caller.recurringInvoices.getAll(), + }), + recurring_create: defineTool({ + description: "Create a recurring invoice template. Invoices will be auto-generated on the given schedule. Items are line item templates (description, hours, rate).", + inputSchema: jsonSchemas.recurringCreate, + schema: recurringCreateSchema, + handler: async (input, caller) => caller.recurringInvoices.create(input), + }), + recurring_update: defineTool({ + description: "Update a recurring invoice template. Replaces all items.", + inputSchema: { + ...jsonSchemas.recurringCreate, + required: ["id", "name", "clientId", "schedule", "items"], + properties: { id: { type: "string" }, ...jsonSchemas.recurringCreate.properties }, + }, + schema: recurringUpdateSchema, + handler: async (input, caller) => caller.recurringInvoices.update(input), + }), + recurring_pause: defineTool({ + description: "Pause a recurring invoice template. No invoices will be generated until resumed.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.recurringInvoices.pause(input), + }), + recurring_resume: defineTool({ + description: "Resume a paused recurring invoice template.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.recurringInvoices.resume(input), + }), + recurring_generate_now: defineTool({ + description: "Immediately generate a draft invoice from a recurring template, regardless of schedule. Returns the new invoice ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.recurringInvoices.generateNow(input), + }), + recurring_delete: defineTool({ + description: "Delete a recurring invoice template by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.recurringInvoices.delete(input), + }), + + // ── Dashboard ──────────────────────────────────────────────────────────────── + dashboard_get_stats: defineTool({ + description: "Get a business overview: total revenue (paid invoices), pending amount (sent/overdue invoices), overdue invoice count, total clients, month-over-month revenue change percentage, 6-month revenue chart data, and 5 most recent invoices.", + inputSchema: jsonSchemas.empty, + schema: z.object({}).optional().default({}), + handler: async (_input, caller) => caller.dashboard.getStats(), + }), + + // ── Invoice extras ─────────────────────────────────────────────────────────── + invoices_get_current_open: defineTool({ + description: "Get the most recent draft invoice for the authenticated user. Useful for quickly finding the active working invoice.", + inputSchema: jsonSchemas.empty, + schema: z.object({}).optional().default({}), + handler: async (_input, caller) => caller.invoices.getCurrentOpen(), + }), + invoices_send: defineTool({ + description: "Send an invoice to the client via email with a PDF attachment. Updates the invoice status to 'sent'. Requires email to be configured (Resend API key on the business or platform).", + inputSchema: jsonSchemas.invoiceSend, + schema: z.object({ + invoiceId: z.string(), + customSubject: z.string().optional(), + customMessage: z.string().optional(), + ccEmails: z.string().optional(), + bccEmails: z.string().optional(), + }), + handler: async (input, caller) => caller.email.sendInvoice({ ...input, useHtml: false }), + }), + invoices_send_reminder: defineTool({ + description: "Send a payment reminder email to the client for a sent or overdue invoice.", + inputSchema: { + type: "object", + properties: { + id: { type: "string" }, + customMessage: { type: "string", description: "Optional custom message to include in the reminder" }, + }, + required: ["id"], + additionalProperties: false, + }, + schema: z.object({ id: z.string(), customMessage: z.string().optional() }), + handler: async (input, caller) => caller.invoices.sendReminder(input), + }), + invoices_generate_public_token: defineTool({ + description: "Generate a shareable public link for an invoice. Returns a web view URL (/i/{token}) and a direct PDF URL (/api/i/{token}/pdf). Set ttlHours to make the link expire automatically (e.g. 24 for a 24-hour preview link). Omit ttlHours for a permanent link.", + inputSchema: { + type: "object", + properties: { + id: { type: "string" }, + ttlHours: { type: "number", exclusiveMinimum: 0, description: "Hours until the link expires. Omit for a permanent link." }, + }, + required: ["id"], + additionalProperties: false, + }, + schema: z.object({ id: z.string(), ttlHours: z.number().positive().optional() }), + handler: async (input, caller) => { + const result = await caller.invoices.generatePublicToken(input); + const base = getAppUrl(); + return { + ...result, + webUrl: `${base}/i/${result.token}`, + pdfUrl: `${base}/api/i/${result.token}/pdf`, + }; + }, + }), + invoices_revoke_public_token: defineTool({ + description: "Revoke the public shareable link for an invoice, making it inaccessible without authentication.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.invoices.revokePublicToken(input), + }), + invoices_bulk_update_status: defineTool({ + description: "Update the status of multiple invoices at once.", + inputSchema: jsonSchemas.bulkStatus, + schema: z.object({ + ids: z.array(z.string()).min(1), + status: z.enum(["draft", "sent", "paid"]), + }), + handler: async (input, caller) => caller.invoices.bulkUpdateStatus(input), + }), + invoices_bulk_delete: defineTool({ + description: "Delete multiple invoices at once.", + inputSchema: jsonSchemas.bulkIds, + schema: z.object({ ids: z.array(z.string()).min(1) }), + handler: async (input, caller) => caller.invoices.bulkDelete(input), + }), + + // ── Invoice Templates ──────────────────────────────────────────────────────── + templates_list: defineTool({ + description: "List all saved invoice templates (notes and terms). Use these to populate invoice notes/terms fields.", + inputSchema: jsonSchemas.empty, + schema: z.object({}).optional().default({}), + handler: async (_input, caller) => caller.invoiceTemplates.getAll(), + }), + templates_list_by_type: defineTool({ + description: "List invoice templates filtered by type: 'notes' for invoice notes, 'terms' for payment terms.", + inputSchema: { + type: "object", + properties: { type: { type: "string", enum: ["notes", "terms"] } }, + required: ["type"], + additionalProperties: false, + }, + schema: z.object({ type: z.enum(["notes", "terms"]) }), + handler: async (input, caller) => caller.invoiceTemplates.getByType(input), + }), + templates_create: defineTool({ + description: "Create an invoice template. Set isDefault=true to automatically apply this template to new invoices of this type.", + inputSchema: { + type: "object", + properties: { + name: { type: "string", minLength: 1, maxLength: 255 }, + type: { type: "string", enum: ["notes", "terms"] }, + content: { type: "string", minLength: 1 }, + isDefault: { type: "boolean" }, + }, + required: ["name", "content"], + additionalProperties: false, + }, + schema: z.object({ + name: z.string().min(1).max(255), + type: z.enum(["notes", "terms"]).default("notes"), + content: z.string().min(1), + isDefault: z.boolean().default(false), + }), + handler: async (input, caller) => caller.invoiceTemplates.create(input), + }), + templates_update: defineTool({ + description: "Update an existing invoice template by ID.", + inputSchema: { + type: "object", + properties: { + id: { type: "string" }, + name: { type: "string", minLength: 1, maxLength: 255 }, + type: { type: "string", enum: ["notes", "terms"] }, + content: { type: "string", minLength: 1 }, + isDefault: { type: "boolean" }, + }, + required: ["id"], + additionalProperties: false, + }, + schema: z.object({ + id: z.string(), + name: z.string().min(1).max(255).optional(), + type: z.enum(["notes", "terms"]).optional(), + content: z.string().min(1).optional(), + isDefault: z.boolean().optional(), + }), + handler: async (input, caller) => caller.invoiceTemplates.update(input), + }), + templates_delete: defineTool({ + description: "Delete an invoice template by ID.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.invoiceTemplates.delete(input), + }), + + // ── Business email config ───────────────────────────────────────────────────── + businesses_get_email_config: defineTool({ + description: "Get the email configuration for a business (Resend domain, from-name, and whether an API key is set). The API key itself is never returned.", + inputSchema: jsonSchemas.id, + schema: z.object({ id: z.string() }), + handler: async (input, caller) => caller.businesses.getEmailConfig(input), + }), + businesses_update_email_config: defineTool({ + description: "Configure custom email sending for a business via Resend. Set resendApiKey and resendDomain to send invoices from your own domain. Set emailFromName for the sender display name.", + inputSchema: { + type: "object", + properties: { + id: { type: "string" }, + resendApiKey: { type: "string", description: "Resend API key (re_...)" }, + resendDomain: { type: "string", description: "Verified Resend sending domain (e.g. mail.example.com)" }, + emailFromName: { type: "string", description: "Display name for the From field" }, + }, + required: ["id"], + additionalProperties: false, + }, + schema: z.object({ + id: z.string(), + resendApiKey: z.string().optional().or(z.literal("")), + resendDomain: z.string().optional().or(z.literal("")), + emailFromName: z.string().optional().or(z.literal("")), + }), + handler: async (input, caller) => caller.businesses.updateEmailConfig(input), + }), + + // ── User profile ────────────────────────────────────────────────────────────── + profile_get: defineTool({ + description: "Get the authenticated user's profile: id, name, email, and role.", + inputSchema: jsonSchemas.empty, + schema: z.object({}).optional().default({}), + handler: async (_input, caller) => caller.settings.getProfile(), + }), + profile_update: defineTool({ + description: "Update the authenticated user's display name.", + inputSchema: { + type: "object", + properties: { name: { type: "string", minLength: 1 } }, + required: ["name"], + additionalProperties: false, + }, + schema: z.object({ name: z.string().min(1) }), + handler: async (input, caller) => caller.settings.updateProfile(input), + }), +} satisfies Record; + +function rpcResult(id: JsonRpcId, result: unknown, init?: ResponseInit) { + return Response.json({ jsonrpc: "2.0", id, result }, init); +} + +function rpcError( + id: JsonRpcId, + code: number, + message: string, + status = 400, + data?: unknown, +) { + return Response.json( + { jsonrpc: "2.0", id, error: { code, message, data } }, + { status }, + ); +} + +function getErrorMessage(error: unknown) { + if (error instanceof Error) return error.message; + return "Unknown error"; +} + +async function handleMcpRequest(request: Request) { + const body = (await request.json().catch(() => null)) as { + jsonrpc?: unknown; + id?: JsonRpcId; + method?: unknown; + params?: unknown; + } | null; + + if (body?.jsonrpc !== "2.0" || typeof body.method !== "string") { + return rpcError(null, -32600, "Invalid JSON-RPC request"); + } + + if (body.id === undefined) { + return new Response(null, { status: 202 }); + } + + const ctx = await createTRPCContext({ headers: request.headers }); + if (!ctx.session?.user || ctx.authSource !== "api-key") { + return rpcError(body.id, -32001, "A valid beenvoice API key is required", 401); + } + + if (body.method === "initialize") { + return rpcResult(body.id, { + protocolVersion: "2025-11-25", + capabilities: { tools: {} }, + serverInfo: { name: "beenvoice", version: "0.1.0" }, + }); + } + + if (body.method === "ping") { + return rpcResult(body.id, {}); + } + + if (body.method === "tools/list") { + return rpcResult(body.id, { + tools: Object.entries(tools).map(([name, tool]) => ({ + name, + description: tool.description, + inputSchema: tool.inputSchema, + })), + }); + } + + if (body.method === "tools/call") { + const params = z + .object({ + name: z.string(), + arguments: z.unknown().optional(), + }) + .safeParse(body.params); + + if (!params.success) { + return rpcError(body.id, -32602, "Invalid tool call parameters", 400); + } + + const tool = tools[params.data.name as keyof typeof tools]; + if (!tool) { + return rpcError(body.id, -32602, `Unknown tool: ${params.data.name}`, 400); + } + + const input = tool.schema.safeParse(params.data.arguments ?? {}); + if (!input.success) { + return rpcError( + body.id, + -32602, + "Invalid tool arguments", + 400, + input.error.flatten(), + ); + } + + try { + const caller = createCaller(async () => ctx); + return rpcResult(body.id, textResult(await tool.handler(input.data, caller))); + } catch (error) { + return rpcError(body.id, -32000, getErrorMessage(error), 500); + } + } + + return rpcError(body.id, -32601, `Method not found: ${body.method}`, 404); +} + +export async function POST(request: Request) { + return handleMcpRequest(request); +} + +export async function GET() { + return rpcError(null, -32000, "Method not allowed", 405); +} + +export async function DELETE() { + return rpcError(null, -32000, "Method not allowed", 405); +} diff --git a/apps/web/src/app/api/receipts/[id]/route.ts b/apps/web/src/app/api/receipts/[id]/route.ts new file mode 100644 index 0000000..125f9a8 --- /dev/null +++ b/apps/web/src/app/api/receipts/[id]/route.ts @@ -0,0 +1,39 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { eq } from "drizzle-orm"; +import { getOptionalServerSession } from "~/lib/auth-server"; +import { getObject } from "~/lib/object-storage"; +import { db } from "~/server/db"; +import { expenseReceipts } from "~/server/db/schema"; + +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const session = await getOptionalServerSession(req.headers); + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = await params; + const receipt = await db.query.expenseReceipts.findFirst({ + where: eq(expenseReceipts.id, id), + with: { expense: true }, + }); + + if (receipt?.expense.createdById !== session.user.id) { + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + + try { + const body = await getObject(receipt.storageKey); + return new NextResponse(new Uint8Array(body), { + headers: { + "Content-Type": receipt.mimeType, + "Content-Disposition": `inline; filename="${encodeURIComponent(receipt.originalFilename)}"`, + "Cache-Control": "private, max-age=3600", + }, + }); + } catch { + return NextResponse.json({ error: "File not found" }, { status: 404 }); + } +} diff --git a/apps/web/src/app/api/trpc/[trpc]/route.ts b/apps/web/src/app/api/trpc/[trpc]/route.ts new file mode 100644 index 0000000..f41a82e --- /dev/null +++ b/apps/web/src/app/api/trpc/[trpc]/route.ts @@ -0,0 +1,34 @@ +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +import { type NextRequest } from "next/server"; + +import { env } from "~/env"; +import { appRouter } from "~/server/api/root"; +import { createTRPCContext } from "~/server/api/trpc"; + +/** + * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when + * handling a HTTP request (e.g. when you make requests from Client Components). + */ +const createContext = async (req: NextRequest) => { + return createTRPCContext({ + headers: req.headers, + }); +}; + +const handler = (req: NextRequest) => + fetchRequestHandler({ + endpoint: "/api/trpc", + req, + router: appRouter, + createContext: () => createContext(req), + onError: + env.NODE_ENV === "development" + ? ({ path, error }) => { + console.error( + `❌ tRPC failed on ${path ?? ""}: ${error.message}`, + ); + } + : undefined, + }); + +export { handler as GET, handler as POST }; diff --git a/apps/web/src/app/auth/forgot-password/page.tsx b/apps/web/src/app/auth/forgot-password/page.tsx new file mode 100644 index 0000000..6d330ab --- /dev/null +++ b/apps/web/src/app/auth/forgot-password/page.tsx @@ -0,0 +1,368 @@ +"use client"; + +import { useState, Suspense } from "react"; +import { Card, CardContent } from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Button } from "~/components/ui/button"; +import { Label } from "~/components/ui/label"; +import { toast } from "sonner"; +import { Logo } from "~/components/branding/logo"; +import { LegalAgreementNotice } from "~/components/legal/legal-links"; +import { + Mail, + ArrowRight, + ArrowLeft, + Shield, + Clock, + CheckCircle, +} from "lucide-react"; + +function ForgotPasswordForm() { + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); + const [sent, setSent] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + + try { + const response = await fetch("/api/auth/forgot-password", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + }); + + const data = (await response.json()) as { error?: string }; + + if (response.ok) { + setSent(true); + toast.success("Password reset instructions sent to your email"); + } else { + toast.error(data.error ?? "Failed to send reset email"); + } + } catch { + toast.error("An error occurred. Please try again."); + } finally { + setLoading(false); + } + } + + if (sent) { + return ( +
+ + + {/* Hero Section - Hidden on mobile */} +
+
+
+ +
+

+ Check your + email inbox +

+

+ We've sent password reset instructions to your email + address. Follow the link to create a new password. +

+
+
+ +
+
+
+ +
+
+

Check your inbox

+

+ Look for an email from beenvoice with reset instructions +

+
+
+ +
+
+ +
+
+

Link expires soon

+

+ The reset link is valid for 24 hours only +

+
+
+ +
+
+ +
+
+

Secure Process

+

+ Your account security is our top priority +

+
+
+
+ +
+ +
+

Email sent successfully

+

+ Follow the instructions in your email to reset your + password +

+
+
+
+
+ + {/* Success Message */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+
+ +
+

Check your email

+

+ We've sent password reset instructions to{" "} + {email} +

+
+ +
+

What's next?

+
    +
  • + 1. + Check your email inbox (and spam folder) +
  • +
  • + 2. + Click the reset link in the email +
  • +
  • + 3. + Create a new secure password +
  • +
+
+ +
+ + + + + +
+ +
+ Didn't receive the email? Check your spam folder or{" "} + + . +
+
+
+
+
+
+ ); + } + + return ( +
+ + + {/* Hero Section - Hidden on mobile */} +
+
+
+ +
+

+ Forgot your + password? +

+

+ No worries! Enter your email address and we'll send you + instructions to reset your password. +

+
+
+ +
+
+
+ +
+
+

Email Instructions

+

+ We'll send a secure link to your email address +

+
+
+ +
+
+ +
+
+

Quick Process

+

+ Reset your password in just a few clicks +

+
+
+ +
+
+ +
+
+

Secure & Safe

+

+ Your account security is our top priority +

+
+
+
+
+
+ + {/* Forgot Password Form */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+

Forgot Password

+

+ Enter your email and we'll send you reset instructions +

+
+ +
+
+ +
+ + setEmail(e.target.value)} + required + autoFocus + className="h-11 pl-10" + placeholder="Enter your email address" + /> +
+
+ + +
+ +
+
+ +
+

Check your spam folder

+

+ Sometimes our emails end up in spam or promotions folders +

+
+
+
+ + + +
+ Remember your password?{" "} + + Sign in instead + +
+ + +
+
+
+
+
+ ); +} + +export default function ForgotPasswordPage() { + return ( + Loading...
}> + + + ); +} diff --git a/apps/web/src/app/auth/layout.tsx b/apps/web/src/app/auth/layout.tsx new file mode 100644 index 0000000..f300b8f --- /dev/null +++ b/apps/web/src/app/auth/layout.tsx @@ -0,0 +1,9 @@ +import { MarketingProviders } from "~/components/providers/marketing-providers"; + +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return {children}; +} diff --git a/apps/web/src/app/auth/register/page.tsx b/apps/web/src/app/auth/register/page.tsx new file mode 100644 index 0000000..b62584a --- /dev/null +++ b/apps/web/src/app/auth/register/page.tsx @@ -0,0 +1,6 @@ +import { env } from "~/env"; +import { RegisterForm } from "./register-form"; + +export default function RegisterPage() { + return ; +} diff --git a/apps/web/src/app/auth/register/register-form.tsx b/apps/web/src/app/auth/register/register-form.tsx new file mode 100644 index 0000000..3a124d0 --- /dev/null +++ b/apps/web/src/app/auth/register/register-form.tsx @@ -0,0 +1,234 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { ArrowRight, Lock, Mail, User, UserX } from "lucide-react"; +import { + AuthCard, + AuthCardHeader, + AuthPageShell, +} from "~/components/auth/auth-page-shell"; +import { LegalAgreementNotice } from "~/components/legal/legal-links"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { toast } from "sonner"; + +function formatAuthError(message: string | undefined, fallback: string): string { + if (!message || message === "Required") { + return fallback; + } + return message; +} + +interface RegisterFormProps { + signupsDisabled?: boolean; +} + +export function RegisterForm({ signupsDisabled = false }: RegisterFormProps) { + const router = useRouter(); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleRegister(e: React.FormEvent) { + e.preventDefault(); + + const trimmedFirstName = firstName.trim(); + const trimmedLastName = lastName.trim(); + const trimmedEmail = email.trim(); + + if (!trimmedFirstName || !trimmedLastName || !trimmedEmail) { + toast.error("Please enter your first name, last name, and email."); + return; + } + + if (password.length < 8) { + toast.error("Password must be at least 8 characters."); + return; + } + + setLoading(true); + + try { + const res = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + firstName: trimmedFirstName, + lastName: trimmedLastName, + email: trimmedEmail, + password, + }), + }); + + let data: { error?: string; signInRequired?: boolean } = {}; + try { + data = (await res.json()) as typeof data; + } catch { + toast.error("Registration failed. Please try again."); + return; + } + + if (!res.ok) { + toast.error( + formatAuthError(data.error, "Registration failed. Please check the form."), + ); + return; + } + + if (data.signInRequired) { + toast.success("Account created! Please sign in."); + router.push("/auth/signin"); + return; + } + + toast.success("Account created successfully!"); + router.push("/dashboard"); + router.refresh(); + } catch { + toast.error("Registration failed. Please try again."); + } finally { + setLoading(false); + } + } + + if (signupsDisabled) { + return ( + + + + +
+ +

+ This workspace is not accepting new registrations. If you already + have an account, sign in below. Contact your administrator if you + need access. +

+
+ + +
+
+ ); + } + + return ( + + + + +
+
+
+ +
+ + setFirstName(e.target.value)} + required + autoFocus + autoComplete="given-name" + className="h-11 pl-10" + placeholder="John" + /> +
+
+ +
+ +
+ + setLastName(e.target.value)} + required + autoComplete="family-name" + className="h-11 pl-10" + placeholder="Doe" + /> +
+
+
+ +
+ +
+ + setEmail(e.target.value)} + required + autoComplete="email" + className="h-11 pl-10" + placeholder="you@example.com" + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + required + minLength={8} + autoComplete="new-password" + className="h-11 pl-10" + placeholder="••••••••" + /> +
+

At least 8 characters

+
+ + +
+ +

+ Already have an account?{" "} + + Sign in + +

+ + +
+
+ ); +} diff --git a/apps/web/src/app/auth/reset-password/page.tsx b/apps/web/src/app/auth/reset-password/page.tsx new file mode 100644 index 0000000..5dd2d3e --- /dev/null +++ b/apps/web/src/app/auth/reset-password/page.tsx @@ -0,0 +1,446 @@ +"use client"; + +import { useState, Suspense, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; +import { Card, CardContent } from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Button } from "~/components/ui/button"; +import { Label } from "~/components/ui/label"; +import { toast } from "sonner"; +import { Logo } from "~/components/branding/logo"; +import { LegalAgreementNotice } from "~/components/legal/legal-links"; +import { + Lock, + ArrowRight, + ArrowLeft, + CheckCircle, + Shield, + Eye, + EyeOff, +} from "lucide-react"; + +function ResetPasswordForm() { + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [tokenValid, setTokenValid] = useState(() => + token ? null : false, + ); + + useEffect(() => { + if (!token) { + return; + } + + // Validate token on page load + const validateToken = async () => { + try { + const response = await fetch("/api/auth/validate-reset-token", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token }), + }); + + if (response.ok) { + setTokenValid(true); + } else { + setTokenValid(false); + } + } catch { + setTokenValid(false); + } + }; + + void validateToken(); + }, [token]); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + if (!token) { + toast.error("Invalid reset token"); + return; + } + + if (password.length < 8) { + toast.error("Password must be at least 8 characters long"); + return; + } + + if (password !== confirmPassword) { + toast.error("Passwords do not match"); + return; + } + + setLoading(true); + + try { + const response = await fetch("/api/auth/reset-password", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token, password }), + }); + + const data = (await response.json()) as { error?: string }; + + if (response.ok) { + setSuccess(true); + toast.success("Password reset successfully!"); + } else { + toast.error(data.error ?? "Failed to reset password"); + } + } catch { + toast.error("An error occurred. Please try again."); + } finally { + setLoading(false); + } + } + + if (tokenValid === null) { + return ( +
+
+
+

+ Validating reset token... +

+
+
+ ); + } + + if (tokenValid === false) { + return ( +
+ + + {/* Hero Section - Hidden on mobile */} +
+
+
+ +
+

+ Invalid or + expired link +

+

+ This password reset link is either invalid or has expired. + Please request a new password reset. +

+
+
+ +
+
+
+ +
+
+

Security First

+

+ Reset links expire after 24 hours for your security +

+
+
+
+
+
+ + {/* Error Form */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+
+ +
+

Link Expired

+

+ This password reset link is no longer valid +

+
+ + +
+
+
+
+
+ ); + } + + if (success) { + return ( +
+ + + {/* Hero Section - Hidden on mobile */} +
+
+
+ +
+

+ Password + reset complete +

+

+ Your password has been successfully reset. You can now + sign in with your new password. +

+
+
+ +
+
+ +
+

Security Updated

+

+ Your account is now secured with your new password +

+
+
+
+
+
+ + {/* Success Form */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+
+ +
+

+ Password Reset Complete +

+

+ Your password has been successfully updated +

+
+ + +
+
+
+
+
+ ); + } + + return ( +
+ + + {/* Hero Section - Hidden on mobile */} +
+
+
+ +
+

+ Create your + new password +

+

+ Choose a strong password to secure your beenvoice account. + Make sure it's something you'll remember. +

+
+
+ +
+
+
+ +
+
+

Secure Password

+

+ Use at least 8 characters with a mix of letters and + numbers +

+
+
+ +
+
+ +
+
+

Account Safety

+

+ Your new password will immediately secure your account +

+
+
+
+
+
+ + {/* Reset Password Form */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+

Reset Password

+

+ Enter your new password below +

+
+ +
+
+ +
+ + setPassword(e.target.value)} + required + autoFocus + className="h-11 pr-10 pl-10" + placeholder="Enter new password" + minLength={8} + /> + +
+

+ Must be at least 8 characters long +

+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + required + className="h-11 pr-10 pl-10" + placeholder="Confirm new password" + /> + +
+
+ + +
+ + + + +
+
+
+
+
+ ); +} + +export default function ResetPasswordPage() { + return ( + Loading...}> + + + ); +} diff --git a/apps/web/src/app/auth/signin/page.tsx b/apps/web/src/app/auth/signin/page.tsx new file mode 100644 index 0000000..1d2b5ff --- /dev/null +++ b/apps/web/src/app/auth/signin/page.tsx @@ -0,0 +1,17 @@ +import { Suspense } from "react"; +import { env } from "~/env"; +import { SignInForm } from "./signin-form"; + +export default function SignInPage() { + return ( + + Loading… + + } + > + + + ); +} diff --git a/apps/web/src/app/auth/signin/signin-form.tsx b/apps/web/src/app/auth/signin/signin-form.tsx new file mode 100644 index 0000000..141c1cb --- /dev/null +++ b/apps/web/src/app/auth/signin/signin-form.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { ArrowRight, Lock, Mail, Shield } from "lucide-react"; +import { + AuthCard, + AuthCardHeader, + AuthPageShell, +} from "~/components/auth/auth-page-shell"; +import { LegalAgreementNotice } from "~/components/legal/legal-links"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { env } from "~/env"; +import { authClient } from "~/lib/auth-client"; +import { safeCallbackPath } from "~/lib/safe-callback-url"; +import { toast } from "sonner"; + +interface SignInFormProps { + allowRegistration: boolean; +} + +export function SignInForm({ allowRegistration }: SignInFormProps) { + const authentikEnabled = env.NEXT_PUBLIC_AUTHENTIK_ENABLED === true; + const router = useRouter(); + const searchParams = useSearchParams(); + const callbackUrl = safeCallbackPath(searchParams.get("callbackUrl")); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleSignIn(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + + const { error } = await authClient.signIn.email({ email, password }); + + setLoading(false); + + if (error) { + const message = error.message?.toLowerCase() ?? ""; + const rateLimited = + error.status === 429 || + message.includes("too many") || + message.includes("rate limit"); + toast.error( + rateLimited + ? "Too many sign-in attempts. Please wait a moment and try again." + : error.message && error.message !== "Required" + ? error.message + : "Invalid email or password", + ); + return; + } + + toast.success("Signed in successfully!"); + router.push(callbackUrl); + router.refresh(); + } + + async function handleSocialSignIn() { + setLoading(true); + try { + await authClient.signIn.oauth2({ + providerId: "authentik", + callbackURL: callbackUrl, + }); + } catch (error) { + console.error("[SSO Error]", error); + setLoading(false); + } + } + + return ( + + + + + {!allowRegistration && ( +

+ New account registration is currently disabled. +

+ )} + + {authentikEnabled && ( +
+ +
+
+ +
+
+ + or + +
+
+
+ )} + +
+
+ +
+ + setEmail(e.target.value)} + required + autoFocus + autoComplete="email" + className="h-11 pl-10" + placeholder="you@example.com" + /> +
+
+ +
+
+ + + Forgot password? + +
+
+ + setPassword(e.target.value)} + required + autoComplete="current-password" + className="h-11 pl-10" + placeholder="••••••••" + /> +
+
+ + +
+ + {allowRegistration && ( +

+ Don't have an account?{" "} + + Create account + +

+ )} + + +
+
+ ); +} diff --git a/apps/web/src/app/dashboard/_components/active-timer-widget.tsx b/apps/web/src/app/dashboard/_components/active-timer-widget.tsx new file mode 100644 index 0000000..f00e7ba --- /dev/null +++ b/apps/web/src/app/dashboard/_components/active-timer-widget.tsx @@ -0,0 +1,244 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; +import { api } from "~/trpc/react"; +import { Card, CardContent } from "~/components/ui/card"; +import { Button } from "~/components/ui/button"; +import { Square, Clock } from "lucide-react"; +import { toast } from "sonner"; +import { + describeClockOutOutcome, + formatElapsedSeconds, + formatRunningTimerLabel, +} from "~/lib/time-clock"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/ui/tooltip"; +import { cn } from "~/lib/utils"; + +interface ActiveTimerWidgetProps { + collapsed?: boolean; + compact?: boolean; +} + +export function ActiveTimerWidget({ + collapsed = false, + compact = false, +}: ActiveTimerWidgetProps) { + const utils = api.useUtils(); + const { data: running, isLoading } = api.timeEntries.getRunning.useQuery( + undefined, + { + staleTime: 60_000, + refetchOnWindowFocus: false, + refetchInterval: 60_000, + }, + ); + + const [elapsed, setElapsed] = useState(0); + const intervalRef = useRef | null>(null); + + useEffect(() => { + if (intervalRef.current) clearInterval(intervalRef.current); + if (running) { + const tick = () => + setElapsed(Math.floor((Date.now() - new Date(running.startedAt).getTime()) / 1000)); + tick(); + intervalRef.current = setInterval(tick, 1000); + } + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [running]); + + const clockOut = api.timeEntries.clockOut.useMutation({ + onSuccess: (data) => { + const message = describeClockOutOutcome({ + outcome: data.outcome, + hours: data.hours, + rate: data.rate, + invoice: data.invoice, + }); + + if (data.outcome === "linked_to_invoice" && data.invoice) { + toast.success("Timer stopped", { + description: message, + action: { + label: "View invoice", + onClick: () => + window.location.assign(`/dashboard/invoices/${data.invoice!.id}`), + }, + }); + } else if (data.outcome === "saved_no_invoice" || data.outcome === "saved_no_client") { + toast.warning("Time saved", { description: message }); + } else { + toast.success(message); + } + + void utils.timeEntries.getRunning.invalidate(); + }, + onError: (e) => toast.error(e.message), + }); + + if (isLoading || !running) return null; + + const invoiceLabel = running.invoice + ? `${running.invoice.invoicePrefix ?? "#"}${running.invoice.invoiceNumber}` + : null; + + const description = formatRunningTimerLabel(running.description); + + const renderStopButton = (className?: string) => ( + + ); + + if (compact) { + return ( +
+ + + + + + + {formatElapsedSeconds(elapsed)} + + + {renderStopButton("shrink-0")} +
+ ); + } + + if (collapsed) { + return ( +
+ + + + + + + + + + + + +

+ {description} + {running.client && ( + + {" "} + · {running.client.name} + + )} +

+

+ {formatElapsedSeconds(elapsed)} +

+ {invoiceLabel ? ( +

+ Billing to{" "} + + {invoiceLabel} + +

+ ) : ( +

No invoice selected

+ )} +
+ + {renderStopButton()} +
+
+
+
+
+ ); + } + + return ( + + +
+ + + + +
+

+ {description} + {running.client && ( + + {" "} + · {running.client.name} + + )} +

+

+ {invoiceLabel ? ( + <> + Billing to{" "} + + {invoiceLabel} + + + ) : ( + <>No invoice selected — open time clock to assign + )} + {" · "} + + Time clock + +

+
+
+ +
+ + {formatElapsedSeconds(elapsed)} + +
+ + {renderStopButton("w-full")} +
+
+
+
+ ); +} diff --git a/apps/web/src/app/dashboard/_components/animated-stats-card.tsx b/apps/web/src/app/dashboard/_components/animated-stats-card.tsx new file mode 100644 index 0000000..a60c9d4 --- /dev/null +++ b/apps/web/src/app/dashboard/_components/animated-stats-card.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { + TrendingDown, + TrendingUp, + Minus, + DollarSign, + Clock, + Users, +} from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { cn } from "~/lib/utils"; + +type IconName = "DollarSign" | "Clock" | "Users" | "TrendingDown"; + +interface AnimatedStatsCardProps { + title: string; + value: string; + change: string; + trend: "up" | "down" | "neutral"; + iconName: IconName; + description: string; + delay?: number; + isCurrency?: boolean; + numericValue?: number; +} + +const iconMap = { + DollarSign, + Clock, + Users, + TrendingDown, +} as const; + +export function AnimatedStatsCard({ + title, + value, + change, + trend, + iconName, + description, + delay = 0, + isCurrency = false, + numericValue, +}: AnimatedStatsCardProps) { + const Icon = iconMap[iconName]; + + let TrendIcon = Minus; + if (trend === "up") TrendIcon = TrendingUp; + if (trend === "down") TrendIcon = TrendingDown; + + const isPositive = trend === "up"; + const isNeutral = trend === "neutral"; + + void delay; + void isCurrency; + void numericValue; + + return ( + + + + + {title} + +
+ + {change} +
+
+ +

+ {value} +

+ {description} +
+
+ ); +} diff --git a/apps/web/src/app/dashboard/_components/charts-client.tsx b/apps/web/src/app/dashboard/_components/charts-client.tsx new file mode 100644 index 0000000..a27a8e7 --- /dev/null +++ b/apps/web/src/app/dashboard/_components/charts-client.tsx @@ -0,0 +1,21 @@ +"use client"; + +import dynamic from "next/dynamic"; +import { Skeleton } from "~/components/ui/skeleton"; + +const chartSkeleton = () => ; + +export const RevenueChart = dynamic( + () => import("./revenue-chart").then((m) => m.RevenueChart), + { ssr: false, loading: chartSkeleton }, +); + +export const InvoiceStatusChart = dynamic( + () => import("./invoice-status-chart").then((m) => m.InvoiceStatusChart), + { ssr: false, loading: chartSkeleton }, +); + +export const MonthlyMetricsChart = dynamic( + () => import("./monthly-metrics-chart").then((m) => m.MonthlyMetricsChart), + { ssr: false, loading: chartSkeleton }, +); diff --git a/apps/web/src/app/dashboard/_components/invoice-status-chart.tsx b/apps/web/src/app/dashboard/_components/invoice-status-chart.tsx new file mode 100644 index 0000000..8e75edd --- /dev/null +++ b/apps/web/src/app/dashboard/_components/invoice-status-chart.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { Cell, Pie, PieChart, Tooltip } from "recharts"; +import { ResponsiveChart } from "~/components/charts/responsive-chart"; +import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider"; + +export interface StatusChartDatum { + status: string; + name: string; + count: number; + value: number; +} + +interface InvoiceStatusChartProps { + data: StatusChartDatum[]; +} + +const STATUS_COLORS = { + draft: "hsl(0, 0%, 60%)", + sent: "hsl(217, 91%, 60%)", + pending: "hsl(217, 91%, 60%)", + paid: "hsl(142, 71%, 45%)", + overdue: "hsl(var(--destructive))", +} as const; + +const formatChartCurrency = (value: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); +}; + +function StatusTooltip({ + active, + payload, +}: { + active?: boolean; + payload?: Array<{ + payload: { name: string; count: number; value: number }; + }>; +}) { + if (active && payload?.length) { + const data = payload[0]!.payload; + return ( +
+

{data.name}

+

+ {data.count} invoice{data.count !== 1 ? "s" : ""} +

+

+ {formatChartCurrency(data.value)} +

+
+ ); + } + return null; +} + +export function InvoiceStatusChart({ data }: InvoiceStatusChartProps) { + const { prefersReducedMotion, animationSpeedMultiplier } = + useAnimationPreferences(); + const pieAnimationDuration = Math.round( + 600 / (animationSpeedMultiplier || 1), + ); + + if (data.length === 0) { + return ( +
+
+

+ No invoice data available +

+

+ Status breakdown will appear here once you create invoices +

+
+
+ ); + } + + return ( +
+ + + + {data.map((entry, index) => ( + + ))} + + } /> + + + +
+ {data.map((item) => ( +
+
+
+ {item.name} +
+
+

+ {item.count} +

+

+ {formatChartCurrency(item.value)} +

+
+
+ ))} +
+
+ ); +} diff --git a/apps/web/src/app/dashboard/_components/monthly-metrics-chart.tsx b/apps/web/src/app/dashboard/_components/monthly-metrics-chart.tsx new file mode 100644 index 0000000..d5025a5 --- /dev/null +++ b/apps/web/src/app/dashboard/_components/monthly-metrics-chart.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { + Bar, + BarChart, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { ResponsiveChart } from "~/components/charts/responsive-chart"; +import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider"; + +export interface MonthlyMetricsChartDatum { + month: string; + monthLabel: string; + totalInvoices: number; + paidInvoices: number; + pendingInvoices: number; + overdueInvoices: number; + draftInvoices: number; +} + +interface MonthlyMetricsChartProps { + data: MonthlyMetricsChartDatum[]; +} + +function MonthlyMetricsTooltip({ + active, + payload, + label, +}: { + active?: boolean; + payload?: Array<{ + payload: MonthlyMetricsChartDatum; + }>; + label?: string; +}) { + if (active && payload?.length) { + const chartDatum = payload[0]!.payload; + return ( +
+

{label}

+
+

+ Paid: {chartDatum.paidInvoices} +

+

+ Pending: {chartDatum.pendingInvoices} +

+

+ Overdue: {chartDatum.overdueInvoices} +

+

+ Draft: {chartDatum.draftInvoices} +

+

+ Total: {chartDatum.totalInvoices} +

+
+
+ ); + } + return null; +} + +export function MonthlyMetricsChart({ data }: MonthlyMetricsChartProps) { + const { prefersReducedMotion, animationSpeedMultiplier } = + useAnimationPreferences(); + const barAnimationDuration = Math.round( + 500 / (animationSpeedMultiplier || 1), + ); + + if (data.length === 0) { + return ( +
+
+

+ No metrics data available +

+

+ Monthly metrics will appear here once you create invoices +

+
+
+ ); + } + + return ( +
+ + + + + } /> + + + + + + + +
+
+
+ Draft +
+
+
+ Paid +
+
+
+ Pending +
+
+
+ Overdue +
+
+
+ ); +} diff --git a/apps/web/src/app/dashboard/_components/revenue-chart.tsx b/apps/web/src/app/dashboard/_components/revenue-chart.tsx new file mode 100644 index 0000000..ac5148d --- /dev/null +++ b/apps/web/src/app/dashboard/_components/revenue-chart.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { + Area, + AreaChart, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { ResponsiveChart } from "~/components/charts/responsive-chart"; +import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider"; + +interface RevenueChartProps { + data: { + month: string; + revenue: number; + monthLabel: string; + }[]; +} + +const CustomTooltip = ({ + active, + payload, + label, +}: { + active?: boolean; + payload?: Array<{ payload: { revenue: number } }>; + label?: string; +}) => { + const formatCurrency = (value: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); + }; + + if (active && payload?.length) { + const data = payload[0]!.payload; + return ( +
+

{label}

+

+ Revenue: {formatCurrency(data.revenue)} +

+

+ {/* Count not available in aggregated view currently */} +

+
+ ); + } + return null; +}; + +export function RevenueChart({ data }: RevenueChartProps) { + // Use data directly + const chartData = data; + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); + }; + + const { prefersReducedMotion, animationSpeedMultiplier } = + useAnimationPreferences(); + if (chartData.length === 0) { + return ( +
+
+

+ No revenue data available +

+

+ Revenue will appear here once you have paid invoices +

+
+
+ ); + } + + return ( + + + + + + + + + + + } /> + + + + ); +} diff --git a/apps/web/src/app/dashboard/_components/status-manager.tsx b/apps/web/src/app/dashboard/_components/status-manager.tsx new file mode 100644 index 0000000..0e1993b --- /dev/null +++ b/apps/web/src/app/dashboard/_components/status-manager.tsx @@ -0,0 +1,343 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Badge } from "~/components/ui/badge"; +import { toast } from "sonner"; +import { api } from "~/trpc/react"; +import { + Send, + DollarSign, + FileText, + AlertCircle, + Clock, + CheckCircle, + RefreshCw, + Calendar, + Loader2, +} from "lucide-react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "~/components/ui/alert-dialog"; +import { + getEffectiveInvoiceStatus, + isInvoiceOverdue, + getDaysPastDue, + getStatusConfig, +} from "~/lib/invoice-status"; +import type { StoredInvoiceStatus } from "~/types/invoice"; + +interface StatusManagerProps { + invoiceId: string; + currentStatus: StoredInvoiceStatus; + dueDate: Date; + clientEmail?: string | null; + onStatusChange?: () => void; +} + +const statusIconConfig = { + draft: FileText, + sent: Send, + paid: CheckCircle, + overdue: AlertCircle, +}; + +export function StatusManager({ + invoiceId, + currentStatus, + dueDate, + clientEmail, + onStatusChange, +}: StatusManagerProps) { + const [isChangingStatus, setIsChangingStatus] = useState(false); + const utils = api.useUtils(); + + const updateStatus = api.invoices.updateStatus.useMutation({ + onSuccess: (data) => { + toast.success(data.message); + void utils.invoices.getById.invalidate({ id: invoiceId }); + void utils.invoices.getAll.invalidate(); + onStatusChange?.(); + setIsChangingStatus(false); + }, + onError: (error) => { + toast.error(error.message ?? "Failed to update status"); + setIsChangingStatus(false); + }, + }); + + const sendEmail = api.email.sendInvoice.useMutation({ + onSuccess: (data) => { + toast.success(data.message); + void utils.invoices.getById.invalidate({ id: invoiceId }); + void utils.invoices.getAll.invalidate(); + onStatusChange?.(); + }, + onError: (error) => { + toast.error(error.message); + }, + }); + + const handleStatusUpdate = async (newStatus: StoredInvoiceStatus) => { + setIsChangingStatus(true); + updateStatus.mutate({ + id: invoiceId, + status: newStatus, + }); + }; + + const handleSendEmail = () => { + sendEmail.mutate({ invoiceId }); + }; + + const effectiveStatus = getEffectiveInvoiceStatus(currentStatus, dueDate); + const isOverdue = isInvoiceOverdue(currentStatus, dueDate); + const daysPastDue = getDaysPastDue(currentStatus, dueDate); + const statusConfig = getStatusConfig(currentStatus, dueDate); + + const StatusIcon = statusIconConfig[effectiveStatus]; + + const getAvailableActions = () => { + const actions = []; + + switch (effectiveStatus) { + case "draft": + if (clientEmail) { + actions.push({ + key: "send", + label: "Send Invoice", + action: handleSendEmail, + variant: "default" as const, + icon: Send, + disabled: sendEmail.isPending, + }); + } + actions.push({ + key: "markPaid", + label: "Mark as Paid", + action: () => handleStatusUpdate("paid"), + variant: "secondary" as const, + icon: DollarSign, + disabled: isChangingStatus, + }); + break; + + case "sent": + actions.push({ + key: "markPaid", + label: "Mark as Paid", + action: () => handleStatusUpdate("paid"), + variant: "default" as const, + icon: DollarSign, + disabled: isChangingStatus, + }); + if (clientEmail) { + actions.push({ + key: "resend", + label: "Resend Invoice", + action: handleSendEmail, + variant: "outline" as const, + icon: Send, + disabled: sendEmail.isPending, + }); + } + actions.push({ + key: "backToDraft", + label: "Back to Draft", + action: () => handleStatusUpdate("draft"), + variant: "outline" as const, + icon: FileText, + disabled: isChangingStatus, + }); + break; + + case "overdue": + actions.push({ + key: "markPaid", + label: "Mark as Paid", + action: () => handleStatusUpdate("paid"), + variant: "default" as const, + icon: DollarSign, + disabled: isChangingStatus, + }); + if (clientEmail) { + actions.push({ + key: "resend", + label: "Resend Invoice", + action: handleSendEmail, + variant: "outline" as const, + icon: Send, + disabled: sendEmail.isPending, + }); + } + actions.push({ + key: "backToSent", + label: "Mark as Sent", + action: () => handleStatusUpdate("sent"), + variant: "outline" as const, + icon: Clock, + disabled: isChangingStatus, + }); + break; + + case "paid": + // Paid invoices can be reverted if needed (rare cases) + actions.push({ + key: "revert", + label: "Revert to Sent", + action: () => handleStatusUpdate("sent"), + variant: "outline" as const, + icon: RefreshCw, + disabled: isChangingStatus, + requireConfirmation: true, + }); + break; + } + + return actions; + }; + + const actions = getAvailableActions(); + + return ( + + + + + Invoice Status + + + + {/* Current Status Display */} +
+ + {statusConfig.label} + + + {statusConfig.description} + +
+ + {/* Overdue Warning */} + {isOverdue && ( +
+ + + {daysPastDue} day{daysPastDue !== 1 ? "s" : ""} overdue + +
+ )} + + {/* Due Date Info */} +
+ + + Due:{" "} + {new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }).format(new Date(dueDate))} + +
+ + {/* Action Buttons */} + {actions.length > 0 && ( +
+
+ Available Actions: +
+
+ {actions.map((action) => { + const ActionIcon = action.icon; + + if (action.requireConfirmation) { + return ( + + + + + + + + Confirm Status Change + + + Are you sure you want to change this invoice status? + This action may affect your records. + + + + Cancel + + Confirm + + + + + ); + } + + return ( + + ); + })} +
+
+ )} + + {/* No Email Warning */} + {!clientEmail && effectiveStatus !== "paid" && ( +
+
+ + + No email address on file for this client + +
+

+ Add an email address to the client to enable sending invoices. +

+
+ )} +
+
+ ); +} diff --git a/apps/web/src/app/dashboard/administration/_components/administration-content.tsx b/apps/web/src/app/dashboard/administration/_components/administration-content.tsx new file mode 100644 index 0000000..ad24645 --- /dev/null +++ b/apps/web/src/app/dashboard/administration/_components/administration-content.tsx @@ -0,0 +1,621 @@ +"use client"; + +import { + Activity, + Building2, + Clock, + FileText, + KeyRound, + Pencil, + ScrollText, + Search, + Shield, + Users, +} from "lucide-react"; +import { useDeferredValue, useState } from "react"; +import { toast } from "sonner"; +import { EmptyState } from "~/components/layout/page-layout"; +import { + PageTabs, + PageTabsContent, + PageTabsList, + PageTabsTrigger, +} from "~/components/layout/page-tabs"; +import { dashboardStatGridClass } from "~/components/layout/dashboard-page"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "~/components/ui/alert-dialog"; +import { Badge } from "~/components/ui/badge"; +import { Button } from "~/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { api } from "~/trpc/react"; + +const PAGE_SIZE = 25; + +const ACTION_LABELS: Record = { + "user.profile_updated": "Profile updated", + "user.role_updated": "Role updated", + "user.password_reset_sent": "Password reset sent", + "platform.pdf_settings_updated": "PDF settings updated", +}; + +function formatAction(action: string) { + return ACTION_LABELS[action] ?? action; +} + +function AdminOverview() { + const { data: stats, isLoading, error } = api.admin.getStats.useQuery(); + + if (error) { + return ( + + + Platform overview + Unable to load statistics. + + + ); + } + + const statCards = [ + { + label: "Total users", + value: stats?.totalUsers ?? 0, + icon: Users, + }, + { + label: `Active (${stats?.activeUserWindowDays ?? 30}d)`, + value: stats?.activeUsers ?? 0, + icon: Activity, + }, + { + label: "Administrators", + value: stats?.adminCount ?? 0, + icon: Shield, + }, + { + label: "Invoices", + value: stats?.totalInvoices ?? 0, + icon: FileText, + }, + { + label: "Businesses", + value: stats?.totalBusinesses ?? 0, + icon: Building2, + }, + { + label: "Clients", + value: stats?.totalClients ?? 0, + icon: Users, + }, + { + label: "Time entries", + value: stats?.totalTimeEntries ?? 0, + icon: Clock, + }, + ]; + + return ( +
+ + + + + Platform overview + + + Aggregate counts only — no customer data, credentials, or bulk PII. + + + + {isLoading ? ( +

Loading statistics…

+ ) : ( +
+ {statCards.map((stat) => ( + + +
+ + {stat.label} +
+

{stat.value}

+
+
+ ))} +
+ )} +
+
+
+ ); +} + +type EditUserState = { + id: string; + name: string; + email: string; + role: "user" | "admin"; +}; + +function AdminUsers() { + const [search, setSearch] = useState(""); + const deferredSearch = useDeferredValue(search); + const [offset, setOffset] = useState(0); + const [editUser, setEditUser] = useState(null); + const [resetUserId, setResetUserId] = useState(null); + const [resetUserName, setResetUserName] = useState(""); + + const utils = api.useUtils(); + const { data, isLoading, error, isFetching } = api.admin.listUsers.useQuery({ + search: deferredSearch || undefined, + offset, + limit: PAGE_SIZE, + }); + + const updateUserMutation = api.admin.updateUser.useMutation({ + onSuccess: () => { + toast.success("User updated"); + setEditUser(null); + void utils.admin.listUsers.invalidate(); + void utils.admin.listAuditLog.invalidate(); + }, + onError: (mutationError) => { + toast.error(mutationError.message); + }, + }); + + const sendResetMutation = api.admin.sendPasswordReset.useMutation({ + onSuccess: (result) => { + if (result.emailSent) { + toast.success("Password reset email sent"); + } else { + toast.warning( + "Reset token created, but email could not be sent. Check Resend configuration.", + ); + } + setResetUserId(null); + void utils.admin.listAuditLog.invalidate(); + }, + onError: (mutationError) => { + toast.error(mutationError.message); + }, + }); + + const users = data?.items ?? []; + const total = data?.total ?? 0; + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + const currentPage = Math.floor(offset / PAGE_SIZE) + 1; + + if (error) { + return ( + + + Users + Administrative access is required. + + + ); + } + + return ( + <> + + + + + Users + + + Search accounts, edit profiles, and manage access. + + + +
+ + { + setSearch(event.target.value); + setOffset(0); + }} + placeholder="Search by name or email…" + className="pl-9" + /> +
+ + {isLoading ? ( +

Loading users…

+ ) : users.length === 0 ? ( + } + title="No users found" + description={ + deferredSearch + ? "Try a different search term." + : "No accounts have been created yet." + } + /> + ) : ( +
+ {users.map((user) => ( +
+
+
+

{user.name}

+ + {user.role} + + {user.emailVerified ? ( + + Verified + + ) : null} +
+

+ {user.email} +

+

+ Joined{" "} + {new Date(user.createdAt).toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + })} +

+
+
+ + +
+
+ ))} +
+ )} + + {total > PAGE_SIZE ? ( +
+

+ Page {currentPage} of {totalPages} · {total} users + {isFetching ? " · Updating…" : ""} +

+
+ + +
+
+ ) : null} +
+
+ + { + if (!open) setEditUser(null); + }} + > + + + Edit user + + {editUser ? ( +
{ + event.preventDefault(); + updateUserMutation.mutate({ + userId: editUser.id, + name: editUser.name, + email: editUser.email, + role: editUser.role, + }); + }} + > +
+ + + setEditUser((current) => + current + ? { ...current, name: event.target.value } + : current, + ) + } + required + /> +
+
+ + + setEditUser((current) => + current + ? { ...current, email: event.target.value } + : current, + ) + } + required + /> +
+
+ + +
+ + + + +
+ ) : null} +
+
+ + { + if (!open) setResetUserId(null); + }} + > + + + Send password reset? + + A password reset email will be sent to{" "} + {resetUserName}. The link + expires in 24 hours. + + + + Cancel + { + if (resetUserId) { + sendResetMutation.mutate({ userId: resetUserId }); + } + }} + > + {sendResetMutation.isPending ? "Sending…" : "Send reset email"} + + + + + + ); +} + +function AdminAuditLog() { + const [offset, setOffset] = useState(0); + const { data, isLoading, error, isFetching } = api.admin.listAuditLog.useQuery( + { + offset, + limit: PAGE_SIZE, + }, + ); + + const entries = data?.items ?? []; + const total = data?.total ?? 0; + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + const currentPage = Math.floor(offset / PAGE_SIZE) + 1; + + if (error) { + return ( + + + Audit log + Administrative access is required. + + + ); + } + + return ( + + + + + Audit log + + + Recent administrative actions across the platform. + + + + {isLoading ? ( +

Loading audit log…

+ ) : entries.length === 0 ? ( + } + title="No audit events yet" + description="Administrative actions will appear here." + /> + ) : ( +
+ {entries.map((entry) => ( +
+
+

+ {formatAction(entry.action)} +

+ + {entry.targetType} + +
+

+ {entry.actor?.name ?? "Unknown admin"} ·{" "} + {new Date(entry.createdAt).toLocaleString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + })} + {entry.targetId ? ` · target ${entry.targetId.slice(0, 8)}…` : ""} +

+ {entry.metadata && + Object.keys(entry.metadata).length > 0 ? ( +

+ {JSON.stringify(entry.metadata)} +

+ ) : null} +
+ ))} +
+ )} + + {total > PAGE_SIZE ? ( +
+

+ Page {currentPage} of {totalPages} · {total} events + {isFetching ? " · Updating…" : ""} +

+
+ + +
+
+ ) : null} +
+
+ ); +} + +export function AdministrationContent() { + return ( + + + Overview + Users + Audit log + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/app/dashboard/administration/page.tsx b/apps/web/src/app/dashboard/administration/page.tsx new file mode 100644 index 0000000..a09136b --- /dev/null +++ b/apps/web/src/app/dashboard/administration/page.tsx @@ -0,0 +1,41 @@ +import { eq } from "drizzle-orm"; +import { redirect } from "next/navigation"; +import { Suspense } from "react"; +import { DataTableSkeleton } from "~/components/data/data-table"; +import { DashboardPageHeader } from "~/components/layout/page-header"; +import { DashboardPage } from "~/components/layout/dashboard-page"; +import { getOptionalServerSessionFromHeaders } from "~/lib/auth-server"; +import { db } from "~/server/db"; +import { users } from "~/server/db/schema"; +import { HydrateClient } from "~/trpc/server"; +import { AdministrationContent } from "./_components/administration-content"; + +export default async function AdministrationPage() { + const session = await getOptionalServerSessionFromHeaders(); + + if (session?.user) { + const user = await db.query.users.findFirst({ + where: eq(users.id, session.user.id), + columns: { role: true }, + }); + + if (user?.role !== "admin") { + redirect("/dashboard"); + } + } + + return ( + + + + + }> + + + + + ); +} diff --git a/apps/web/src/app/dashboard/businesses/[id]/edit/page.tsx b/apps/web/src/app/dashboard/businesses/[id]/edit/page.tsx new file mode 100644 index 0000000..9ba59b9 --- /dev/null +++ b/apps/web/src/app/dashboard/businesses/[id]/edit/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { BusinessForm } from "~/components/forms/business-form"; + +export default function EditBusinessPage() { + const params = useParams(); + const businessId = Array.isArray(params?.id) ? params.id[0] : params?.id; + if (!businessId) return null; + + return ; +} diff --git a/apps/web/src/app/dashboard/businesses/[id]/page.tsx b/apps/web/src/app/dashboard/businesses/[id]/page.tsx new file mode 100644 index 0000000..c73cb1b --- /dev/null +++ b/apps/web/src/app/dashboard/businesses/[id]/page.tsx @@ -0,0 +1,344 @@ +import { notFound } from "next/navigation"; +import { api } from "~/trpc/server"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Button } from "~/components/ui/button"; +import { Badge } from "~/components/ui/badge"; +import { DashboardPageHeader } from "~/components/layout/page-header"; +import { + DashboardPage, + dashboardGapClass, + dashboardGridClass, +} from "~/components/layout/dashboard-page"; +import { cn } from "~/lib/utils"; +import { Separator } from "~/components/ui/separator"; +import Link from "next/link"; +import { + Edit, + Mail, + Phone, + MapPin, + Building, + Calendar, + DollarSign, + Globe, + Hash, + ArrowLeft, +} from "lucide-react"; + +interface BusinessDetailPageProps { + params: Promise<{ id: string }>; +} + +export default async function BusinessDetailPage({ + params, +}: BusinessDetailPageProps) { + const { id } = await params; + + const business = await api.businesses.getById({ id }); + + if (!business) { + notFound(); + } + + const formatDate = (date: Date) => { + return new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }).format(date); + }; + + return ( + + + + + + +
+ {/* Business Information Card */} +
+ + + + {business.logoStorageKey ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element -- object-storage-backed image, not a static asset */} + {`${business.name} +
+ ) : ( +
+ +
+ )} + Business Information +
+
+ + {/* Contact Information */} +
+

+ Contact Information +

+
+ {business.email && ( +
+
+ +
+
+

+ Email +

+

+ {business.email} +

+
+
+ )} + + {business.phone && ( +
+
+ +
+
+

+ Phone +

+

+ {business.phone} +

+
+
+ )} + + {business.website && ( +
+
+ +
+
+

+ Website +

+ + {business.website} + +
+
+ )} + + {business.taxId && ( +
+
+ +
+
+

+ Tax ID +

+

+ {business.taxId} +

+
+
+ )} +
+
+ + {/* Address */} + {(business.addressLine1 ?? business.city ?? business.state) && ( + <> + +
+

+ Business Address +

+
+
+ +
+
+ {business.addressLine1 && ( +

+ {business.addressLine1} +

+ )} + {business.addressLine2 && ( +

+ {business.addressLine2} +

+ )} + {(business.city ?? + business.state ?? + business.postalCode) && ( +

+ {[ + business.city, + business.state, + business.postalCode, + ] + .filter(Boolean) + .join(", ")} +

+ )} + {business.country && ( +

{business.country}

+ )} +
+
+
+ + )} + + + + {/* Business Metadata */} +
+

Business Details

+
+
+
+ +
+
+

+ Business Added +

+

+ {formatDate(business.createdAt)} +

+
+
+ + {business.nickname && ( +
+
+ +
+
+
+

+ Nickname +

+ + Internal only + +
+

+ {business.nickname} +

+
+
+ )} + + {/* Default Business Badge */} + {business.isDefault && ( +
+
+ +
+
+

+ Status +

+ + Default Business + +
+
+ )} +
+
+
+
+
+ + {/* Settings & Actions Card */} +
+ + + +
+ +
+ Quick Actions +
+
+ +
+ + +
+
+
+ + {/* Information Card */} + + + About This Business + + +
+

+ This business profile is used for generating invoices and + represents your company information to clients. +

+ {business.isDefault && ( +

+ This is your default business and will be automatically + selected when creating new invoices. +

+ )} +
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/dashboard/businesses/_components/businesses-data-table.tsx b/apps/web/src/app/dashboard/businesses/_components/businesses-data-table.tsx new file mode 100644 index 0000000..acce46e --- /dev/null +++ b/apps/web/src/app/dashboard/businesses/_components/businesses-data-table.tsx @@ -0,0 +1,265 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button } from "~/components/ui/button"; +import { DataTable, DataTableColumnHeader } from "~/components/data/data-table"; +import { Building, Pencil, Trash2, ExternalLink, Plus } from "lucide-react"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { api } from "~/trpc/react"; +import { toast } from "sonner"; + +// Type for business data +interface Business { + id: string; + name: string; + nickname: string | null; + email: string | null; + phone: string | null; + addressLine1: string | null; + addressLine2: string | null; + city: string | null; + state: string | null; + postalCode: string | null; + country: string | null; + website: string | null; + taxId: string | null; + logoUrl: string | null; + logoStorageKey: string | null; + createdById: string; + createdAt: Date; + updatedAt: Date | null; +} + +interface BusinessesDataTableProps { + businesses: Business[]; +} + +export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) { + const router = useRouter(); + const [businessToDelete, setBusinessToDelete] = useState( + null, + ); + + const utils = api.useUtils(); + + const searchableBusinesses = businesses.map((b) => ({ + ...b, + searchValue: `${b.name} ${b.nickname ?? ""}`.trim(), + })); + + const deleteBusinessMutation = api.businesses.delete.useMutation({ + onSuccess: () => { + toast.success("Business deleted successfully"); + setBusinessToDelete(null); + void utils.businesses.getAll.invalidate(); + }, + onError: (error) => { + toast.error(`Failed to delete business: ${error.message}`); + }, + }); + + const handleDelete = () => { + if (!businessToDelete) return; + deleteBusinessMutation.mutate({ id: businessToDelete.id }); + }; + + const handleRowClick = (business: Business) => { + router.push(`/dashboard/businesses/${business.id}`); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const business = row.original; + return ( +
+
+ {business.logoStorageKey ? ( + // eslint-disable-next-line @next/next/no-img-element -- object-storage-backed image, not a static asset + + ) : ( + + )} +
+
+

{business.name}

+

+ {business.nickname ?? "—"} +

+
+
+ ); + }, + }, + { + accessorKey: "email", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.email ?? "—", + meta: { + headerClassName: "hidden sm:table-cell", + cellClassName: "hidden sm:table-cell", + }, + }, + { + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.phone ?? "—", + meta: { + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", + }, + }, + { + accessorKey: "website", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const website = row.original.website; + if (!website) return "—"; + + // Add https:// if not present + const url = website.startsWith("http") ? website : `https://${website}`; + + return ( + <> + {/* Desktop: Show full URL */} + + {website} + + {/* Mobile: Show link button */} + + + ); + }, + }, + { + accessorKey: "searchValue", + header: "Search", + cell: () => null, + meta: { + headerClassName: "hidden", + cellClassName: "hidden", + }, + }, + { + id: "actions", + cell: ({ row }) => { + const business = row.original; + return ( +
+ + + + +
+ ); + }, + }, + ]; + + return ( + <> + } + emptyAction={ + + } + onRowClick={handleRowClick} + /> + + {/* Delete confirmation dialog */} + !open && setBusinessToDelete(null)} + > + + + Are you sure? + + This action cannot be undone. This will permanently delete the + business "{businessToDelete?.name}" and remove all + associated data. + + + + + + + + + + ); +} diff --git a/apps/web/src/app/dashboard/businesses/_components/businesses-table.tsx b/apps/web/src/app/dashboard/businesses/_components/businesses-table.tsx new file mode 100644 index 0000000..25f4165 --- /dev/null +++ b/apps/web/src/app/dashboard/businesses/_components/businesses-table.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { api } from "~/trpc/react"; +import { DataTableSkeleton } from "~/components/data/data-table"; +import { BusinessesDataTable } from "./businesses-data-table"; + +export function BusinessesTable() { + const { data: businesses, isLoading } = api.businesses.getAll.useQuery(); + + if (isLoading) { + return ; + } + + if (!businesses) { + return null; + } + + return ; +} diff --git a/apps/web/src/app/dashboard/businesses/new/page.tsx b/apps/web/src/app/dashboard/businesses/new/page.tsx new file mode 100644 index 0000000..e2b5eb9 --- /dev/null +++ b/apps/web/src/app/dashboard/businesses/new/page.tsx @@ -0,0 +1,10 @@ +import { BusinessForm } from "~/components/forms/business-form"; +import { HydrateClient } from "~/trpc/server"; + +export default function NewBusinessPage() { + return ( + + + + ); +} diff --git a/apps/web/src/app/dashboard/businesses/page.tsx b/apps/web/src/app/dashboard/businesses/page.tsx new file mode 100644 index 0000000..5f26cff --- /dev/null +++ b/apps/web/src/app/dashboard/businesses/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function BusinessesPage() { + redirect("/dashboard/entities?tab=businesses"); +} diff --git a/apps/web/src/app/dashboard/clients/[id]/edit/page.tsx b/apps/web/src/app/dashboard/clients/[id]/edit/page.tsx new file mode 100644 index 0000000..fa1818f --- /dev/null +++ b/apps/web/src/app/dashboard/clients/[id]/edit/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { ClientForm } from "~/components/forms/client-form"; + +export default function EditClientPage() { + const params = useParams(); + const clientId = Array.isArray(params?.id) ? params.id[0] : params?.id; + if (!clientId) return null; + + return ; +} diff --git a/apps/web/src/app/dashboard/clients/[id]/page.tsx b/apps/web/src/app/dashboard/clients/[id]/page.tsx new file mode 100644 index 0000000..456c1e4 --- /dev/null +++ b/apps/web/src/app/dashboard/clients/[id]/page.tsx @@ -0,0 +1,285 @@ +import { notFound } from "next/navigation"; +import { api } from "~/trpc/server"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Button } from "~/components/ui/button"; +import { Badge } from "~/components/ui/badge"; +import { DashboardPageHeader } from "~/components/layout/page-header"; +import { + DashboardPage, + dashboardGapClass, + dashboardGridClass, +} from "~/components/layout/dashboard-page"; +import { cn } from "~/lib/utils"; +import Link from "next/link"; +import { + Edit, + Mail, + Phone, + MapPin, + Building, + Calendar, + DollarSign, + ArrowLeft, +} from "lucide-react"; +import { getEffectiveInvoiceStatus } from "~/lib/invoice-status"; +import type { StoredInvoiceStatus } from "~/types/invoice"; + +interface ClientDetailPageProps { + params: Promise<{ id: string }>; +} + +export default async function ClientDetailPage({ + params, +}: ClientDetailPageProps) { + const { id } = await params; + + const client = await api.clients.getById({ id }); + + if (!client) { + notFound(); + } + + const formatDate = (date: Date) => { + return new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }).format(date); + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount); + }; + + const totalInvoiced = + client.invoices?.reduce((sum, invoice) => sum + invoice.totalAmount, 0) || + 0; + const paidInvoices = + client.invoices?.filter((invoice) => invoice.status === "paid").length || 0; + const pendingInvoices = + client.invoices?.filter((invoice) => invoice.status === "sent").length || 0; + + return ( + + + + + + +
+ {/* Client Information Card */} +
+ + + +
+ +
+ Contact Information +
+
+ + {/* Basic Info */} +
+ {client.email && ( +
+
+ +
+
+

+ Email +

+

{client.email}

+
+
+ )} + + {client.phone && ( +
+
+ +
+
+

+ Phone +

+

{client.phone}

+
+
+ )} +
+ + {/* Address */} + {(client.addressLine1 ?? client.city ?? client.state) && ( +
+

Client Address

+
+
+ +
+
+ {client.addressLine1 && ( +

{client.addressLine1}

+ )} + {client.addressLine2 && ( +

{client.addressLine2}

+ )} + {(client.city ?? client.state ?? client.postalCode) && ( +

+ {[client.city, client.state, client.postalCode] + .filter(Boolean) + .join(", ")} +

+ )} + {client.country && ( +

{client.country}

+ )} +
+
+
+ )} + + {/* Client Since */} +
+

Client Details

+
+
+ +
+
+

+ Client Since +

+

+ {formatDate(client.createdAt)} +

+
+
+
+
+
+
+ + {/* Stats Card */} +
+ + + +
+ +
+ Invoice Summary +
+
+ +
+

+ {formatCurrency(totalInvoiced)} +

+

Total Invoiced

+
+ +
+
+

+ {paidInvoices} +

+

Paid

+
+
+

+ {pendingInvoices} +

+

Pending

+
+
+
+
+ + {/* Recent Invoices */} + {client.invoices && client.invoices.length > 0 && ( + + + +
+ +
+ Recent Invoices +
+
+ +
+ {client.invoices.slice(0, 3).map((invoice) => ( +
+
+
+

+ {invoice.invoiceNumber} +

+

+ {formatDate(invoice.issueDate)} +

+
+
+

+ {formatCurrency(invoice.totalAmount)} +

+ + {getEffectiveInvoiceStatus( + invoice.status as StoredInvoiceStatus, + invoice.dueDate, + )} + +
+
+
+ ))} +
+
+
+ )} +
+
+
+ ); +} diff --git a/apps/web/src/app/dashboard/clients/_components/clients-data-table.tsx b/apps/web/src/app/dashboard/clients/_components/clients-data-table.tsx new file mode 100644 index 0000000..c2856c2 --- /dev/null +++ b/apps/web/src/app/dashboard/clients/_components/clients-data-table.tsx @@ -0,0 +1,226 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button } from "~/components/ui/button"; +import { DataTable, DataTableColumnHeader } from "~/components/data/data-table"; +import { UserPlus, Pencil, Trash2, Plus, Users } from "lucide-react"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { api } from "~/trpc/react"; +import { toast } from "sonner"; + +// Type for client data +interface Client { + id: string; + name: string; + email: string | null; + phone: string | null; + addressLine1: string | null; + addressLine2: string | null; + city: string | null; + state: string | null; + postalCode: string | null; + country: string | null; + createdById: string; + createdAt: Date; + updatedAt: Date | null; +} + +interface ClientsDataTableProps { + clients: Client[]; +} + +const formatAddress = (client: Client) => { + const parts = [ + client.addressLine1, + client.addressLine2, + client.city, + client.state, + client.postalCode, + ].filter(Boolean); + return parts.join(", ") || "—"; +}; + +export function ClientsDataTable({ + clients: initialClients, +}: ClientsDataTableProps) { + const router = useRouter(); + const [clients, setClients] = useState(initialClients); + const [clientToDelete, setClientToDelete] = useState(null); + + const utils = api.useUtils(); + + const deleteClientMutation = api.clients.delete.useMutation({ + onSuccess: () => { + toast.success("Client deleted successfully"); + setClients(clients.filter((c) => c.id !== clientToDelete?.id)); + setClientToDelete(null); + void utils.clients.getAll.invalidate(); + }, + onError: (error) => { + toast.error(`Failed to delete client: ${error.message}`); + }, + }); + + const handleDelete = () => { + if (!clientToDelete) return; + deleteClientMutation.mutate({ id: clientToDelete.id }); + }; + + const handleRowClick = (client: Client) => { + router.push(`/dashboard/clients/${client.id}`); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const client = row.original; + return ( +
+
+ +
+
+

{client.name}

+

+ {client.email ?? "—"} +

+
+
+ ); + }, + }, + { + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.phone ?? "—", + meta: { + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", + }, + }, + { + id: "address", + header: "Address", + cell: ({ row }) => formatAddress(row.original), + meta: { + headerClassName: "hidden lg:table-cell", + cellClassName: "hidden lg:table-cell", + }, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const date = row.getValue("createdAt"); + return new Intl.DateTimeFormat("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + }).format(new Date(date as Date)); + }, + meta: { + headerClassName: "hidden xl:table-cell", + cellClassName: "hidden xl:table-cell", + }, + }, + { + id: "actions", + cell: ({ row }) => { + const client = row.original; + return ( +
+ + + + +
+ ); + }, + }, + ]; + + return ( + <> + } + emptyAction={ + + } + onRowClick={handleRowClick} + /> + + {/* Delete confirmation dialog */} + !open && setClientToDelete(null)} + > + + + Are you sure? + + This action cannot be undone. This will permanently delete the + client "{clientToDelete?.name}" and remove all + associated data. + + + + + + + + + + ); +} diff --git a/apps/web/src/app/dashboard/clients/_components/clients-table.tsx b/apps/web/src/app/dashboard/clients/_components/clients-table.tsx new file mode 100644 index 0000000..65b2ec3 --- /dev/null +++ b/apps/web/src/app/dashboard/clients/_components/clients-table.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { api } from "~/trpc/react"; +import { DataTableSkeleton } from "~/components/data/data-table"; +import { ClientsDataTable } from "./clients-data-table"; + +export function ClientsTable() { + const { data: clients, isLoading } = api.clients.getAll.useQuery(); + + if (isLoading) { + return ; + } + + if (!clients) { + return null; + } + + return ; +} diff --git a/apps/web/src/app/dashboard/clients/new/page.tsx b/apps/web/src/app/dashboard/clients/new/page.tsx new file mode 100644 index 0000000..629ccdc --- /dev/null +++ b/apps/web/src/app/dashboard/clients/new/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ClientForm } from "~/components/forms/client-form"; + +export default function NewClientPage() { + return ; +} diff --git a/apps/web/src/app/dashboard/clients/page.tsx b/apps/web/src/app/dashboard/clients/page.tsx new file mode 100644 index 0000000..2281a60 --- /dev/null +++ b/apps/web/src/app/dashboard/clients/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function ClientsPage() { + redirect("/dashboard/entities?tab=clients"); +} diff --git a/apps/web/src/app/dashboard/entities/_components/entities-view.tsx b/apps/web/src/app/dashboard/entities/_components/entities-view.tsx new file mode 100644 index 0000000..7c9d468 --- /dev/null +++ b/apps/web/src/app/dashboard/entities/_components/entities-view.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { Plus } from "lucide-react"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { DashboardPageHeader } from "~/components/layout/page-header"; +import { + PageTabs, + PageTabsContent, + PageTabsList, + PageTabsTrigger, +} from "~/components/layout/page-tabs"; +import { Button } from "~/components/ui/button"; +import { ClientsDataTable } from "../../clients/_components/clients-data-table"; +import { BusinessesDataTable } from "../../businesses/_components/businesses-data-table"; +import type { RouterOutputs } from "~/trpc/react"; + +type EntityTab = "clients" | "businesses"; + +type Client = RouterOutputs["clients"]["getAll"][number]; +type Business = RouterOutputs["businesses"]["getAll"][number]; + +export function EntitiesView({ + initialTab, + clients, + businesses, +}: { + initialTab: EntityTab; + clients: Client[]; + businesses: Business[]; +}) { + const router = useRouter(); + const searchParams = useSearchParams(); + const tab: EntityTab = + searchParams.get("tab") === "businesses" ? "businesses" : initialTab; + + function handleTabChange(value: string) { + const next = value === "businesses" ? "businesses" : "clients"; + router.replace(`/dashboard/entities?tab=${next}`, { scroll: false }); + } + + const addHref = + tab === "clients" ? "/dashboard/clients/new" : "/dashboard/businesses/new"; + const addLabel = tab === "clients" ? "Add client" : "Add business"; + + return ( + <> + + + + + + + Clients + Businesses + + + + {tab === "clients" ? : null} + + + + {tab === "businesses" ? ( + + ) : null} + + + + ); +} diff --git a/apps/web/src/app/dashboard/entities/page.tsx b/apps/web/src/app/dashboard/entities/page.tsx new file mode 100644 index 0000000..b3ab799 --- /dev/null +++ b/apps/web/src/app/dashboard/entities/page.tsx @@ -0,0 +1,27 @@ +import { api } from "~/trpc/server"; +import { DashboardPage } from "~/components/layout/dashboard-page"; +import { EntitiesView } from "./_components/entities-view"; + +export default async function EntitiesPage({ + searchParams, +}: { + searchParams: Promise<{ tab?: string }>; +}) { + const params = await searchParams; + const initialTab = params.tab === "businesses" ? "businesses" : "clients"; + + const [clients, businesses] = await Promise.all([ + api.clients.getAll(), + api.businesses.getAll(), + ]); + + return ( + + + + ); +} diff --git a/apps/web/src/app/dashboard/expenses/page.tsx b/apps/web/src/app/dashboard/expenses/page.tsx new file mode 100644 index 0000000..7b30e22 --- /dev/null +++ b/apps/web/src/app/dashboard/expenses/page.tsx @@ -0,0 +1,882 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { api } from "~/trpc/react"; +import { DashboardPageHeader } from "~/components/layout/page-header"; +import { + DashboardPage, + dashboardStatGridClass, +} from "~/components/layout/dashboard-page"; +import { EmptyState } from "~/components/layout/page-layout"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Badge } from "~/components/ui/badge"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Checkbox } from "~/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { DatePicker } from "~/components/ui/date-picker"; +import { NumberInput } from "~/components/ui/number-input"; +import { ExpenseReceiptsPanel } from "~/components/expenses/expense-receipts-panel"; +import { ExpenseReceiptIndicator } from "~/components/expenses/expense-receipt-indicator"; +import { toast } from "sonner"; +import { + MoreHorizontal, + Pencil, + Plus, + Receipt, + Search, + Trash2, +} from "lucide-react"; +import { formatCurrency, SUPPORTED_CURRENCIES } from "~/lib/currency"; +import { EXPENSE_CATEGORIES } from "~/lib/expense-categories"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; + +interface ExpenseFormData { + date: Date; + description: string; + amount: number; + currency: string; + category: string; + billable: boolean; + reimbursable: boolean; + taxDeductible: boolean; + notes: string; + clientId: string; + businessId: string; +} + +const defaultForm: ExpenseFormData = { + date: new Date(), + description: "", + amount: 0, + currency: "USD", + category: "", + billable: false, + reimbursable: false, + taxDeductible: false, + notes: "", + clientId: "", + businessId: "", +}; + +type ExpenseDialogMode = "create" | "view" | "edit"; +type ExpenseFilter = "all" | "billable" | "deductible" | "receipts"; + +function expenseToForm( + expense: { + date: Date | string; + description: string; + amount: number; + currency: string; + category: string | null; + billable: boolean; + reimbursable: boolean; + taxDeductible: boolean | null; + notes: string | null; + clientId: string | null; + businessId: string | null; + }, + defaultBusinessId: string, +): ExpenseFormData { + return { + date: new Date(expense.date), + description: expense.description, + amount: expense.amount, + currency: expense.currency, + category: expense.category ?? "", + billable: expense.billable, + reimbursable: expense.reimbursable, + taxDeductible: expense.taxDeductible ?? false, + notes: expense.notes ?? "", + clientId: expense.clientId ?? "", + businessId: expense.businessId ?? defaultBusinessId, + }; +} + +export default function ExpensesPage() { + const [open, setOpen] = useState(false); + const [dialogMode, setDialogMode] = useState("create"); + const [editId, setEditId] = useState(null); + const [form, setForm] = useState(defaultForm); + const [deleteId, setDeleteId] = useState(null); + const [businessFilter, setBusinessFilter] = useState("all"); + const [expenseFilter, setExpenseFilter] = useState("all"); + const [search, setSearch] = useState(""); + + const utils = api.useUtils(); + const { data: businesses = [] } = api.businesses.getAll.useQuery(); + const { data: expenses = [], isLoading } = api.expenses.getAll.useQuery( + businessFilter === "all" ? undefined : { businessId: businessFilter }, + ); + const { data: clients = [] } = api.clients.getAll.useQuery(); + + const defaultBusinessId = useMemo( + () => businesses.find((b) => b.isDefault)?.id ?? businesses[0]?.id ?? "", + [businesses], + ); + + const create = api.expenses.create.useMutation({ + onSuccess: (expense) => { + if (!expense) return; + toast.success("Expense saved — you can now attach receipts"); + void utils.expenses.getAll.invalidate(); + setEditId(expense.id); + setDialogMode("edit"); + }, + onError: (e) => toast.error(e.message), + }); + const update = api.expenses.update.useMutation({ + onSuccess: () => { + toast.success("Expense updated"); + void utils.expenses.getAll.invalidate(); + setOpen(false); + setEditId(null); + setDialogMode("create"); + setForm(defaultForm); + }, + onError: (e) => toast.error(e.message), + }); + const del = api.expenses.delete.useMutation({ + onSuccess: () => { + toast.success("Expense deleted"); + void utils.expenses.getAll.invalidate(); + setDeleteId(null); + }, + onError: (e) => toast.error(e.message), + }); + + const closeDialog = () => { + setOpen(false); + setEditId(null); + setDialogMode("create"); + setForm(defaultForm); + }; + + const handleOpen = () => { + setEditId(null); + setDialogMode("create"); + setForm({ ...defaultForm, businessId: defaultBusinessId }); + setOpen(true); + }; + const handleView = (expense: (typeof expenses)[0]) => { + setEditId(expense.id); + setDialogMode("view"); + setForm(expenseToForm(expense, defaultBusinessId)); + setOpen(true); + }; + const handleEdit = (expense: (typeof expenses)[0]) => { + setEditId(expense.id); + setDialogMode("edit"); + setForm(expenseToForm(expense, defaultBusinessId)); + setOpen(true); + }; + const handleSubmit = () => { + if (!form.description.trim()) { + toast.error("Description is required"); + return; + } + if (form.amount <= 0) { + toast.error("Amount must be greater than 0"); + return; + } + const payload = { + ...form, + clientId: form.clientId || undefined, + businessId: form.businessId || undefined, + category: form.category || undefined, + notes: form.notes || undefined, + taxDeductible: form.taxDeductible, + }; + if (editId) update.mutate({ id: editId, ...payload }); + else create.mutate(payload); + }; + + const filteredExpenses = useMemo(() => { + const needle = search.trim().toLowerCase(); + return expenses.filter((expense) => { + if (expenseFilter === "billable" && !expense.billable) return false; + if (expenseFilter === "deductible" && !expense.taxDeductible) + return false; + if (expenseFilter === "receipts" && expense.receiptCount === 0) + return false; + if (!needle) return true; + return [ + expense.description, + expense.category, + expense.notes, + expense.business?.name, + expense.client?.name, + ] + .filter(Boolean) + .some((value) => value?.toLowerCase().includes(needle)); + }); + }, [expenseFilter, expenses, search]); + + const totalExpenses = expenses.reduce((s, e) => s + e.amount, 0); + const visibleTotal = filteredExpenses.reduce((s, e) => s + e.amount, 0); + const billableTotal = expenses + .filter((e) => e.billable) + .reduce((s, e) => s + e.amount, 0); + const deductibleTotal = expenses + .filter((e) => e.taxDeductible) + .reduce((s, e) => s + e.amount, 0); + const withReceipts = expenses.filter((e) => e.receiptCount > 0).length; + const hasActiveFilters = + search.trim().length > 0 || + expenseFilter !== "all" || + businessFilter !== "all"; + + const isViewMode = dialogMode === "view"; + const isEditMode = dialogMode === "edit"; + const isCreateMode = dialogMode === "create"; + + const dialogTitle = isCreateMode + ? "Add expense" + : isViewMode + ? "View expense" + : "Edit expense"; + + const businessName = + businesses.find((b) => b.id === form.businessId)?.name ?? + (form.businessId ? "Unknown business" : "Default business"); + const clientName = form.clientId + ? (clients.find((c) => c.id === form.clientId)?.name ?? "Unknown client") + : "No client"; + + const formattedDate = new Intl.DateTimeFormat("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }).format(form.date); + + return ( + + + + + +
+ + +

+ Total +

+

+ {formatCurrency(totalExpenses)} +

+
+
+ + +

+ Billable +

+

+ {formatCurrency(billableTotal)} +

+
+
+ + +

+ Deductible +

+

+ {formatCurrency(deductibleTotal)} +

+
+
+ + +

+ With receipts +

+

{withReceipts}

+
+
+
+ + + +
+
+ + Expenses + +

+ {filteredExpenses.length === expenses.length + ? `${expenses.length} recorded` + : `${filteredExpenses.length} of ${expenses.length} shown`} + {filteredExpenses.length !== expenses.length + ? ` · ${formatCurrency(visibleTotal)} visible` + : ""} +

+
+
+
+ + setSearch(e.target.value)} + placeholder="Search expenses" + className="pl-9" + /> +
+ +
+
+
+ {[ + ["all", "All"] as const, + ["billable", "Billable"] as const, + ["deductible", "Deductible"] as const, + ["receipts", "With receipts"] as const, + ].map(([value, label]) => ( + + ))} +
+
+ + {isLoading ? ( +
+ Loading… +
+ ) : expenses.length === 0 ? ( + } + title="Create your first expense" + description="Track billable costs, reimbursements, and tax-deductible spending." + action={ + + } + /> + ) : filteredExpenses.length === 0 ? ( + } + title="No matching expenses" + description="Adjust the search or filters to bring expenses back into view." + action={ + hasActiveFilters ? ( + + ) : undefined + } + /> + ) : ( + <> +
+ Expense + Receipts + Amount + +
+
+ {filteredExpenses.map((expense) => ( +
handleView(expense)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleView(expense); + } + }} + className="hover:bg-muted/40 focus-visible:ring-ring flex cursor-pointer flex-col gap-3 p-4 transition-colors focus-visible:ring-2 focus-visible:outline-none sm:grid sm:grid-cols-[minmax(0,1fr)_104px_116px_44px] sm:items-center sm:gap-3" + > +
+
+

{expense.description}

+ {expense.billable && ( + + Billable + + )} + {expense.reimbursable && ( + + Reimbursable + + )} + {expense.taxDeductible && ( + + Tax Deductible + + )} + {expense.category && ( + + {expense.category} + + )} +
+

+ {new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }).format(new Date(expense.date))} + {expense.business ? ` · ${expense.business.name}` : ""} + {expense.client ? ` · ${expense.client.name}` : ""} +

+ {expense.notes && ( +

+ {expense.notes} +

+ )} +
+ +
e.stopPropagation()} + > + + Receipts + + +
+ +

+ {formatCurrency(expense.amount, expense.currency)} +

+ +
e.stopPropagation()} + > + + + + + + handleView(expense)}> + + View details + + handleEdit(expense)}> + + Edit + + setDeleteId(expense.id)} + > + + Delete + + + +
+
+ ))} +
+ + )} +
+
+ + { + setOpen(next); + if (!next) { + setEditId(null); + setDialogMode("create"); + setForm(defaultForm); + } + }} + > + + + {dialogTitle} + {isCreateMode && ( + + Fill in the details below. You can attach receipts after saving. + + )} + +
+ {isViewMode ? ( +
+
+

+ Description +

+

{form.description}

+
+
+

+ Amount +

+

+ {formatCurrency(form.amount, form.currency)} +

+
+
+

+ Date +

+

{formattedDate}

+
+
+

+ Category +

+

{form.category || "None"}

+
+
+

+ Business +

+

{businessName}

+
+
+

+ Client +

+

{clientName}

+
+
+

+ Flags +

+
+ {form.billable ? ( + Billable + ) : ( + Not billable + )} + {form.reimbursable ? ( + Reimbursable + ) : null} + {form.taxDeductible ? ( + + Tax deductible + + ) : null} +
+
+ {form.notes ? ( +
+

+ Notes +

+

{form.notes}

+
+ ) : null} +
+ ) : ( + <> +
+ + + setForm((p) => ({ ...p, description: e.target.value })) + } + placeholder="e.g. Laptop charger" + /> +
+
+
+ + setForm((p) => ({ ...p, amount: v }))} + min={0} + step={0.01} + /> +
+
+ + +
+
+
+
+ + + setForm((p) => ({ ...p, date: d ?? new Date() })) + } + className="w-full" + /> +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + + +
+
+ + + setForm((p) => ({ ...p, notes: e.target.value })) + } + placeholder="Additional details…" + /> +
+ + )} + + +
+ + {isViewMode ? ( + <> + + + + ) : ( + <> + + + + )} + +
+
+ + !o && setDeleteId(null)}> + + + Delete Expense + This action cannot be undone. + + + + + + + +
+ ); +} diff --git a/apps/web/src/app/dashboard/invoices/[id]/_components/invoice-details-skeleton.tsx b/apps/web/src/app/dashboard/invoices/[id]/_components/invoice-details-skeleton.tsx new file mode 100644 index 0000000..8069207 --- /dev/null +++ b/apps/web/src/app/dashboard/invoices/[id]/_components/invoice-details-skeleton.tsx @@ -0,0 +1,179 @@ +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Separator } from "~/components/ui/separator"; +import { Skeleton } from "~/components/ui/skeleton"; +import { DashboardPageHeader } from "~/components/layout/page-header"; +import { + DashboardPage, + dashboardGapClass, + dashboardGridClass, +} from "~/components/layout/dashboard-page"; +import { cn } from "~/lib/utils"; + +export function InvoiceDetailsSkeleton() { + return ( + + + + + + +
+
+ {/* Invoice Header Skeleton */} + + +
+
+
+
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + {/* Client & Business Info */} +
+ {/* Client Skeleton */} + + + + + + + + + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ + {/* Business Skeleton */} + + + + + + + + + +
+
+ + +
+
+ + +
+
+
+
+
+ + {/* Invoice Items Skeleton */} + + + + + + + + + {/* Item Rows */} + {Array.from({ length: 3 }).map((_, i) => ( + + +
+
+
+ +
+ + + +
+
+ +
+
+
+
+ ))} + + {/* Totals */} +
+
+
+ + +
+
+ + +
+ +
+ + +
+
+
+
+
+
+ + {/* Right Column - Actions */} +
+ + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/apps/web/src/app/dashboard/invoices/[id]/_components/invoice-items-table.tsx b/apps/web/src/app/dashboard/invoices/[id]/_components/invoice-items-table.tsx new file mode 100644 index 0000000..2f5a88f --- /dev/null +++ b/apps/web/src/app/dashboard/invoices/[id]/_components/invoice-items-table.tsx @@ -0,0 +1,126 @@ +"use client"; + +import type { ColumnDef } from "@tanstack/react-table"; +import { DataTable } from "~/components/data/data-table"; +import { + formatLineItemDetail, + isFixedLineItem, +} from "~/lib/invoice-line-item"; + +const formatDate = (date: Date) => { + return new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }).format(new Date(date)); +}; + +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount); +}; + +// Type for invoice item data +interface InvoiceItem { + id: string; + invoiceId: string; + date: Date; + description: string; + hours: number; + rate: number; + amount: number; + position: number; + createdAt: Date; +} + +interface InvoiceItemsTableProps { + items: InvoiceItem[]; +} + +const columns: ColumnDef[] = [ + { + accessorKey: "date", + header: "Date", + cell: ({ row }) => formatDate(row.getValue("date")), + meta: { + headerClassName: "hidden sm:table-cell", + cellClassName: "hidden sm:table-cell", + }, + }, + { + accessorKey: "description", + header: "Description", + cell: ({ row }) => { + const item = row.original; + return ( + <> + {/* Desktop: plain description */} +
{item.description}
+ {/* Mobile: description + date + hours @ rate stacked */} +
+

{item.description}

+

+ {formatDate(item.date)} ·{" "} + {formatLineItemDetail(item.hours, item.rate, formatCurrency)} +

+
+ + ); + }, + }, + { + accessorKey: "hours", + header: "Hours", + cell: ({ row }) => { + const hours = row.getValue("hours"); + return ( +
{isFixedLineItem(hours) ? "—" : hours}
+ ); + }, + meta: { + headerClassName: "hidden sm:table-cell", + cellClassName: "hidden sm:table-cell", + }, + }, + { + accessorKey: "rate", + header: "Rate", + cell: ({ row }) => { + const item = row.original; + return ( +
+ {isFixedLineItem(item.hours) + ? "—" + : `${formatCurrency(item.rate)}/hr`} +
+ ); + }, + meta: { + headerClassName: "hidden sm:table-cell", + cellClassName: "hidden sm:table-cell", + }, + }, + { + accessorKey: "amount", + header: "Amount", + cell: ({ row }) => ( +
+ {formatCurrency(row.getValue("amount"))} +
+ ), + }, +]; + +export function InvoiceItemsTable({ items }: InvoiceItemsTableProps) { + return ( + + ); +} diff --git a/apps/web/src/app/dashboard/invoices/[id]/_components/invoice-timer-card.tsx b/apps/web/src/app/dashboard/invoices/[id]/_components/invoice-timer-card.tsx new file mode 100644 index 0000000..1cf7c60 --- /dev/null +++ b/apps/web/src/app/dashboard/invoices/[id]/_components/invoice-timer-card.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { TimeClockPanel } from "~/components/time-clock/time-clock-panel"; + +interface InvoiceTimerCardProps { + invoiceId: string; + clientId: string; +} + +export function InvoiceTimerCard({ invoiceId, clientId }: InvoiceTimerCardProps) { + return ( + + ); +} diff --git a/apps/web/src/app/dashboard/invoices/[id]/_components/pdf-download-button.tsx b/apps/web/src/app/dashboard/invoices/[id]/_components/pdf-download-button.tsx new file mode 100644 index 0000000..866b273 --- /dev/null +++ b/apps/web/src/app/dashboard/invoices/[id]/_components/pdf-download-button.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "~/components/ui/button"; +import { toast } from "sonner"; +import { api } from "~/trpc/react"; +import { generateInvoicePDF } from "~/lib/pdf-export"; +import { Download, Loader2 } from "lucide-react"; + +interface PDFDownloadButtonProps { + invoiceId: string; + variant?: "default" | "outline" | "ghost" | "icon" | "secondary"; + className?: string; +} + +export function PDFDownloadButton({ + invoiceId, + variant = "outline", + className, +}: PDFDownloadButtonProps) { + const [isGenerating, setIsGenerating] = useState(false); + + // Fetch invoice data when PDF generation is triggered + const { refetch: fetchInvoice } = api.invoices.getById.useQuery( + { id: invoiceId }, + { enabled: false }, + ); + const { data: pdfSettings } = api.settings.getPdfSettings.useQuery(undefined, { + staleTime: 60_000, + }); + + const handleDownloadPDF = async () => { + if (isGenerating) return; + + setIsGenerating(true); + + try { + // Fetch fresh invoice data + const { data: invoiceData } = await fetchInvoice(); + + if (!invoiceData) { + throw new Error("Invoice not found"); + } + + // Map invoice to PDF format with currency support + const pdfData = { + invoiceNumber: invoiceData.invoiceNumber, + invoicePrefix: invoiceData.invoicePrefix, + issueDate: new Date(invoiceData.issueDate), + dueDate: new Date(invoiceData.dueDate), + status: invoiceData.status, + totalAmount: invoiceData.totalAmount, + taxRate: invoiceData.taxRate, + currency: invoiceData.currency ?? "USD", + notes: invoiceData.notes, + business: invoiceData.business, + client: invoiceData.client, + items: invoiceData.items, + }; + + await generateInvoicePDF(pdfData, { + pdfTemplate: pdfSettings?.pdfTemplate, + pdfAccentColor: pdfSettings?.pdfAccentColor, + pdfFontFamily: pdfSettings?.pdfFontFamily, + pdfNumericFontFamily: pdfSettings?.pdfNumericFontFamily, + pdfFooterText: pdfSettings?.pdfFooterText, + pdfShowLogo: pdfSettings?.pdfShowLogo, + pdfShowPageNumbers: pdfSettings?.pdfShowPageNumbers, + }); + toast.success("PDF downloaded successfully"); + } catch (error) { + console.error("PDF generation error:", error); + toast.error( + error instanceof Error ? error.message : "Failed to generate PDF", + ); + } finally { + setIsGenerating(false); + } + }; + + if (variant === "icon") { + return ( + + ); + } + + return ( + + ); +} diff --git a/apps/web/src/app/dashboard/invoices/[id]/edit/page.tsx b/apps/web/src/app/dashboard/invoices/[id]/edit/page.tsx new file mode 100644 index 0000000..f24b0f0 --- /dev/null +++ b/apps/web/src/app/dashboard/invoices/[id]/edit/page.tsx @@ -0,0 +1,22 @@ +import { redirect } from "next/navigation"; +import InvoiceForm from "~/components/forms/invoice-form"; +import { api } from "~/trpc/server"; + +interface EditInvoicePageProps { + params: Promise<{ id: string }>; +} + +export default async function EditInvoicePage({ params }: EditInvoicePageProps) { + const { id } = await params; + + try { + const invoice = await api.invoices.getById({ id }); + if (invoice.status !== "draft") { + redirect(`/dashboard/invoices/${id}?editBlocked=1`); + } + } catch { + redirect("/dashboard/invoices"); + } + + return ; +} diff --git a/apps/web/src/app/dashboard/invoices/[id]/page.tsx b/apps/web/src/app/dashboard/invoices/[id]/page.tsx new file mode 100644 index 0000000..25860cf --- /dev/null +++ b/apps/web/src/app/dashboard/invoices/[id]/page.tsx @@ -0,0 +1,946 @@ +"use client"; + +import { + AlertTriangle, + Bell, + Building, + Check, + Copy, + DollarSign, + Edit, + FileText, + Link2, + Link2Off, + Loader2, + Mail, + MapPin, + Phone, + Plus, + Trash2, + User, +} from "lucide-react"; +import Link from "next/link"; +import { notFound, useParams, useRouter, useSearchParams } from "next/navigation"; +import { useState, useEffect } from "react"; +import { toast } from "sonner"; +import { StatusBadge } from "~/components/data/status-badge"; +import { + DashboardPage, + dashboardGapClass, + dashboardGridClass, +} from "~/components/layout/dashboard-page"; +import { DashboardPageHeader } from "~/components/layout/page-header"; +import { cn } from "~/lib/utils"; +import { Button } from "~/components/ui/button"; +import { Badge } from "~/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "~/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { Separator } from "~/components/ui/separator"; +import { Textarea } from "~/components/ui/textarea"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { DatePicker } from "~/components/ui/date-picker"; +import { + getEffectiveInvoiceStatus, + isInvoiceOverdue, +} from "~/lib/invoice-status"; +import { api } from "~/trpc/react"; +import type { StoredInvoiceStatus } from "~/types/invoice"; +import { InvoiceDetailsSkeleton } from "./_components/invoice-details-skeleton"; +import { PDFDownloadButton } from "./_components/pdf-download-button"; +import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button"; +import { InvoiceTimerCard } from "./_components/invoice-timer-card"; + +const PAYMENT_METHODS = [ + { value: "cash", label: "Cash" }, + { value: "check", label: "Check" }, + { value: "bank_transfer", label: "Bank Transfer" }, + { value: "credit_card", label: "Credit Card" }, + { value: "paypal", label: "PayPal" }, + { value: "other", label: "Other" }, +] as const; + +function methodLabel(method: string) { + return PAYMENT_METHODS.find((m) => m.value === method)?.label ?? method; +} + +function daysSince(date: Date) { + return Math.floor((Date.now() - new Date(date).getTime()) / 86_400_000); +} + +function InvoiceViewContent({ invoiceId }: { invoiceId: string }) { + const router = useRouter(); + const searchParams = useSearchParams(); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [recordPaymentOpen, setRecordPaymentOpen] = useState(false); + const [reminderOpen, setReminderOpen] = useState(false); + const [shareOpen, setShareOpen] = useState(false); + const [paymentAmount, setPaymentAmount] = useState(""); + const [paymentMethod, setPaymentMethod] = useState("other"); + const [paymentNotes, setPaymentNotes] = useState(""); + const [reminderMessage, setReminderMessage] = useState(""); + const [copied, setCopied] = useState(false); + + const { data: invoice, isLoading } = api.invoices.getById.useQuery({ + id: invoiceId, + }); + const { data: payments, isLoading: paymentsLoading } = + api.payments.getByInvoice.useQuery({ invoiceId }); + const utils = api.useUtils(); + + useEffect(() => { + if (searchParams.get("editBlocked") === "1") { + toast.error("Only draft invoices can be edited"); + router.replace(`/dashboard/invoices/${invoiceId}`); + } + }, [searchParams, invoiceId, router]); + + const invalidate = () => { + void utils.invoices.getById.invalidate({ id: invoiceId }); + void utils.payments.getByInvoice.invalidate({ invoiceId }); + }; + + const deleteInvoice = api.invoices.delete.useMutation({ + onSuccess: () => { + toast.success("Invoice deleted"); + router.push("/dashboard/invoices"); + }, + onError: (e) => toast.error(e.message ?? "Failed to delete invoice"), + }); + + const updateStatus = api.invoices.updateStatus.useMutation({ + onSuccess: (data) => { + toast.success(data.message); + invalidate(); + }, + onError: (e) => toast.error(e.message ?? "Failed to update status"), + }); + + const createPayment = api.payments.create.useMutation({ + onSuccess: () => { + toast.success("Payment recorded"); + setRecordPaymentOpen(false); + setPaymentAmount(""); + setPaymentMethod("other"); + setPaymentNotes(""); + invalidate(); + }, + onError: (e) => toast.error(e.message ?? "Failed to record payment"), + }); + + const deletePayment = api.payments.delete.useMutation({ + onSuccess: () => { + toast.success("Payment removed"); + invalidate(); + }, + onError: (e) => toast.error(e.message ?? "Failed to remove payment"), + }); + + const generatePublicToken = api.invoices.generatePublicToken.useMutation({ + onSuccess: () => { + toast.success("Share link generated"); + void utils.invoices.getById.invalidate({ id: invoiceId }); + }, + onError: (e) => toast.error(e.message ?? "Failed to generate link"), + }); + + const revokePublicToken = api.invoices.revokePublicToken.useMutation({ + onSuccess: () => { + toast.success("Share link revoked"); + void utils.invoices.getById.invalidate({ id: invoiceId }); + }, + onError: (e) => toast.error(e.message ?? "Failed to revoke link"), + }); + + const sendReminder = api.invoices.sendReminder.useMutation({ + onSuccess: () => { + toast.success("Reminder sent"); + setReminderOpen(false); + setReminderMessage(""); + void utils.invoices.getById.invalidate({ id: invoiceId }); + }, + onError: (e) => toast.error(e.message ?? "Failed to send reminder"), + }); + + const updateInvoice = api.invoices.update.useMutation({ + onSuccess: () => { + toast.success("Reminder saved"); + void utils.invoices.getById.invalidate({ id: invoiceId }); + void utils.dashboard.getStats.invalidate(); + }, + onError: (e) => toast.error(e.message ?? "Failed to save reminder"), + }); + + if (isLoading) return ; + if (!invoice) notFound(); + + const formatDate = (date: Date) => + new Intl.DateTimeFormat("en-US", { year: "numeric", month: "short", day: "numeric" }).format( + new Date(date), + ); + + const formatCurrency = (amount: number, currency = invoice.currency) => + new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount); + + const subtotal = invoice.items.reduce((s, i) => s + i.amount, 0); + const taxAmount = (subtotal * invoice.taxRate) / 100; + const total = subtotal + taxAmount; + const totalPaid = (payments ?? []).reduce((s, p) => s + p.amount, 0); + const balanceDue = total - totalPaid; + const storedStatus = invoice.status as StoredInvoiceStatus; + const effectiveStatus = getEffectiveInvoiceStatus(storedStatus, invoice.dueDate); + const isOverdue = isInvoiceOverdue(storedStatus, invoice.dueDate); + const canSendReminder = effectiveStatus === "sent" || effectiveStatus === "overdue"; + + const publicUrl = invoice.publicToken + ? `${window.location.origin}/i/${invoice.publicToken}` + : null; + + const handleCopyLink = async () => { + if (!publicUrl) return; + await navigator.clipboard.writeText(publicUrl); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const handleRecordPayment = () => { + const amount = parseFloat(paymentAmount); + if (isNaN(amount) || amount <= 0) { + toast.error("Enter a valid payment amount"); + return; + } + createPayment.mutate({ + invoiceId, + amount, + date: new Date(), + method: paymentMethod as Parameters[0]["method"], + notes: paymentNotes || undefined, + }); + }; + + return ( + + + + {storedStatus === "draft" ? ( + + ) : null} + + +
+ {/* Left Column */} +
+ {/* Invoice Header */} + + +
+
+
+
+

+ {invoice.invoiceNumber} +

+ +
+
+
Issued {formatDate(invoice.issueDate)}
+
+ Due {formatDate(invoice.dueDate)} +
+
+
+
+

Total Amount

+

{formatCurrency(total)}

+ {totalPaid > 0 && balanceDue > 0 && ( +

+ Balance due: {formatCurrency(balanceDue)} +

+ )} +
+
+
+
+
+ + {/* Overdue Alert */} + {isOverdue && ( + + +
+ +
+

Invoice Overdue

+

+ {Math.ceil( + (new Date().getTime() - new Date(invoice.dueDate).getTime()) / + (1000 * 60 * 60 * 24), + )}{" "} + days past due date +

+
+
+
+
+ )} + + {/* Client & Business */} +
+ + + + + Bill To + + + +

{invoice.client.name}

+
+ {invoice.client.email && ( +
+
+ +
+ {invoice.client.email} +
+ )} + {invoice.client.phone && ( +
+
+ +
+ {invoice.client.phone} +
+ )} + {(invoice.client.addressLine1 ?? invoice.client.city) && ( +
+
+ +
+
+ {invoice.client.addressLine1 &&
{invoice.client.addressLine1}
} + {invoice.client.addressLine2 &&
{invoice.client.addressLine2}
} + {(invoice.client.city ?? + invoice.client.state ?? + invoice.client.postalCode) && ( +
+ {[ + invoice.client.city, + invoice.client.state, + invoice.client.postalCode, + ] + .filter(Boolean) + .join(", ")} +
+ )} + {invoice.client.country &&
{invoice.client.country}
} +
+
+ )} +
+
+
+ + {invoice.business && ( + + + + + From + + + + {invoice.business.logoStorageKey && ( +
+ {/* eslint-disable-next-line @next/next/no-img-element -- object-storage-backed image, not a static asset */} + {`${invoice.business.name} +
+ )} +

+ {invoice.business.name} +

+
+ {invoice.business.email && ( +
+
+ +
+ {invoice.business.email} +
+ )} + {invoice.business.phone && ( +
+
+ +
+ {invoice.business.phone} +
+ )} +
+
+
+ )} +
+ + {/* Invoice Items */} + + + + + Invoice Items + + + + {invoice.items.map((item) => ( + + +
+
+

+ {item.description} +

+
+ + {formatDate(item.date).replace(/ /g, " ")} + + + {item.hours.toString()} hours + + @ ${item.rate}/hr +
+
+

+ {formatCurrency(item.amount)} +

+
+
+
+ ))} + + {/* Totals */} +
+
+ Subtotal: + {formatCurrency(subtotal)} +
+ {invoice.taxRate > 0 && ( +
+ Tax ({invoice.taxRate}%): + {formatCurrency(taxAmount)} +
+ )} + +
+ Total: + {formatCurrency(total)} +
+ {totalPaid > 0 && ( + <> +
+ Paid: + + − {formatCurrency(totalPaid)} + +
+ +
+ Balance Due: + + {formatCurrency(Math.max(0, balanceDue))} + +
+ + )} +
+
+
+ + {/* Payments */} + + + + + + Payments + + + + + + {paymentsLoading ? ( +

Loading…

+ ) : (payments ?? []).length === 0 ? ( +

No payments recorded yet.

+ ) : ( +
+ {(payments ?? []).map((p) => ( +
+
+ {formatCurrency(p.amount)} + {methodLabel(p.method)} + {formatDate(p.date)} + {p.notes && ( + + {p.notes} + + )} +
+ +
+ ))} +
+ )} +
+
+ + {/* Notes */} + {invoice.notes && ( + + + Notes + + +

{invoice.notes}

+
+
+ )} +
+ + {/* Right Column - Actions */} +
+ {storedStatus === "draft" && ( + + )} + + + + + + Actions + + + + {storedStatus === "draft" ? ( + + ) : null} + + {invoice.items && invoice.client && ( + + )} + + {effectiveStatus === "draft" && ( + + )} + + {effectiveStatus === "draft" && ( + + updateInvoice.mutate({ + id: invoiceId, + sendReminderAt, + }) + } + onClear={() => + updateInvoice.mutate({ id: invoiceId, sendReminderAt: null }) + } + /> + )} + + {(effectiveStatus === "sent" || effectiveStatus === "overdue") && ( + + )} + + {/* Send Reminder */} + {canSendReminder && ( +
+ + {invoice.lastReminderSentAt && ( +

+ Last sent {daysSince(invoice.lastReminderSentAt)} day + {daysSince(invoice.lastReminderSentAt) === 1 ? "" : "s"} ago +

+ )} +
+ )} + + {/* Share Link */} + + + + + +

Client share link

+ {publicUrl ? ( + <> +
+

{publicUrl}

+ +
+ + + ) : ( + <> +

+ Generate a shareable link your client can use to view this invoice without + logging in. +

+ + + )} +
+
+ + {/* Mark as Paid */} + {(effectiveStatus === "sent" || effectiveStatus === "overdue") && ( + + )} + + +
+
+
+
+ + {/* Record Payment Dialog */} + + + + Record Payment + + Record a payment received for invoice {invoice.invoiceNumber}. + + +
+
+ + setPaymentAmount(e.target.value)} + /> +
+
+ + +
+
+ + setPaymentNotes(e.target.value)} + /> +
+
+ + + + +
+
+ + {/* Send Reminder Dialog */} + + + + Send Reminder + + Send a payment reminder to {invoice.client.name} for invoice{" "} + {invoice.invoiceNumber}. + + +
+ +