From 8e5f4aafa2ee03d7834561f3e326aa086c2c7f69 Mon Sep 17 00:00:00 2001 From: Alessio Date: Tue, 2 Jun 2026 04:02:59 +0200 Subject: [PATCH] first commit --- .dockerignore | 5 + .gitignore | 2 + Dockerfile | 23 + README.md | 109 +++- database/init.sql | 150 ++++++ .../migrations/001_subscription_lifecycle.sql | 104 ++++ .../002_subscription_custom_intervals.sql | 49 ++ .../003_subscription_ledger_renewal_dates.sql | 7 + database/migrations/004_income_titles.sql | 32 ++ database/migrations/005_user_avatar.sql | 2 + docker-compose.yml | 35 ++ next.config.ts | 3 +- package.json | 6 +- pnpm-lock.yaml | 38 +- pnpm-workspace.yaml | 2 + public/logo.png | Bin 0 -> 198643 bytes src/app/(protected)/bills/page.tsx | 5 + src/app/(protected)/dashboard/page.tsx | 5 + src/app/(protected)/income/page.tsx | 5 + src/app/(protected)/layout.tsx | 17 + .../(protected)/one-time-payments/page.tsx | 5 + src/app/(protected)/profile/page.tsx | 8 + src/app/(protected)/reports/page.tsx | 17 + src/app/(protected)/subscriptions/page.tsx | 5 + src/app/(public)/login/page.tsx | 11 + src/app/(public)/setup/page.tsx | 11 + src/app/api/auth/login/route.ts | 39 ++ src/app/api/auth/logout/route.ts | 6 + src/app/api/auth/session/route.ts | 6 + src/app/api/auth/setup/route.ts | 49 ++ src/app/api/bills/[id]/route.ts | 55 ++ src/app/api/bills/route.ts | 46 ++ src/app/api/income/[id]/route.ts | 38 ++ src/app/api/income/route.ts | 37 ++ src/app/api/one-time-payments/[id]/route.ts | 67 +++ src/app/api/one-time-payments/route.ts | 54 ++ src/app/api/payment-titles/route.ts | 49 ++ src/app/api/payments-ledger/route.ts | 18 + src/app/api/profile/account/route.ts | 120 +++++ src/app/api/profile/avatar/route.ts | 98 ++++ src/app/api/profile/data/route.ts | 37 ++ src/app/api/profile/settings/route.ts | 19 + .../api/subscription-payments/[id]/route.ts | 79 +++ src/app/api/subscription-payments/route.ts | 160 ++++++ src/app/api/subscriptions/[id]/route.ts | 79 +++ src/app/api/subscriptions/route.ts | 40 ++ src/app/globals.css | 91 +++- src/app/layout.tsx | 9 +- src/app/page.tsx | 70 +-- src/components/AppShell.tsx | 12 + src/components/AuthForm.tsx | 75 +++ src/components/SetupForm.tsx | 82 +++ src/components/bills/BillForm.tsx | 162 ++++++ src/components/bills/BillList.tsx | 109 ++++ src/components/bills/BillStatusBadge.tsx | 13 + src/components/bills/BillsPageClient.tsx | 162 ++++++ src/components/dashboard/DashboardCards.tsx | 35 ++ src/components/dashboard/DashboardKpiRow.tsx | 48 ++ .../dashboard/DashboardPageClient.tsx | 104 ++++ .../dashboard/DashboardStatCard.tsx | 62 +++ .../dashboard/ExpenseDistribution.tsx | 126 +++++ src/components/dashboard/FinancialSummary.tsx | 46 ++ src/components/dashboard/GoalProgress.tsx | 62 +++ src/components/dashboard/UpcomingPayments.tsx | 127 +++++ src/components/dashboard/YearSelector.tsx | 45 ++ src/components/income/IncomeForm.tsx | 132 +++++ src/components/income/IncomeList.tsx | 83 +++ src/components/income/IncomePageClient.tsx | 138 +++++ src/components/income/IncomePeriodToolbar.tsx | 26 + .../one-time-payments/OneTimePaymentForm.tsx | 116 ++++ .../one-time-payments/OneTimePaymentList.tsx | 84 +++ .../OneTimePaymentsPageClient.tsx | 147 +++++ src/components/profile/AccountCard.tsx | 148 +++++ src/components/profile/AccountEditModal.tsx | 95 ++++ src/components/profile/AvatarCropModal.tsx | 306 +++++++++++ src/components/profile/ConfirmDangerModal.tsx | 45 ++ src/components/profile/DangerZoneCard.tsx | 34 ++ src/components/profile/DeleteAccountModal.tsx | 77 +++ src/components/profile/PasswordEditModal.tsx | 118 ++++ src/components/profile/ProfileCard.tsx | 25 + src/components/profile/ProfilePageClient.tsx | 225 ++++++++ src/components/reports/BillsInsights.tsx | 66 +++ src/components/reports/ExpenseBreakdown.tsx | 41 ++ src/components/reports/MonthSummary.tsx | 48 ++ src/components/reports/ReportsPageClient.tsx | 186 +++++++ .../reports/SubscriptionInsights.tsx | 63 +++ src/components/shared/AnnualPageToolbar.tsx | 20 + src/components/shared/Badge.tsx | 26 + src/components/shared/DateField.tsx | 220 ++++++++ src/components/shared/EmptyState.tsx | 8 + src/components/shared/EntityFormModal.tsx | 126 +++++ src/components/shared/Field.tsx | 17 + src/components/shared/Header.tsx | 54 ++ src/components/shared/ListCardAction.tsx | 90 ++++ src/components/shared/LocalMonthNavigator.tsx | 148 +++++ src/components/shared/MonthlyPageToolbar.tsx | 29 + src/components/shared/PageShell.tsx | 26 + src/components/shared/PaymentBadge.tsx | 20 + src/components/shared/SectionAddButton.tsx | 19 + src/components/shared/SectionPeriodTabs.tsx | 57 ++ src/components/shared/Select.tsx | 178 ++++++ src/components/shared/SimpleFormSteps.tsx | 107 ++++ src/components/shared/TitleCombobox.tsx | 192 +++++++ src/components/shared/Toast.tsx | 53 ++ src/components/shared/UserAvatar.tsx | 47 ++ src/components/shared/financeListCard.ts | 2 + .../shared/navigation/MobileSidebar.tsx | 89 +++ src/components/shared/navigation/Sidebar.tsx | 122 +++++ .../shared/navigation/SidebarOverlay.tsx | 19 + .../shared/navigation/SidebarTrigger.tsx | 24 + src/components/shared/useFinanceData.ts | 231 ++++++++ .../subscriptions/SubscriptionForm.tsx | 508 ++++++++++++++++++ .../subscriptions/SubscriptionList.tsx | 142 +++++ .../SubscriptionOccurrenceList.tsx | 69 +++ .../subscriptions/SubscriptionStatusBadge.tsx | 23 + .../subscriptions/SubscriptionsPageClient.tsx | 262 +++++++++ src/lib/auth.ts | 9 + src/lib/calendar/calendarEvents.ts | 112 ++++ src/lib/calendar/month.ts | 31 ++ src/lib/database/server.ts | 321 +++++++++++ src/lib/database/session.ts | 88 +++ src/lib/database/setup.ts | 10 + src/lib/date/formatDate.ts | 95 ++++ src/lib/date/parseItalianDate.ts | 26 + src/lib/finance/billStatus.ts | 12 + src/lib/finance/dashboardSummary.ts | 69 +++ src/lib/finance/expenseDistribution.ts | 64 +++ src/lib/finance/labels.ts | 27 + src/lib/finance/ledger.ts | 203 +++++++ src/lib/finance/money.ts | 35 ++ src/lib/finance/paymentVisualStyle.ts | 128 +++++ src/lib/finance/projections.ts | 34 ++ src/lib/finance/reportAnalysis.ts | 67 +++ src/lib/finance/subscriptionOccurrences.ts | 196 +++++++ src/lib/finance/subscriptionStatus.ts | 17 + src/lib/finance/upcomingPayments.ts | 79 +++ src/lib/finance/yearlySummary.ts | 140 +++++ src/lib/profile/avatarStorage.ts | 111 ++++ src/lib/profile/settings.ts | 51 ++ src/lib/validation/account.ts | 30 ++ src/lib/validation/bills.ts | 26 + src/lib/validation/common.ts | 80 +++ src/lib/validation/income.ts | 20 + src/lib/validation/oneTimePayments.ts | 15 + src/lib/validation/paymentTitles.ts | 23 + src/lib/validation/subscriptions.ts | 83 +++ src/lib/validation/userSettings.ts | 32 ++ src/types/finance.ts | 143 +++++ 148 files changed, 10590 insertions(+), 110 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 database/init.sql create mode 100644 database/migrations/001_subscription_lifecycle.sql create mode 100644 database/migrations/002_subscription_custom_intervals.sql create mode 100644 database/migrations/003_subscription_ledger_renewal_dates.sql create mode 100644 database/migrations/004_income_titles.sql create mode 100644 database/migrations/005_user_avatar.sql create mode 100644 docker-compose.yml create mode 100644 public/logo.png create mode 100644 src/app/(protected)/bills/page.tsx create mode 100644 src/app/(protected)/dashboard/page.tsx create mode 100644 src/app/(protected)/income/page.tsx create mode 100644 src/app/(protected)/layout.tsx create mode 100644 src/app/(protected)/one-time-payments/page.tsx create mode 100644 src/app/(protected)/profile/page.tsx create mode 100644 src/app/(protected)/reports/page.tsx create mode 100644 src/app/(protected)/subscriptions/page.tsx create mode 100644 src/app/(public)/login/page.tsx create mode 100644 src/app/(public)/setup/page.tsx create mode 100644 src/app/api/auth/login/route.ts create mode 100644 src/app/api/auth/logout/route.ts create mode 100644 src/app/api/auth/session/route.ts create mode 100644 src/app/api/auth/setup/route.ts create mode 100644 src/app/api/bills/[id]/route.ts create mode 100644 src/app/api/bills/route.ts create mode 100644 src/app/api/income/[id]/route.ts create mode 100644 src/app/api/income/route.ts create mode 100644 src/app/api/one-time-payments/[id]/route.ts create mode 100644 src/app/api/one-time-payments/route.ts create mode 100644 src/app/api/payment-titles/route.ts create mode 100644 src/app/api/payments-ledger/route.ts create mode 100644 src/app/api/profile/account/route.ts create mode 100644 src/app/api/profile/avatar/route.ts create mode 100644 src/app/api/profile/data/route.ts create mode 100644 src/app/api/profile/settings/route.ts create mode 100644 src/app/api/subscription-payments/[id]/route.ts create mode 100644 src/app/api/subscription-payments/route.ts create mode 100644 src/app/api/subscriptions/[id]/route.ts create mode 100644 src/app/api/subscriptions/route.ts create mode 100644 src/components/AppShell.tsx create mode 100644 src/components/AuthForm.tsx create mode 100644 src/components/SetupForm.tsx create mode 100644 src/components/bills/BillForm.tsx create mode 100644 src/components/bills/BillList.tsx create mode 100644 src/components/bills/BillStatusBadge.tsx create mode 100644 src/components/bills/BillsPageClient.tsx create mode 100644 src/components/dashboard/DashboardCards.tsx create mode 100644 src/components/dashboard/DashboardKpiRow.tsx create mode 100644 src/components/dashboard/DashboardPageClient.tsx create mode 100644 src/components/dashboard/DashboardStatCard.tsx create mode 100644 src/components/dashboard/ExpenseDistribution.tsx create mode 100644 src/components/dashboard/FinancialSummary.tsx create mode 100644 src/components/dashboard/GoalProgress.tsx create mode 100644 src/components/dashboard/UpcomingPayments.tsx create mode 100644 src/components/dashboard/YearSelector.tsx create mode 100644 src/components/income/IncomeForm.tsx create mode 100644 src/components/income/IncomeList.tsx create mode 100644 src/components/income/IncomePageClient.tsx create mode 100644 src/components/income/IncomePeriodToolbar.tsx create mode 100644 src/components/one-time-payments/OneTimePaymentForm.tsx create mode 100644 src/components/one-time-payments/OneTimePaymentList.tsx create mode 100644 src/components/one-time-payments/OneTimePaymentsPageClient.tsx create mode 100644 src/components/profile/AccountCard.tsx create mode 100644 src/components/profile/AccountEditModal.tsx create mode 100644 src/components/profile/AvatarCropModal.tsx create mode 100644 src/components/profile/ConfirmDangerModal.tsx create mode 100644 src/components/profile/DangerZoneCard.tsx create mode 100644 src/components/profile/DeleteAccountModal.tsx create mode 100644 src/components/profile/PasswordEditModal.tsx create mode 100644 src/components/profile/ProfileCard.tsx create mode 100644 src/components/profile/ProfilePageClient.tsx create mode 100644 src/components/reports/BillsInsights.tsx create mode 100644 src/components/reports/ExpenseBreakdown.tsx create mode 100644 src/components/reports/MonthSummary.tsx create mode 100644 src/components/reports/ReportsPageClient.tsx create mode 100644 src/components/reports/SubscriptionInsights.tsx create mode 100644 src/components/shared/AnnualPageToolbar.tsx create mode 100644 src/components/shared/Badge.tsx create mode 100644 src/components/shared/DateField.tsx create mode 100644 src/components/shared/EmptyState.tsx create mode 100644 src/components/shared/EntityFormModal.tsx create mode 100644 src/components/shared/Field.tsx create mode 100644 src/components/shared/Header.tsx create mode 100644 src/components/shared/ListCardAction.tsx create mode 100644 src/components/shared/LocalMonthNavigator.tsx create mode 100644 src/components/shared/MonthlyPageToolbar.tsx create mode 100644 src/components/shared/PageShell.tsx create mode 100644 src/components/shared/PaymentBadge.tsx create mode 100644 src/components/shared/SectionAddButton.tsx create mode 100644 src/components/shared/SectionPeriodTabs.tsx create mode 100644 src/components/shared/Select.tsx create mode 100644 src/components/shared/SimpleFormSteps.tsx create mode 100644 src/components/shared/TitleCombobox.tsx create mode 100644 src/components/shared/Toast.tsx create mode 100644 src/components/shared/UserAvatar.tsx create mode 100644 src/components/shared/financeListCard.ts create mode 100644 src/components/shared/navigation/MobileSidebar.tsx create mode 100644 src/components/shared/navigation/Sidebar.tsx create mode 100644 src/components/shared/navigation/SidebarOverlay.tsx create mode 100644 src/components/shared/navigation/SidebarTrigger.tsx create mode 100644 src/components/shared/useFinanceData.ts create mode 100644 src/components/subscriptions/SubscriptionForm.tsx create mode 100644 src/components/subscriptions/SubscriptionList.tsx create mode 100644 src/components/subscriptions/SubscriptionOccurrenceList.tsx create mode 100644 src/components/subscriptions/SubscriptionStatusBadge.tsx create mode 100644 src/components/subscriptions/SubscriptionsPageClient.tsx create mode 100644 src/lib/auth.ts create mode 100644 src/lib/calendar/calendarEvents.ts create mode 100644 src/lib/calendar/month.ts create mode 100644 src/lib/database/server.ts create mode 100644 src/lib/database/session.ts create mode 100644 src/lib/database/setup.ts create mode 100644 src/lib/date/formatDate.ts create mode 100644 src/lib/date/parseItalianDate.ts create mode 100644 src/lib/finance/billStatus.ts create mode 100644 src/lib/finance/dashboardSummary.ts create mode 100644 src/lib/finance/expenseDistribution.ts create mode 100644 src/lib/finance/labels.ts create mode 100644 src/lib/finance/ledger.ts create mode 100644 src/lib/finance/money.ts create mode 100644 src/lib/finance/paymentVisualStyle.ts create mode 100644 src/lib/finance/projections.ts create mode 100644 src/lib/finance/reportAnalysis.ts create mode 100644 src/lib/finance/subscriptionOccurrences.ts create mode 100644 src/lib/finance/subscriptionStatus.ts create mode 100644 src/lib/finance/upcomingPayments.ts create mode 100644 src/lib/finance/yearlySummary.ts create mode 100644 src/lib/profile/avatarStorage.ts create mode 100644 src/lib/profile/settings.ts create mode 100644 src/lib/validation/account.ts create mode 100644 src/lib/validation/bills.ts create mode 100644 src/lib/validation/common.ts create mode 100644 src/lib/validation/income.ts create mode 100644 src/lib/validation/oneTimePayments.ts create mode 100644 src/lib/validation/paymentTitles.ts create mode 100644 src/lib/validation/subscriptions.ts create mode 100644 src/lib/validation/userSettings.ts create mode 100644 src/types/finance.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..320297d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.next +.git +.env* +*.tsbuildinfo diff --git a/.gitignore b/.gitignore index 5ef6a52..a84f41a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ # misc .DS_Store *.pem +/.data/ # debug npm-debug.log* @@ -32,6 +33,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2a98a2f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM node:22-alpine AS base +RUN corepack enable +WORKDIR /app + +FROM base AS dependencies +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +RUN pnpm install --frozen-lockfile + +FROM base AS builder +COPY --from=dependencies /app/node_modules ./node_modules +COPY . . +RUN pnpm build + +FROM node:22-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/README.md b/README.md index e215bc4..3e72e8a 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,103 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# App Spese -## Getting Started +Full-stack personal finance app built with Next.js App Router, TypeScript, +Tailwind CSS, and PostgreSQL. -First, run the development server: +The browser only talks to Next.js Route Handlers. PostgreSQL credentials and +session tokens stay on the server. + +## Docker Compose + +Copy the example environment file and choose a strong database password: ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +cp .env.example .env +docker compose up --build -d ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +The first startup initializes the PostgreSQL schema from `database/init.sql`. +Open `http://localhost:3000`: the app redirects to `/setup` and asks you to +create the first account. The setup page becomes unavailable as soon as that +account exists. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Local Development -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +Start PostgreSQL with Docker: -## Learn More +```bash +docker compose up -d postgres +``` -To learn more about Next.js, take a look at the following resources: +The compose file publishes PostgreSQL on `localhost:5432` by default so the +local Next.js development server can reach it. -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +Create `.env.local`: -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +```env +DATABASE_URL=postgresql://appspese:change-me@localhost:5432/appspese +``` -## Deploy on Vercel +Install dependencies and run Next.js: -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +```bash +pnpm install +pnpm dev +``` -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +## Data Model + +Tables: + +- `users` and `sessions`: local credentials and revocable login sessions. +- `subscriptions`: recurring payment rules. +- `subscription_payments`: paid or unpaid state for generated renewals. +- `bills`: expected manual bills with due dates. +- `one_time_payments`: immediate manual outgoing payments. +- `payments_ledger`: single source of truth for real paid outgoing money. +- `payment_titles`: reusable deduplicated payment names. +- `income`: incoming money. +- `user_settings`: one row of preferences per user. + +Subscriptions and bills count as projected obligations until paid. Paid +outgoing money is written to `payments_ledger`, with unique database constraints +preventing duplicate ledger rows. + +Subscriptions keep service activation and billing separate. Recurring payments +start from the first payment date, can stop at an optional end date or after an +optional number of installments, and may otherwise continue indefinitely. +Suspended and terminated subscriptions do not generate new virtual renewals, +while already recorded payments remain available as history. +Custom renewal intervals use an exact quantity and unit, supporting schedules +such as every two weeks, every three months, or every two years. + +## Database Migrations + +`database/init.sql` contains the complete schema for new installations. +Versioned files in `database/migrations` update existing volumes. After pulling +an update that includes a new migration, apply it once with: + +```bash +cat database/migrations/001_subscription_lifecycle.sql | docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U appspese -d appspese +cat database/migrations/002_subscription_custom_intervals.sql | docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U appspese -d appspese +``` + +## Authentication + +There is no public registration page. The `/setup` page and its API endpoint are +available only while the database contains no users. The first request that +creates an account takes a PostgreSQL transaction lock, preventing concurrent +requests from creating multiple initial accounts. + +- Passwords are hashed with bcrypt. +- Login sessions use random secrets stored in an HTTP-only cookie. +- Only a SHA-256 hash of each session secret is stored in PostgreSQL. +- API Route Handlers verify the current user server-side and ignore + client-provided user identifiers. + +## Verification + +```bash +pnpm lint +pnpm build +docker compose config +``` diff --git a/database/init.sql b/database/init.sql new file mode 100644 index 0000000..0239d94 --- /dev/null +++ b/database/init.sql @@ -0,0 +1,150 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE users ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + email text NOT NULL UNIQUE, + password_hash text NOT NULL, + avatar_file text, + created_at timestamptz NOT NULL DEFAULT NOW(), + updated_at timestamptz NOT NULL DEFAULT NOW() +); + +CREATE TABLE sessions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash text NOT NULL UNIQUE, + expires_at timestamptz NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW() +); +CREATE INDEX sessions_user_id_idx ON sessions(user_id); +CREATE INDEX sessions_expires_at_idx ON sessions(expires_at); + +CREATE TABLE subscriptions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title text NOT NULL, + amount double precision NOT NULL, + billing_cycle text NOT NULL, + activation_date text NOT NULL, + first_payment_date text NOT NULL, + end_date text, + occurrence_count integer CHECK (occurrence_count > 0), + custom_interval_value integer CHECK (custom_interval_value > 0), + custom_interval_unit text CHECK ( + custom_interval_unit IN ('days', 'weeks', 'months', 'years') + ), + subscription_status text NOT NULL, + created_at text NOT NULL, + updated_at text NOT NULL, + CONSTRAINT subscriptions_amount_positive CHECK (amount > 0), + CONSTRAINT subscriptions_billing_cycle_valid CHECK ( + billing_cycle IN ('weekly', 'monthly', 'yearly', 'custom') + ), + CONSTRAINT subscriptions_status_valid CHECK ( + subscription_status IN ('active', 'suspended', 'terminated') + ), + CONSTRAINT subscriptions_dates_valid CHECK ( + activation_date <= first_payment_date + AND (end_date IS NULL OR end_date >= first_payment_date) + ) +); +CREATE INDEX subscriptions_user_id_idx ON subscriptions(user_id); +CREATE INDEX subscriptions_first_payment_date_idx ON subscriptions(first_payment_date); + +CREATE TABLE subscription_payments ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + subscription_id uuid NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE, + renewal_date text NOT NULL, + status text NOT NULL, + payment_date text, + linked_payment_id uuid, + created_at text NOT NULL, + updated_at text NOT NULL, + UNIQUE (subscription_id, renewal_date, user_id) +); +CREATE INDEX subscription_payments_user_id_idx ON subscription_payments(user_id); + +CREATE TABLE bills ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title text NOT NULL, + amount double precision NOT NULL, + due_date text NOT NULL, + status text NOT NULL, + payment_date text, + linked_payment_id uuid, + created_at text NOT NULL, + updated_at text NOT NULL +); +CREATE INDEX bills_user_id_idx ON bills(user_id); +CREATE INDEX bills_due_date_idx ON bills(due_date); + +CREATE TABLE one_time_payments ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title text NOT NULL, + amount double precision NOT NULL, + date text NOT NULL, + linked_payment_id uuid, + created_at text NOT NULL, + updated_at text NOT NULL +); +CREATE INDEX one_time_payments_user_id_idx ON one_time_payments(user_id); + +CREATE TABLE payments_ledger ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title text NOT NULL, + amount double precision NOT NULL, + date text NOT NULL, + source_type text NOT NULL, + source_id uuid NOT NULL, + created_at text NOT NULL, + updated_at text NOT NULL, + UNIQUE (source_type, source_id, user_id) +); +CREATE INDEX payments_ledger_user_id_idx ON payments_ledger(user_id); +CREATE INDEX payments_ledger_date_idx ON payments_ledger(date); + +CREATE TABLE payment_titles ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name text NOT NULL, + normalized_name text NOT NULL, + payment_type text NOT NULL, + created_at text NOT NULL, + updated_at text NOT NULL, + CONSTRAINT payment_titles_type_valid CHECK ( + payment_type IN ('subscription', 'bill', 'one_time', 'income', 'any') + ), + UNIQUE (user_id, normalized_name, payment_type) +); +CREATE INDEX payment_titles_user_id_idx ON payment_titles(user_id); + +CREATE TABLE income ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title text NOT NULL, + amount double precision NOT NULL, + type text NOT NULL, + date text NOT NULL, + created_at text NOT NULL, + updated_at text NOT NULL +); +CREATE INDEX income_user_id_idx ON income(user_id); + +CREATE TABLE user_settings ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, + full_name text, + monthly_income_goal double precision, + monthly_savings_goal double precision, + currency text NOT NULL, + locale text NOT NULL, + first_day_of_week text NOT NULL, + default_dashboard_view text NOT NULL, + created_at text NOT NULL, + updated_at text NOT NULL +); diff --git a/database/migrations/001_subscription_lifecycle.sql b/database/migrations/001_subscription_lifecycle.sql new file mode 100644 index 0000000..1fb5f94 --- /dev/null +++ b/database/migrations/001_subscription_lifecycle.sql @@ -0,0 +1,104 @@ +BEGIN; + +ALTER TABLE subscriptions + ADD COLUMN IF NOT EXISTS activation_date text, + ADD COLUMN IF NOT EXISTS first_payment_date text, + ADD COLUMN IF NOT EXISTS end_date text, + ADD COLUMN IF NOT EXISTS occurrence_count integer, + ADD COLUMN IF NOT EXISTS custom_interval_days integer; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'subscriptions' AND column_name = 'renewal_date' + ) THEN + UPDATE subscriptions + SET + activation_date = COALESCE(activation_date, renewal_date), + first_payment_date = COALESCE(first_payment_date, renewal_date), + subscription_status = CASE + WHEN subscription_status = 'disabled' THEN 'terminated' + ELSE subscription_status + END; + ELSE + UPDATE subscriptions + SET subscription_status = 'terminated' + WHERE subscription_status = 'disabled'; + END IF; +END $$; + +ALTER TABLE subscriptions + ALTER COLUMN activation_date SET NOT NULL, + ALTER COLUMN first_payment_date SET NOT NULL; + +ALTER TABLE subscriptions + DROP COLUMN IF EXISTS renewal_date, + DROP COLUMN IF EXISTS auto_renew; + +DROP INDEX IF EXISTS subscriptions_renewal_date_idx; +CREATE INDEX IF NOT EXISTS subscriptions_first_payment_date_idx + ON subscriptions(first_payment_date); + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'subscriptions_occurrence_count_positive' + ) THEN + ALTER TABLE subscriptions + ADD CONSTRAINT subscriptions_occurrence_count_positive + CHECK (occurrence_count > 0); + END IF; + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'subscriptions_custom_interval_days_positive' + ) THEN + ALTER TABLE subscriptions + ADD CONSTRAINT subscriptions_custom_interval_days_positive + CHECK (custom_interval_days > 0); + END IF; + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'subscriptions_amount_positive' + ) THEN + ALTER TABLE subscriptions + ADD CONSTRAINT subscriptions_amount_positive CHECK (amount > 0); + END IF; + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'subscriptions_billing_cycle_valid' + ) THEN + ALTER TABLE subscriptions + ADD CONSTRAINT subscriptions_billing_cycle_valid + CHECK (billing_cycle IN ('weekly', 'monthly', 'yearly', 'custom')); + END IF; + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'subscriptions_status_valid' + ) THEN + ALTER TABLE subscriptions + ADD CONSTRAINT subscriptions_status_valid + CHECK (subscription_status IN ('active', 'suspended', 'terminated')); + END IF; + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'subscriptions_dates_valid' + ) THEN + ALTER TABLE subscriptions + ADD CONSTRAINT subscriptions_dates_valid + CHECK ( + activation_date <= first_payment_date + AND (end_date IS NULL OR end_date >= first_payment_date) + ); + END IF; +END $$; + +COMMIT; diff --git a/database/migrations/002_subscription_custom_intervals.sql b/database/migrations/002_subscription_custom_intervals.sql new file mode 100644 index 0000000..c38b6f7 --- /dev/null +++ b/database/migrations/002_subscription_custom_intervals.sql @@ -0,0 +1,49 @@ +BEGIN; + +ALTER TABLE subscriptions + ADD COLUMN IF NOT EXISTS custom_interval_value integer, + ADD COLUMN IF NOT EXISTS custom_interval_unit text; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'subscriptions' AND column_name = 'custom_interval_days' + ) THEN + UPDATE subscriptions + SET + custom_interval_value = COALESCE(custom_interval_value, custom_interval_days), + custom_interval_unit = CASE + WHEN custom_interval_days IS NOT NULL THEN COALESCE(custom_interval_unit, 'days') + ELSE custom_interval_unit + END; + END IF; +END $$; + +ALTER TABLE subscriptions + DROP COLUMN IF EXISTS custom_interval_days; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'subscriptions_custom_interval_value_positive' + ) THEN + ALTER TABLE subscriptions + ADD CONSTRAINT subscriptions_custom_interval_value_positive + CHECK (custom_interval_value > 0); + END IF; + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'subscriptions_custom_interval_unit_valid' + ) THEN + ALTER TABLE subscriptions + ADD CONSTRAINT subscriptions_custom_interval_unit_valid + CHECK (custom_interval_unit IN ('days', 'weeks', 'months', 'years')); + END IF; +END $$; + +COMMIT; diff --git a/database/migrations/003_subscription_ledger_renewal_dates.sql b/database/migrations/003_subscription_ledger_renewal_dates.sql new file mode 100644 index 0000000..db398f3 --- /dev/null +++ b/database/migrations/003_subscription_ledger_renewal_dates.sql @@ -0,0 +1,7 @@ +UPDATE payments_ledger AS ledger +SET date = payment.renewal_date, + updated_at = NOW() +FROM subscription_payments AS payment +WHERE ledger.source_type = 'subscription' + AND ledger.source_id = payment.id + AND ledger.date IS DISTINCT FROM payment.renewal_date; diff --git a/database/migrations/004_income_titles.sql b/database/migrations/004_income_titles.sql new file mode 100644 index 0000000..8afc1ce --- /dev/null +++ b/database/migrations/004_income_titles.sql @@ -0,0 +1,32 @@ +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'payment_titles_type_valid' + ) THEN + ALTER TABLE payment_titles + ADD CONSTRAINT payment_titles_type_valid CHECK ( + payment_type IN ('subscription', 'bill', 'one_time', 'income', 'any') + ); + END IF; +END $$; + +INSERT INTO payment_titles ( + user_id, + name, + normalized_name, + payment_type, + created_at, + updated_at +) +SELECT DISTINCT + user_id, + title, + LOWER(REGEXP_REPLACE(TRIM(title), '\s+', ' ', 'g')), + 'income', + NOW()::text, + NOW()::text +FROM income +WHERE TRIM(title) <> '' +ON CONFLICT (user_id, normalized_name, payment_type) DO NOTHING; diff --git a/database/migrations/005_user_avatar.sql b/database/migrations/005_user_avatar.sql new file mode 100644 index 0000000..376fbe4 --- /dev/null +++ b/database/migrations/005_user_avatar.sql @@ -0,0 +1,2 @@ +ALTER TABLE users + ADD COLUMN IF NOT EXISTS avatar_file text; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..591de70 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +services: + app: + build: . + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-appspese}:${POSTGRES_PASSWORD:-change-me}@postgres:5432/${POSTGRES_DB:-appspese} + AVATAR_STORAGE_DIR: /data/avatars + ports: + - "${APP_PORT:-3000}:3000" + volumes: + - ./.data/avatars:/data/avatars + + postgres: + image: postgres:17-alpine + restart: unless-stopped + ports: + - "${POSTGRES_PORT:-5432}:5432" + environment: + POSTGRES_DB: ${POSTGRES_DB:-appspese} + POSTGRES_USER: ${POSTGRES_USER:-appspese} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-appspese} -d ${POSTGRES_DB:-appspese}"] + interval: 5s + timeout: 5s + retries: 10 + volumes: + - postgres_data:/var/lib/postgresql/data + - ./database/init.sql:/docker-entrypoint-initdb.d/001-init.sql:ro + +volumes: + postgres_data: diff --git a/next.config.ts b/next.config.ts index e9ffa30..dbbd989 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,8 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", + allowedDevOrigins: ["100.64.0.3"], }; export default nextConfig; diff --git a/package.json b/package.json index d27aac9..ce18567 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "appspese", "version": "0.1.0", "private": true, + "packageManager": "pnpm@11.4.0", "scripts": { "dev": "next dev", "build": "next build", @@ -10,9 +11,12 @@ "format": "biome format --write" }, "dependencies": { + "bcryptjs": "^3.0.3", "next": "16.2.6", + "postgres": "^3.4.9", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "react-icons": "^5.6.0" }, "devDependencies": { "@biomejs/biome": "2.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dafec85..26c06ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,24 @@ importers: .: dependencies: + bcryptjs: + specifier: ^3.0.3 + version: 3.0.3 next: specifier: 16.2.6 version: 16.2.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + postgres: + specifier: ^3.4.9 + version: 3.4.9 react: specifier: 19.2.4 version: 19.2.4 react-dom: specifier: 19.2.4 version: 19.2.4(react@19.2.4) + react-icons: + specifier: ^5.6.0 + version: 5.6.0(react@19.2.4) devDependencies: '@biomejs/biome': specifier: 2.2.0 @@ -441,6 +450,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} @@ -454,8 +467,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - enhanced-resolve@5.22.1: - resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + enhanced-resolve@5.22.0: + resolution: {integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==} engines: {node: '>=10.13.0'} graceful-fs@4.2.11: @@ -579,11 +592,20 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + postgres@3.4.9: + resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} + engines: {node: '>=12'} + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: react: ^19.2.4 + react-icons@5.6.0: + resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==} + peerDependencies: + react: '*' + react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -828,7 +850,7 @@ snapshots: '@tailwindcss/node@4.3.0': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.22.1 + enhanced-resolve: 5.22.0 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 @@ -908,6 +930,8 @@ snapshots: baseline-browser-mapping@2.10.32: {} + bcryptjs@3.0.3: {} + caniuse-lite@1.0.30001793: {} client-only@0.0.1: {} @@ -916,7 +940,7 @@ snapshots: detect-libc@2.1.2: {} - enhanced-resolve@5.22.1: + enhanced-resolve@5.22.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -1018,11 +1042,17 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres@3.4.9: {} + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 scheduler: 0.27.0 + react-icons@5.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + react@19.2.4: {} scheduler@0.27.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 581a9d5..72b211f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +allowBuilds: + sharp: false ignoredBuiltDependencies: - sharp - unrs-resolver diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e49259607bb0976dc0d323057fa69ed24034d395 GIT binary patch literal 198643 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 literal 0 HcmV?d00001 diff --git a/src/app/(protected)/bills/page.tsx b/src/app/(protected)/bills/page.tsx new file mode 100644 index 0000000..86a1428 --- /dev/null +++ b/src/app/(protected)/bills/page.tsx @@ -0,0 +1,5 @@ +import { BillsPageClient } from "@/components/bills/BillsPageClient"; + +export default function BillsPage() { + return ; +} diff --git a/src/app/(protected)/dashboard/page.tsx b/src/app/(protected)/dashboard/page.tsx new file mode 100644 index 0000000..ca0aafe --- /dev/null +++ b/src/app/(protected)/dashboard/page.tsx @@ -0,0 +1,5 @@ +import { DashboardPageClient } from "@/components/dashboard/DashboardPageClient"; + +export default function DashboardPage() { + return ; +} diff --git a/src/app/(protected)/income/page.tsx b/src/app/(protected)/income/page.tsx new file mode 100644 index 0000000..8a89003 --- /dev/null +++ b/src/app/(protected)/income/page.tsx @@ -0,0 +1,5 @@ +import { IncomePageClient } from "@/components/income/IncomePageClient"; + +export default function IncomePage() { + return ; +} diff --git a/src/app/(protected)/layout.tsx b/src/app/(protected)/layout.tsx new file mode 100644 index 0000000..eac5c0f --- /dev/null +++ b/src/app/(protected)/layout.tsx @@ -0,0 +1,17 @@ +import { redirect } from "next/navigation"; +import { AppShell } from "@/components/AppShell"; +import { getAuthenticatedUser } from "@/lib/auth"; +import { isInstanceConfigured } from "@/lib/database/setup"; + +export default async function ProtectedLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const user = await getAuthenticatedUser(); + if (!user) { + redirect((await isInstanceConfigured()) ? "/login" : "/setup"); + } + + return {children}; +} diff --git a/src/app/(protected)/one-time-payments/page.tsx b/src/app/(protected)/one-time-payments/page.tsx new file mode 100644 index 0000000..ced90de --- /dev/null +++ b/src/app/(protected)/one-time-payments/page.tsx @@ -0,0 +1,5 @@ +import { OneTimePaymentsPageClient } from "@/components/one-time-payments/OneTimePaymentsPageClient"; + +export default function OneTimePaymentsPage() { + return ; +} diff --git a/src/app/(protected)/profile/page.tsx b/src/app/(protected)/profile/page.tsx new file mode 100644 index 0000000..0684479 --- /dev/null +++ b/src/app/(protected)/profile/page.tsx @@ -0,0 +1,8 @@ +import { ProfilePageClient } from "@/components/profile/ProfilePageClient"; +import { getAuthenticatedUser } from "@/lib/auth"; + +export default async function ProfilePage() { + const user = await getAuthenticatedUser(); + if (!user) return null; + return ; +} diff --git a/src/app/(protected)/reports/page.tsx b/src/app/(protected)/reports/page.tsx new file mode 100644 index 0000000..131e484 --- /dev/null +++ b/src/app/(protected)/reports/page.tsx @@ -0,0 +1,17 @@ +import { ReportsPageClient } from "@/components/reports/ReportsPageClient"; + +export default async function ReportsPage({ + searchParams, +}: { + searchParams: Promise<{ year?: string }>; +}) { + const { year } = await searchParams; + return ; +} + +function normalizeYear(value?: string) { + const year = Number(value); + return Number.isInteger(year) && year >= 2000 && year <= 2100 + ? year + : new Date().getFullYear(); +} diff --git a/src/app/(protected)/subscriptions/page.tsx b/src/app/(protected)/subscriptions/page.tsx new file mode 100644 index 0000000..be7d229 --- /dev/null +++ b/src/app/(protected)/subscriptions/page.tsx @@ -0,0 +1,5 @@ +import { SubscriptionsPageClient } from "@/components/subscriptions/SubscriptionsPageClient"; + +export default function SubscriptionsPage() { + return ; +} diff --git a/src/app/(public)/login/page.tsx b/src/app/(public)/login/page.tsx new file mode 100644 index 0000000..939f35d --- /dev/null +++ b/src/app/(public)/login/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from "next/navigation"; +import { AuthForm } from "@/components/AuthForm"; +import { getAuthenticatedUser } from "@/lib/auth"; +import { isInstanceConfigured } from "@/lib/database/setup"; + +export default async function LoginPage() { + const user = await getAuthenticatedUser(); + if (user) redirect("/dashboard"); + if (!(await isInstanceConfigured())) redirect("/setup"); + return ; +} diff --git a/src/app/(public)/setup/page.tsx b/src/app/(public)/setup/page.tsx new file mode 100644 index 0000000..3c050ec --- /dev/null +++ b/src/app/(public)/setup/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from "next/navigation"; +import { SetupForm } from "@/components/SetupForm"; +import { getAuthenticatedUser } from "@/lib/auth"; +import { isInstanceConfigured } from "@/lib/database/setup"; + +export default async function SetupPage() { + const user = await getAuthenticatedUser(); + if (user) redirect("/dashboard"); + if (await isInstanceConfigured()) redirect("/login"); + return ; +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..110e235 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,39 @@ +import bcrypt from "bcryptjs"; +import { createSession } from "@/lib/auth"; +import { database } from "@/lib/database/server"; +import { ensureUserSettings } from "@/lib/profile/settings"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const email = String(body.email ?? "") + .trim() + .toLowerCase(); + const password = String(body.password ?? ""); + + if (!email || !password) { + return Response.json( + { error: "Email e password sono obbligatorie" }, + { status: 400 }, + ); + } + + const [user] = await database()< + { id: string; password_hash: string }[] + >`SELECT id, password_hash FROM users WHERE email = ${email} LIMIT 1`; + if (!user || !(await bcrypt.compare(password, user.password_hash))) { + return Response.json( + { error: "Credenziali non valide" }, + { status: 401 }, + ); + } + + await createSession(user.id); + await ensureUserSettings(user.id); + return Response.json({ ok: true }); + } catch (error) { + const message = + error instanceof Error ? error.message : "Accesso non riuscito"; + return Response.json({ error: message }, { status: 401 }); + } +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..f7c7512 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,6 @@ +import { deleteCurrentSession } from "@/lib/auth"; + +export async function POST() { + await deleteCurrentSession(); + return Response.json({ ok: true }); +} diff --git a/src/app/api/auth/session/route.ts b/src/app/api/auth/session/route.ts new file mode 100644 index 0000000..6a3f62d --- /dev/null +++ b/src/app/api/auth/session/route.ts @@ -0,0 +1,6 @@ +import { getAuthenticatedUser } from "@/lib/auth"; + +export async function GET() { + const user = await getAuthenticatedUser(); + return Response.json({ authenticated: Boolean(user), user }); +} diff --git a/src/app/api/auth/setup/route.ts b/src/app/api/auth/setup/route.ts new file mode 100644 index 0000000..9977091 --- /dev/null +++ b/src/app/api/auth/setup/route.ts @@ -0,0 +1,49 @@ +import bcrypt from "bcryptjs"; +import { createSession } from "@/lib/auth"; +import { database } from "@/lib/database/server"; +import { ensureUserSettings } from "@/lib/profile/settings"; +import { requireString } from "@/lib/validation/common"; + +class SetupCompletedError extends Error {} + +export async function POST(request: Request) { + try { + const body = (await request.json()) as Record; + const name = requireString(body.name, "name"); + const email = requireString(body.email, "email").toLowerCase(); + const password = requireString(body.password, "password"); + if (!email.includes("@")) throw new Error("L'email non è valida"); + if (password.length < 8) { + throw new Error("La password deve contenere almeno 8 caratteri"); + } + + const passwordHash = await bcrypt.hash(password, 12); + const user = await database().begin(async (sql) => { + await sql`SELECT pg_advisory_xact_lock(727743)`; + const [existing] = await sql<[{ configured: boolean }]>` + SELECT EXISTS(SELECT 1 FROM users) AS configured + `; + if (existing.configured) throw new SetupCompletedError(); + const [created] = await sql<[{ id: string }]>` + INSERT INTO users (email, name, password_hash) + VALUES (${email}, ${name}, ${passwordHash}) + RETURNING id + `; + return created; + }); + + await createSession(user.id); + await ensureUserSettings(user.id); + return Response.json({ ok: true }, { status: 201 }); + } catch (error) { + if (error instanceof SetupCompletedError) { + return Response.json( + { error: "L'istanza è già configurata" }, + { status: 409 }, + ); + } + const message = + error instanceof Error ? error.message : "Configurazione non riuscita"; + return Response.json({ error: message }, { status: 400 }); + } +} diff --git a/src/app/api/bills/[id]/route.ts b/src/app/api/bills/[id]/route.ts new file mode 100644 index 0000000..24aa0bf --- /dev/null +++ b/src/app/api/bills/[id]/route.ts @@ -0,0 +1,55 @@ +import { requireUser } from "@/lib/auth"; +import { + databaseConfig, + deleteDocument, + getOwnedDocument, + updateDocument, +} from "@/lib/database/server"; +import { removeLinkedLedger, syncBillLedger } from "@/lib/finance/ledger"; +import { parseBillInput } from "@/lib/validation/bills"; +import { parseBody, updatedField } from "@/lib/validation/common"; +import type { Bill } from "@/types/finance"; + +type Ctx = { params: Promise<{ id: string }> }; + +export async function PATCH(request: Request, ctx: Ctx) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { id } = await ctx.params; + const parsed = await parseBody(request, parseBillInput); + if (!parsed.ok) + return Response.json({ error: parsed.error }, { status: 400 }); + const { collections } = databaseConfig(); + const current = await getOwnedDocument( + collections.bills, + id, + auth.user.id, + ); + let bill = await updateDocument(collections.bills, id, { + ...parsed.data, + paymentDate: parsed.data.status === "paid" ? parsed.data.paymentDate : null, + linkedPaymentId: current.linkedPaymentId ?? null, + ...updatedField(), + }); + const ledger = await syncBillLedger(bill); + bill = await updateDocument(collections.bills, id, { + linkedPaymentId: ledger?.$id ?? null, + ...updatedField(), + }); + return Response.json(bill); +} + +export async function DELETE(_request: Request, ctx: Ctx) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { id } = await ctx.params; + const { collections } = databaseConfig(); + const bill = await getOwnedDocument( + collections.bills, + id, + auth.user.id, + ); + await removeLinkedLedger(auth.user.id, bill.linkedPaymentId, "bill", id); + await deleteDocument(collections.bills, id); + return Response.json({ ok: true }); +} diff --git a/src/app/api/bills/route.ts b/src/app/api/bills/route.ts new file mode 100644 index 0000000..0ff76f0 --- /dev/null +++ b/src/app/api/bills/route.ts @@ -0,0 +1,46 @@ +import { requireUser } from "@/lib/auth"; +import { + createDocument, + databaseConfig, + listDocuments, + query, + updateDocument, +} from "@/lib/database/server"; +import { syncBillLedger } from "@/lib/finance/ledger"; +import { parseBillInput } from "@/lib/validation/bills"; +import { nowFields, parseBody } from "@/lib/validation/common"; +import type { Bill } from "@/types/finance"; + +export async function GET() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { collections } = databaseConfig(); + const result = await listDocuments(collections.bills, [ + query.equal("userId", auth.user.id), + query.orderAsc("dueDate"), + query.limit(5000), + ]); + return Response.json(result.documents); +} + +export async function POST(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const parsed = await parseBody(request, parseBillInput); + if (!parsed.ok) + return Response.json({ error: parsed.error }, { status: 400 }); + const { collections } = databaseConfig(); + let bill = await createDocument(collections.bills, { + ...parsed.data, + userId: auth.user.id, + ...nowFields(), + }); + const ledger = await syncBillLedger(bill); + if (ledger && bill.linkedPaymentId !== ledger.$id) { + bill = await updateDocument(collections.bills, bill.$id, { + linkedPaymentId: ledger.$id, + updatedAt: new Date().toISOString(), + }); + } + return Response.json(bill, { status: 201 }); +} diff --git a/src/app/api/income/[id]/route.ts b/src/app/api/income/[id]/route.ts new file mode 100644 index 0000000..b3aeb7f --- /dev/null +++ b/src/app/api/income/[id]/route.ts @@ -0,0 +1,38 @@ +import { requireUser } from "@/lib/auth"; +import { + databaseConfig, + deleteDocument, + getOwnedDocument, + updateDocument, +} from "@/lib/database/server"; +import { parseBody, updatedField } from "@/lib/validation/common"; +import { parseIncomeInput } from "@/lib/validation/income"; +import type { Income } from "@/types/finance"; + +type Ctx = { params: Promise<{ id: string }> }; + +export async function PATCH(request: Request, ctx: Ctx) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { id } = await ctx.params; + const parsed = await parseBody(request, parseIncomeInput); + if (!parsed.ok) + return Response.json({ error: parsed.error }, { status: 400 }); + const { collections } = databaseConfig(); + await getOwnedDocument(collections.income, id, auth.user.id); + const document = await updateDocument(collections.income, id, { + ...parsed.data, + ...updatedField(), + }); + return Response.json(document); +} + +export async function DELETE(_request: Request, ctx: Ctx) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { id } = await ctx.params; + const { collections } = databaseConfig(); + await getOwnedDocument(collections.income, id, auth.user.id); + await deleteDocument(collections.income, id); + return Response.json({ ok: true }); +} diff --git a/src/app/api/income/route.ts b/src/app/api/income/route.ts new file mode 100644 index 0000000..5c2b220 --- /dev/null +++ b/src/app/api/income/route.ts @@ -0,0 +1,37 @@ +import { requireUser } from "@/lib/auth"; +import { + createDocument, + databaseConfig, + listDocuments, + query, +} from "@/lib/database/server"; +import { nowFields, parseBody } from "@/lib/validation/common"; +import { parseIncomeInput } from "@/lib/validation/income"; +import type { Income } from "@/types/finance"; + +export async function GET() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { collections } = databaseConfig(); + const result = await listDocuments(collections.income, [ + query.equal("userId", auth.user.id), + query.orderDesc("date"), + query.limit(5000), + ]); + return Response.json(result.documents); +} + +export async function POST(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const parsed = await parseBody(request, parseIncomeInput); + if (!parsed.ok) + return Response.json({ error: parsed.error }, { status: 400 }); + const { collections } = databaseConfig(); + const document = await createDocument(collections.income, { + ...parsed.data, + userId: auth.user.id, + ...nowFields(), + }); + return Response.json(document, { status: 201 }); +} diff --git a/src/app/api/one-time-payments/[id]/route.ts b/src/app/api/one-time-payments/[id]/route.ts new file mode 100644 index 0000000..8ddb063 --- /dev/null +++ b/src/app/api/one-time-payments/[id]/route.ts @@ -0,0 +1,67 @@ +import { requireUser } from "@/lib/auth"; +import { + databaseConfig, + deleteDocument, + getOwnedDocument, + updateDocument, +} from "@/lib/database/server"; +import { removeLinkedLedger, syncOneTimeLedger } from "@/lib/finance/ledger"; +import { parseBody, updatedField } from "@/lib/validation/common"; +import { parseOneTimePaymentInput } from "@/lib/validation/oneTimePayments"; +import type { OneTimePayment } from "@/types/finance"; + +type Ctx = { params: Promise<{ id: string }> }; + +export async function PATCH(request: Request, ctx: Ctx) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { id } = await ctx.params; + const parsed = await parseBody(request, parseOneTimePaymentInput); + if (!parsed.ok) + return Response.json({ error: parsed.error }, { status: 400 }); + const { collections } = databaseConfig(); + const current = await getOwnedDocument( + collections.oneTimePayments, + id, + auth.user.id, + ); + let payment = await updateDocument( + collections.oneTimePayments, + id, + { + ...parsed.data, + linkedPaymentId: current.linkedPaymentId, + ...updatedField(), + }, + ); + const ledger = await syncOneTimeLedger(payment); + payment = await updateDocument( + collections.oneTimePayments, + id, + { + linkedPaymentId: ledger.$id, + ...updatedField(), + }, + ); + return Response.json(payment); +} + +export async function DELETE(_request: Request, ctx: Ctx) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { id } = await ctx.params; + const { collections } = databaseConfig(); + const payment = await getOwnedDocument( + collections.oneTimePayments, + id, + auth.user.id, + ); + await removeLinkedLedger( + auth.user.id, + payment.linkedPaymentId, + "one_time", + id, + ); + await deleteDocument(collections.oneTimePayments, id); + return Response.json({ ok: true }); +} diff --git a/src/app/api/one-time-payments/route.ts b/src/app/api/one-time-payments/route.ts new file mode 100644 index 0000000..38e66de --- /dev/null +++ b/src/app/api/one-time-payments/route.ts @@ -0,0 +1,54 @@ +import { requireUser } from "@/lib/auth"; +import { + createDocument, + databaseConfig, + listDocuments, + query, + updateDocument, +} from "@/lib/database/server"; +import { syncOneTimeLedger } from "@/lib/finance/ledger"; +import { nowFields, parseBody } from "@/lib/validation/common"; +import { parseOneTimePaymentInput } from "@/lib/validation/oneTimePayments"; +import type { OneTimePayment } from "@/types/finance"; + +export async function GET() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { collections } = databaseConfig(); + const result = await listDocuments( + collections.oneTimePayments, + [ + query.equal("userId", auth.user.id), + query.orderDesc("date"), + query.limit(5000), + ], + ); + return Response.json(result.documents); +} + +export async function POST(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const parsed = await parseBody(request, parseOneTimePaymentInput); + if (!parsed.ok) + return Response.json({ error: parsed.error }, { status: 400 }); + const { collections } = databaseConfig(); + let payment = await createDocument( + collections.oneTimePayments, + { + ...parsed.data, + userId: auth.user.id, + ...nowFields(), + }, + ); + const ledger = await syncOneTimeLedger(payment); + payment = await updateDocument( + collections.oneTimePayments, + payment.$id, + { + linkedPaymentId: ledger.$id, + updatedAt: new Date().toISOString(), + }, + ); + return Response.json(payment, { status: 201 }); +} diff --git a/src/app/api/payment-titles/route.ts b/src/app/api/payment-titles/route.ts new file mode 100644 index 0000000..df29420 --- /dev/null +++ b/src/app/api/payment-titles/route.ts @@ -0,0 +1,49 @@ +import { requireUser } from "@/lib/auth"; +import { + createDocument, + databaseConfig, + listDocuments, + query, +} from "@/lib/database/server"; +import { nowFields, parseBody } from "@/lib/validation/common"; +import { parsePaymentTitleInput } from "@/lib/validation/paymentTitles"; +import type { PaymentTitle } from "@/types/finance"; + +export async function GET() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { collections } = databaseConfig(); + const result = await listDocuments(collections.paymentTitles, [ + query.equal("userId", auth.user.id), + query.orderAsc("normalizedName"), + ]); + return Response.json(result.documents); +} + +export async function POST(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const parsed = await parseBody(request, parsePaymentTitleInput); + if (!parsed.ok) + return Response.json({ error: parsed.error }, { status: 400 }); + const { collections } = databaseConfig(); + const existing = await listDocuments( + collections.paymentTitles, + [ + query.equal("userId", auth.user.id), + query.equal("normalizedName", parsed.data.normalizedName), + query.equal("paymentType", parsed.data.paymentType), + query.limit(1), + ], + ); + if (existing.documents[0]) return Response.json(existing.documents[0]); + const document = await createDocument( + collections.paymentTitles, + { + ...parsed.data, + userId: auth.user.id, + ...nowFields(), + }, + ); + return Response.json(document, { status: 201 }); +} diff --git a/src/app/api/payments-ledger/route.ts b/src/app/api/payments-ledger/route.ts new file mode 100644 index 0000000..50081de --- /dev/null +++ b/src/app/api/payments-ledger/route.ts @@ -0,0 +1,18 @@ +import { requireUser } from "@/lib/auth"; +import { databaseConfig, listDocuments, query } from "@/lib/database/server"; +import type { PaymentLedger } from "@/types/finance"; + +export async function GET() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { collections } = databaseConfig(); + const result = await listDocuments( + collections.paymentsLedger, + [ + query.equal("userId", auth.user.id), + query.orderDesc("date"), + query.limit(5000), + ], + ); + return Response.json(result.documents); +} diff --git a/src/app/api/profile/account/route.ts b/src/app/api/profile/account/route.ts new file mode 100644 index 0000000..1c54483 --- /dev/null +++ b/src/app/api/profile/account/route.ts @@ -0,0 +1,120 @@ +import bcrypt from "bcryptjs"; +import { clearSessionSecret, requireUser } from "@/lib/auth"; +import { database } from "@/lib/database/server"; +import { deleteAvatar } from "@/lib/profile/avatarStorage"; +import { + parseAccountPatch, + parseDeleteAccount, + parsePasswordPatch, +} from "@/lib/validation/account"; +import { parseBody } from "@/lib/validation/common"; +import type { UserSession } from "@/types/finance"; + +export async function PATCH(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const parsed = await parseBody(request, parseAccountPatch); + if (!parsed.ok) { + return Response.json({ error: parsed.error }, { status: 400 }); + } + try { + const [user] = await database()` + UPDATE users + SET name = ${parsed.data.name}, + email = ${parsed.data.email}, + updated_at = NOW() + WHERE id = ${auth.user.id} + RETURNING + id, + name, + email, + CASE + WHEN avatar_file IS NULL THEN NULL + ELSE '/api/profile/avatar?v=' || EXTRACT(EPOCH FROM updated_at)::bigint::text + END AS "avatarUrl" + `; + return Response.json(user); + } catch (error) { + if (hasDatabaseCode(error, "23505")) { + return Response.json( + { error: "Esiste già un account con questa email" }, + { status: 409 }, + ); + } + return Response.json( + { error: "Impossibile aggiornare i dati account" }, + { status: 500 }, + ); + } +} + +export async function POST(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const parsed = await parseBody(request, parsePasswordPatch); + if (!parsed.ok) { + return Response.json({ error: parsed.error }, { status: 400 }); + } + const [user] = await database()<{ password_hash: string }[]>` + SELECT password_hash + FROM users + WHERE id = ${auth.user.id} + LIMIT 1 + `; + if ( + !user || + !(await bcrypt.compare(parsed.data.currentPassword, user.password_hash)) + ) { + return Response.json( + { error: "La password attuale non è corretta" }, + { status: 401 }, + ); + } + const passwordHash = await bcrypt.hash(parsed.data.newPassword, 12); + await database()` + UPDATE users + SET password_hash = ${passwordHash}, + updated_at = NOW() + WHERE id = ${auth.user.id} + `; + return Response.json({ ok: true }); +} + +export async function DELETE(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const parsed = await parseBody(request, parseDeleteAccount); + if (!parsed.ok) { + return Response.json({ error: parsed.error }, { status: 400 }); + } + const [user] = await database()< + { avatar_file: string | null; password_hash: string }[] + >` + SELECT avatar_file, password_hash + FROM users + WHERE id = ${auth.user.id} + LIMIT 1 + `; + if ( + !user || + !(await bcrypt.compare(parsed.data.currentPassword, user.password_hash)) + ) { + return Response.json( + { error: "La password attuale non è corretta" }, + { status: 401 }, + ); + } + await database()`DELETE FROM users WHERE id = ${auth.user.id}`; + await clearSessionSecret(); + await deleteAvatar(user.avatar_file).catch(() => undefined); + return Response.json({ ok: true }); +} + +function hasDatabaseCode(error: unknown, code: string) { + return ( + typeof error === "object" && + error !== null && + "code" in error && + error.code === code + ); +} diff --git a/src/app/api/profile/avatar/route.ts b/src/app/api/profile/avatar/route.ts new file mode 100644 index 0000000..1f6fbe2 --- /dev/null +++ b/src/app/api/profile/avatar/route.ts @@ -0,0 +1,98 @@ +import { requireUser } from "@/lib/auth"; +import { database } from "@/lib/database/server"; +import { + avatarContentType, + deleteAvatar, + readAvatar, + saveAvatar, +} from "@/lib/profile/avatarStorage"; +import type { UserSession } from "@/types/finance"; + +export async function GET() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const [user] = await database()<{ avatar_file: string | null }[]>` + SELECT avatar_file + FROM users + WHERE id = ${auth.user.id} + LIMIT 1 + `; + if (!user?.avatar_file) { + return Response.json({ error: "Avatar non impostato" }, { status: 404 }); + } + try { + return new Response(await readAvatar(user.avatar_file), { + headers: { + "Cache-Control": "private, no-cache", + "Content-Type": avatarContentType(user.avatar_file), + }, + }); + } catch { + return Response.json({ error: "Avatar non disponibile" }, { status: 404 }); + } +} + +export async function POST(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + try { + const formData = await request.formData(); + const file = formData.get("avatar"); + if (!(file instanceof File)) { + return Response.json({ error: "Seleziona un'immagine" }, { status: 400 }); + } + const [current] = await database()<{ avatar_file: string | null }[]>` + SELECT avatar_file + FROM users + WHERE id = ${auth.user.id} + LIMIT 1 + `; + const filename = await saveAvatar(auth.user.id, file); + const user = await updateAvatar(auth.user.id, filename); + if (current?.avatar_file && current.avatar_file !== filename) { + await deleteAvatar(current.avatar_file); + } + return Response.json(user); + } catch (error) { + return Response.json( + { error: errorMessage(error, "Impossibile salvare l'avatar") }, + { status: 400 }, + ); + } +} + +export async function DELETE() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const [current] = await database()<{ avatar_file: string | null }[]>` + SELECT avatar_file + FROM users + WHERE id = ${auth.user.id} + LIMIT 1 + `; + const user = await updateAvatar(auth.user.id, null); + await deleteAvatar(current?.avatar_file); + return Response.json(user); +} + +async function updateAvatar(userId: string, filename: string | null) { + const [user] = await database()` + UPDATE users + SET avatar_file = ${filename}, + updated_at = NOW() + WHERE id = ${userId} + RETURNING + id, + name, + email, + CASE + WHEN avatar_file IS NULL THEN NULL + ELSE '/api/profile/avatar?v=' || EXTRACT(EPOCH FROM updated_at)::bigint::text + END AS "avatarUrl" + `; + return user; +} + +function errorMessage(error: unknown, fallback: string) { + return error instanceof Error ? error.message : fallback; +} diff --git a/src/app/api/profile/data/route.ts b/src/app/api/profile/data/route.ts new file mode 100644 index 0000000..d7b4b1b --- /dev/null +++ b/src/app/api/profile/data/route.ts @@ -0,0 +1,37 @@ +import { requireUser } from "@/lib/auth"; +import { + databaseConfig, + deleteDocument, + listDocuments, + query, +} from "@/lib/database/server"; + +export async function DELETE() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + await deleteFinancialData(auth.user.id); + return Response.json({ ok: true }); +} + +async function deleteFinancialData(userId: string) { + const { collections } = databaseConfig(); + const collectionIds = [ + collections.subscriptionPayments, + collections.paymentsLedger, + collections.subscriptions, + collections.bills, + collections.oneTimePayments, + collections.paymentTitles, + collections.income, + ]; + + for (const collectionId of collectionIds) { + const result = await listDocuments<{ $id: string }>(collectionId, [ + query.equal("userId", userId), + query.limit(5000), + ]); + for (const document of result.documents) { + await deleteDocument(collectionId, document.$id); + } + } +} diff --git a/src/app/api/profile/settings/route.ts b/src/app/api/profile/settings/route.ts new file mode 100644 index 0000000..4284764 --- /dev/null +++ b/src/app/api/profile/settings/route.ts @@ -0,0 +1,19 @@ +import { requireUser } from "@/lib/auth"; +import { ensureUserSettings, saveUserSettings } from "@/lib/profile/settings"; +import { parseBody } from "@/lib/validation/common"; +import { parseUserSettingsPatch } from "@/lib/validation/userSettings"; + +export async function GET() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + return Response.json(await ensureUserSettings(auth.user.id)); +} + +export async function PATCH(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const parsed = await parseBody(request, parseUserSettingsPatch); + if (!parsed.ok) + return Response.json({ error: parsed.error }, { status: 400 }); + return Response.json(await saveUserSettings(auth.user.id, parsed.data)); +} diff --git a/src/app/api/subscription-payments/[id]/route.ts b/src/app/api/subscription-payments/[id]/route.ts new file mode 100644 index 0000000..c11e62a --- /dev/null +++ b/src/app/api/subscription-payments/[id]/route.ts @@ -0,0 +1,79 @@ +import { requireUser } from "@/lib/auth"; +import { + databaseConfig, + deleteDocument, + getOwnedDocument, + updateDocument, +} from "@/lib/database/server"; +import { + removeLinkedLedger, + syncSubscriptionLedger, +} from "@/lib/finance/ledger"; +import { requireDate, updatedField } from "@/lib/validation/common"; +import type { Subscription, SubscriptionPayment } from "@/types/finance"; + +type Ctx = { params: Promise<{ id: string }> }; + +export async function PATCH(request: Request, ctx: Ctx) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { id } = await ctx.params; + const body = (await request.json()) as Record; + const status = body.status === "paid" ? "paid" : "unpaid"; + const { collections } = databaseConfig(); + const current = await getOwnedDocument( + collections.subscriptionPayments, + id, + auth.user.id, + ); + const subscription = await getOwnedDocument( + collections.subscriptions, + current.subscriptionId, + auth.user.id, + ); + let payment = await updateDocument( + collections.subscriptionPayments, + id, + { + status, + paymentDate: + status === "paid" + ? body.paymentDate + ? requireDate(body.paymentDate, "paymentDate") + : current.renewalDate + : "", + linkedPaymentId: current.linkedPaymentId, + ...updatedField(), + }, + ); + const ledger = await syncSubscriptionLedger(subscription, payment); + payment = await updateDocument( + collections.subscriptionPayments, + id, + { + linkedPaymentId: ledger?.$id ?? "", + ...updatedField(), + }, + ); + return Response.json(payment); +} + +export async function DELETE(_request: Request, ctx: Ctx) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { id } = await ctx.params; + const { collections } = databaseConfig(); + const payment = await getOwnedDocument( + collections.subscriptionPayments, + id, + auth.user.id, + ); + await removeLinkedLedger( + auth.user.id, + payment.linkedPaymentId, + "subscription", + id, + ); + await deleteDocument(collections.subscriptionPayments, id); + return Response.json({ ok: true }); +} diff --git a/src/app/api/subscription-payments/route.ts b/src/app/api/subscription-payments/route.ts new file mode 100644 index 0000000..d3626d6 --- /dev/null +++ b/src/app/api/subscription-payments/route.ts @@ -0,0 +1,160 @@ +import { requireUser } from "@/lib/auth"; +import { + createDocument, + DatabaseRequestError, + databaseConfig, + getOwnedDocument, + listDocuments, + query, + updateDocument, +} from "@/lib/database/server"; +import { + removeLinkedLedger, + syncSubscriptionLedger, +} from "@/lib/finance/ledger"; +import { isScheduledSubscriptionOccurrence } from "@/lib/finance/subscriptionOccurrences"; +import { + requireDate, + requireString, + updatedField, +} from "@/lib/validation/common"; +import type { Subscription, SubscriptionPayment } from "@/types/finance"; + +function parsePayment(body: Record) { + const status = body.status === "paid" ? "paid" : "unpaid"; + return { + subscriptionId: requireString(body.subscriptionId, "subscriptionId"), + renewalDate: requireDate(body.renewalDate, "renewalDate"), + status, + paymentDate: + status === "paid" + ? body.paymentDate + ? requireDate(body.paymentDate, "paymentDate") + : requireDate(body.renewalDate, "renewalDate") + : undefined, + }; +} + +async function findPayment( + userId: string, + subscriptionId: string, + renewalDate: string, +) { + const { collections } = databaseConfig(); + const result = await listDocuments( + collections.subscriptionPayments, + [ + query.equal("subscriptionId", subscriptionId), + query.equal("renewalDate", renewalDate), + query.equal("userId", userId), + query.limit(1), + ], + ); + return result.documents[0]; +} + +export async function GET() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { collections } = databaseConfig(); + const result = await listDocuments( + collections.subscriptionPayments, + [ + query.equal("userId", auth.user.id), + query.orderDesc("renewalDate"), + query.limit(5000), + ], + ); + return Response.json(result.documents); +} + +export async function POST(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const userId = auth.user.id; + try { + const body = (await request.json()) as Record; + const parsed = parsePayment(body); + const { collections } = databaseConfig(); + const subscription = await getOwnedDocument( + collections.subscriptions, + parsed.subscriptionId, + userId, + ); + const existing = await findPayment( + userId, + parsed.subscriptionId, + parsed.renewalDate, + ); + if ( + !existing && + !isScheduledSubscriptionOccurrence(subscription, parsed.renewalDate) + ) { + throw new Error("Il rinnovo richiesto non è previsto"); + } + let payment = existing + ? await updatePayment(existing, parsed) + : await createPayment(parsed).catch(async (error) => { + if ( + !(error instanceof DatabaseRequestError) || + error.status !== 409 + ) { + throw error; + } + const winner = await findPayment( + userId, + parsed.subscriptionId, + parsed.renewalDate, + ); + if (!winner) throw error; + return updatePayment(winner, parsed); + }); + const ledger = await syncSubscriptionLedger(subscription, payment); + if (parsed.status === "unpaid") { + await removeLinkedLedger( + userId, + payment.linkedPaymentId, + "subscription", + payment.$id, + ); + } + payment = await updateDocument( + collections.subscriptionPayments, + payment.$id, + { + linkedPaymentId: ledger?.$id ?? "", + paymentDate: parsed.status === "paid" ? payment.paymentDate : "", + ...updatedField(), + }, + ); + return Response.json(payment, { status: existing ? 200 : 201 }); + + function updatePayment(current: SubscriptionPayment, data: typeof parsed) { + return updateDocument( + collections.subscriptionPayments, + current.$id, + { + ...data, + linkedPaymentId: current.linkedPaymentId, + ...updatedField(), + }, + ); + } + + function createPayment(data: typeof parsed) { + return createDocument( + collections.subscriptionPayments, + { + ...data, + userId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ); + } + } catch (error) { + const message = + error instanceof Error ? error.message : "Richiesta non valida"; + return Response.json({ error: message }, { status: 400 }); + } +} diff --git a/src/app/api/subscriptions/[id]/route.ts b/src/app/api/subscriptions/[id]/route.ts new file mode 100644 index 0000000..4845b4f --- /dev/null +++ b/src/app/api/subscriptions/[id]/route.ts @@ -0,0 +1,79 @@ +import { requireUser } from "@/lib/auth"; +import { + databaseConfig, + deleteDocument, + getOwnedDocument, + listDocuments, + query, + updateDocument, +} from "@/lib/database/server"; +import { removeLinkedLedger } from "@/lib/finance/ledger"; +import { parseBody, updatedField } from "@/lib/validation/common"; +import { parseSubscriptionPatch } from "@/lib/validation/subscriptions"; +import type { Subscription, SubscriptionPayment } from "@/types/finance"; + +type Ctx = { params: Promise<{ id: string }> }; + +export async function PATCH(request: Request, ctx: Ctx) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { id } = await ctx.params; + const parsed = await parseBody(request, parseSubscriptionPatch); + if (!parsed.ok) + return Response.json({ error: parsed.error }, { status: 400 }); + const { collections } = databaseConfig(); + const current = await getOwnedDocument( + collections.subscriptions, + id, + auth.user.id, + ); + if ( + current.subscriptionStatus === "terminated" && + parsed.data.subscriptionStatus !== "terminated" + ) { + return Response.json( + { error: "Un abbonamento terminato non può essere riattivato" }, + { status: 400 }, + ); + } + const document = await updateDocument( + collections.subscriptions, + id, + { + ...parsed.data, + ...updatedField(), + }, + ); + return Response.json(document); +} + +export async function DELETE(_request: Request, ctx: Ctx) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { id } = await ctx.params; + const { collections } = databaseConfig(); + await getOwnedDocument( + collections.subscriptions, + id, + auth.user.id, + ); + const payments = await listDocuments( + collections.subscriptionPayments, + [ + query.equal("userId", auth.user.id), + query.equal("subscriptionId", id), + query.limit(5000), + ], + ); + for (const payment of payments.documents) { + await removeLinkedLedger( + auth.user.id, + payment.linkedPaymentId, + "subscription", + payment.$id, + ); + await deleteDocument(collections.subscriptionPayments, payment.$id); + } + await deleteDocument(collections.subscriptions, id); + return Response.json({ ok: true }); +} diff --git a/src/app/api/subscriptions/route.ts b/src/app/api/subscriptions/route.ts new file mode 100644 index 0000000..6258955 --- /dev/null +++ b/src/app/api/subscriptions/route.ts @@ -0,0 +1,40 @@ +import { requireUser } from "@/lib/auth"; +import { + createDocument, + databaseConfig, + listDocuments, + query, +} from "@/lib/database/server"; +import { nowFields, parseBody } from "@/lib/validation/common"; +import { parseSubscriptionInput } from "@/lib/validation/subscriptions"; +import type { Subscription } from "@/types/finance"; + +export async function GET() { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const { collections } = databaseConfig(); + const result = await listDocuments(collections.subscriptions, [ + query.equal("userId", auth.user.id), + query.orderAsc("firstPaymentDate"), + query.limit(5000), + ]); + return Response.json(result.documents); +} + +export async function POST(request: Request) { + const auth = await requireUser(); + if ("response" in auth) return auth.response; + const parsed = await parseBody(request, parseSubscriptionInput); + if (!parsed.ok) + return Response.json({ error: parsed.error }, { status: 400 }); + const { collections } = databaseConfig(); + const document = await createDocument( + collections.subscriptions, + { + ...parsed.data, + userId: auth.user.id, + ...nowFields(), + }, + ); + return Response.json(document, { status: 201 }); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..4a7ff6e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,85 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; +/* Riserva sempre lo spazio della scrollbar */ +html { + overflow-y: auto; + scrollbar-gutter: stable; } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); +/* Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.25) transparent; } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +/* Chrome, Edge, Brave, Opera */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.18); + border-radius: 999px; + border: 2px solid transparent; + background-clip: content-box; + transition: all 0.25s ease; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.35); + background-clip: content-box; +} + +::-webkit-scrollbar-thumb:active { + background: rgba(255, 255, 255, 0.5); + background-clip: content-box; +} + +::-webkit-scrollbar-corner { + background: transparent; +} + +@keyframes modal-in { + from { + opacity: 0; + transform: translateY(8px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); } } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +@keyframes modal-out { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + to { + opacity: 0; + transform: translateY(8px) scale(0.98); + } +} + +@keyframes modal-backdrop-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes modal-backdrop-out { + from { + opacity: 1; + } + to { + opacity: 0; + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..86a7e0b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -13,8 +13,9 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "MyMoney", + description: + "Tieni traccia delle tue finanze con MyMoney, in modo semplice ed efficace.", }; export default function RootLayout({ @@ -24,10 +25,10 @@ export default function RootLayout({ }>) { return ( - {children} + {children} ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 3f36f7c..938978c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,9 @@ -import Image from "next/image"; +import { redirect } from "next/navigation"; +import { getAuthenticatedUser } from "@/lib/auth"; +import { isInstanceConfigured } from "@/lib/database/setup"; -export default function Home() { - return ( - - ); +export default async function Home() { + const user = await getAuthenticatedUser(); + if (user) redirect("/dashboard"); + redirect((await isInstanceConfigured()) ? "/login" : "/setup"); } diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx new file mode 100644 index 0000000..3a9e894 --- /dev/null +++ b/src/components/AppShell.tsx @@ -0,0 +1,12 @@ +import { Header } from "@/components/shared/Header"; +import type { UserSession } from "@/types/finance"; + +export function AppShell({ + user, + children, +}: { + user: UserSession; + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/src/components/AuthForm.tsx b/src/components/AuthForm.tsx new file mode 100644 index 0000000..bfd74a3 --- /dev/null +++ b/src/components/AuthForm.tsx @@ -0,0 +1,75 @@ +"use client"; + +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { inputClass } from "@/components/shared/Field"; + +export function AuthForm() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + async function submit(event: React.FormEvent) { + event.preventDefault(); + setLoading(true); + setError(""); + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + const payload = await response.json().catch(() => ({})); + setLoading(false); + if (!response.ok) { + setError(payload.error ?? "Accesso non riuscito"); + return; + } + router.push("/dashboard"); + router.refresh(); + } + + return ( +
+
+
+ MyMoney +

MyMoney

+
+ +

+ Accedi a MyMoney - l'app per tracciare le tue finanze. +

+
+ setEmail(event.target.value)} + /> + setPassword(event.target.value)} + /> + {error ?

{error}

: null} + +
+
+
+ ); +} diff --git a/src/components/SetupForm.tsx b/src/components/SetupForm.tsx new file mode 100644 index 0000000..4ab8ac7 --- /dev/null +++ b/src/components/SetupForm.tsx @@ -0,0 +1,82 @@ +"use client"; + +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { inputClass } from "@/components/shared/Field"; + +export function SetupForm() { + const router = useRouter(); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + async function submit(event: React.FormEvent) { + event.preventDefault(); + setLoading(true); + setError(""); + const response = await fetch("/api/auth/setup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, email, password }), + }); + const payload = await response.json().catch(() => ({})); + setLoading(false); + if (!response.ok) { + setError(payload.error ?? "Configurazione non riuscita"); + return; + } + router.push("/dashboard"); + router.refresh(); + } + + return ( +
+
+
+ MyMoney +

MyMoney

+
+ +

+ Configura il primo account per iniziare a usare la tua istanza. +

+
+ setName(event.target.value)} + /> + setEmail(event.target.value)} + /> + setPassword(event.target.value)} + /> + {error ?

{error}

: null} + +
+
+
+ ); +} diff --git a/src/components/bills/BillForm.tsx b/src/components/bills/BillForm.tsx new file mode 100644 index 0000000..933ba39 --- /dev/null +++ b/src/components/bills/BillForm.tsx @@ -0,0 +1,162 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { DateField } from "@/components/shared/DateField"; +import { Field, inputClass } from "@/components/shared/Field"; +import { Select } from "@/components/shared/Select"; +import { + SimpleFormActions, + SimpleFormReview, + SimpleFormStepIndicator, +} from "@/components/shared/SimpleFormSteps"; +import { TitleCombobox } from "@/components/shared/TitleCombobox"; +import { formatItalianDate, todayIso } from "@/lib/date/formatDate"; +import { paymentStatusLabel } from "@/lib/finance/labels"; +import { + euroFormatter, + formatMoneyInput, + parseMoneyInput, +} from "@/lib/finance/money"; +import type { Bill, PaymentTitle } from "@/types/finance"; + +export function BillForm({ + titles, + editing, + onSubmit, + onCreateTitle, + onCancel, + defaultDate = todayIso(), + saving = false, +}: { + titles: PaymentTitle[]; + editing?: Bill; + onSubmit: (data: object) => Promise; + onCreateTitle?: (name: string) => Promise; + onCancel?: () => void; + defaultDate?: string; + saving?: boolean; +}) { + const [review, setReview] = useState(false); + const [title, setTitle] = useState(editing?.title ?? ""); + const [amount, setAmount] = useState(formatMoneyInput(editing?.amount)); + const [dueDate, setDueDate] = useState(editing?.dueDate ?? defaultDate); + const [status, setStatus] = useState(editing?.status ?? "pending"); + const [paymentDate, setPaymentDate] = useState( + editing?.paymentDate ?? todayIso(), + ); + const [error, setError] = useState(""); + + useEffect(() => { + setReview(false); + setTitle(editing?.title ?? ""); + setAmount(formatMoneyInput(editing?.amount)); + setDueDate(editing?.dueDate ?? defaultDate); + setStatus(editing?.status ?? "pending"); + setPaymentDate(editing?.paymentDate ?? todayIso()); + setError(""); + }, [defaultDate, editing]); + + function payload() { + if (!title.trim()) throw new Error("Inserisci il titolo della bolletta"); + if (!dueDate) throw new Error("Inserisci la scadenza della bolletta"); + return { + title, + amount: parseMoneyInput(amount), + dueDate, + status, + paymentDate: status === "paid" ? paymentDate : undefined, + }; + } + + return ( +
{ + event.preventDefault(); + try { + setError(""); + const data = payload(); + if (!review) { + setReview(true); + return; + } + await onSubmit(data); + } catch (err) { + setError( + err instanceof Error ? err.message : "Salvataggio non riuscito", + ); + } + }} + > + + {review ? ( + + ) : ( +
+ + + setAmount(event.target.value)} + /> + + + setAmount(event.target.value)} + /> + + setAmount(event.target.value)} + /> + + +
+ )} + {error ?

{error}

: null} + setReview(false)} + onCancel={onCancel} + review={review} + saving={saving} + /> + + ); +} diff --git a/src/components/one-time-payments/OneTimePaymentList.tsx b/src/components/one-time-payments/OneTimePaymentList.tsx new file mode 100644 index 0000000..033b791 --- /dev/null +++ b/src/components/one-time-payments/OneTimePaymentList.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useState } from "react"; +import { + LuCalendarDays, + LuPencil, + LuReceiptText, + LuTrash2, +} from "react-icons/lu"; +import { EmptyState } from "@/components/shared/EmptyState"; +import { financeListCardClass } from "@/components/shared/financeListCard"; +import { ListCardAction } from "@/components/shared/ListCardAction"; +import { PaymentBadge } from "@/components/shared/PaymentBadge"; +import { formatItalianDate } from "@/lib/date/formatDate"; +import { euroFormatter } from "@/lib/finance/money"; +import type { OneTimePayment } from "@/types/finance"; + +export function OneTimePaymentList({ + payments, + onEdit, + onDelete, +}: { + payments: OneTimePayment[]; + onEdit: (payment: OneTimePayment) => void; + onDelete: (payment: OneTimePayment) => Promise; +}) { + const [busy, setBusy] = useState(""); + if (!payments.length) return ; + return ( +
+ {payments.map((payment) => ( +
+
+
+
+

+ {payment.title} +

+ + Manuale + +
+

+ {euroFormatter.format(payment.amount)} +

+
+
+ onEdit(payment)} + /> + { + setBusy(payment.$id); + try { + await onDelete(payment); + } finally { + setBusy(""); + } + }} + /> +
+
+
+

+ + Pagamento singolo +

+

+ + Registrato il {formatItalianDate(payment.date)} +

+
+
+ ))} +
+ ); +} diff --git a/src/components/one-time-payments/OneTimePaymentsPageClient.tsx b/src/components/one-time-payments/OneTimePaymentsPageClient.tsx new file mode 100644 index 0000000..4434ef8 --- /dev/null +++ b/src/components/one-time-payments/OneTimePaymentsPageClient.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { OneTimePaymentForm } from "@/components/one-time-payments/OneTimePaymentForm"; +import { OneTimePaymentList } from "@/components/one-time-payments/OneTimePaymentList"; +import { EntityFormModal } from "@/components/shared/EntityFormModal"; +import { MonthlyPageToolbar } from "@/components/shared/MonthlyPageToolbar"; +import { PageShell } from "@/components/shared/PageShell"; +import { SectionAddButton } from "@/components/shared/SectionAddButton"; +import { notify } from "@/components/shared/Toast"; +import { + apiSave, + evictCachedLedgerSource, + invalidateResource, + useOneTimePaymentsResource, + usePaymentTitlesResource, +} from "@/components/shared/useFinanceData"; +import { + defaultDateForPeriod, + monthKey, + periodBounds, +} from "@/lib/date/formatDate"; +import type { OneTimePayment, PaymentTitle } from "@/types/finance"; + +export function OneTimePaymentsPageClient() { + const payments = useOneTimePaymentsResource(); + const titles = usePaymentTitlesResource(); + const [editing, setEditing] = useState(); + const [modalOpen, setModalOpen] = useState(false); + const [saving, setSaving] = useState(false); + const [month, setMonth] = useState(monthKey); + const visiblePayments = useMemo(() => { + const { start, end } = periodBounds(month); + return (payments.data ?? []).filter( + (payment) => payment.date >= start && payment.date <= end, + ); + }, [month, payments.data]); + const populatedMonths = useMemo( + () => + new Set((payments.data ?? []).map((payment) => payment.date.slice(0, 7))), + [payments.data], + ); + + async function createTitle(name: string) { + const saved = await apiSave("/api/payment-titles", "POST", { + name, + paymentType: "one_time", + }); + titles.mutate((current) => { + const list = current ?? []; + return list.some((item) => item.$id === saved.$id) + ? list + : [...list, saved]; + }); + } + + async function save(payload: object) { + setSaving(true); + try { + if ("title" in payload) await createTitle(String(payload.title)); + const saved = await apiSave( + editing + ? `/api/one-time-payments/${editing.$id}` + : "/api/one-time-payments", + editing ? "PATCH" : "POST", + payload, + ); + payments.mutate((current) => { + const list = current ?? []; + return editing + ? list.map((item) => (item.$id === saved.$id ? saved : item)) + : [saved, ...list]; + }); + invalidateResource("payments-ledger"); + setModalOpen(false); + setEditing(undefined); + notify("Pagamento salvato"); + } finally { + setSaving(false); + } + } + + return ( + { + setEditing(undefined); + setModalOpen(true); + }} + /> + } + > +
+ + { + setEditing(payment); + setModalOpen(true); + }} + onDelete={async (payment) => { + await apiSave(`/api/one-time-payments/${payment.$id}`, "DELETE"); + payments.mutate((current) => + (current ?? []).filter((item) => item.$id !== payment.$id), + ); + evictCachedLedgerSource("one_time", payment.$id); + invalidateResource("payments-ledger"); + notify("Pagamento eliminato"); + }} + /> +
+ {modalOpen ? ( + { + setModalOpen(false); + setEditing(undefined); + }} + open + saving={saving} + title={editing ? "Modifica Pagamento" : "Nuovo Pagamento"} + > + { + setModalOpen(false); + setEditing(undefined); + }} + onCreateTitle={createTitle} + onSubmit={save} + saving={saving} + /> + + ) : null} +
+ ); +} diff --git a/src/components/profile/AccountCard.tsx b/src/components/profile/AccountCard.tsx new file mode 100644 index 0000000..3e493c1 --- /dev/null +++ b/src/components/profile/AccountCard.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useRef } from "react"; +import type { IconType } from "react-icons"; +import { + LuImagePlus, + LuKeyRound, + LuMail, + LuPencil, + LuTrash2, + LuUserRound, +} from "react-icons/lu"; +import { ProfileCard } from "@/components/profile/ProfileCard"; +import { ListCardAction } from "@/components/shared/ListCardAction"; +import { UserAvatar } from "@/components/shared/UserAvatar"; +import type { UserSession } from "@/types/finance"; + +export function AccountCard({ + onDeleteAvatar, + onEditAccount, + onEditAvatar, + onEditPassword, + onSelectAvatar, + savingAvatar, + user, +}: { + onDeleteAvatar: () => Promise; + onEditAccount: () => void; + onEditAvatar: () => void; + onEditPassword: () => void; + onSelectAvatar: (file: File) => void; + savingAvatar: boolean; + user: UserSession; +}) { + const inputRef = useRef(null); + + return ( + +
+
+
+ +
+

+ {user.name} +

+

+ {user.email} +

+

+ JPG, PNG o WebP fino a 10 MB +

+
+
+
+ { + const file = event.target.files?.[0]; + if (!file) return; + onSelectAvatar(file); + event.target.value = ""; + }} + ref={inputRef} + type="file" + /> + + user.avatarUrl ? onEditAvatar() : inputRef.current?.click() + } + /> + {user.avatarUrl ? ( + + ) : null} +
+
+
+ + } + icon={LuUserRound} + label="Nome" + value={user.name} + /> + + + } + icon={LuKeyRound} + label="Password" + value="••••••••" + /> +
+
+
+ ); +} + +function InfoRow({ + action, + icon: Icon, + label, + value, +}: { + action?: React.ReactNode; + icon: IconType; + label: string; + value: string; +}) { + return ( +
+ +
+

{label}

+

{value}

+
+ {action} +
+ ); +} diff --git a/src/components/profile/AccountEditModal.tsx b/src/components/profile/AccountEditModal.tsx new file mode 100644 index 0000000..9e44d4d --- /dev/null +++ b/src/components/profile/AccountEditModal.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { EntityFormModal } from "@/components/shared/EntityFormModal"; +import { Field, inputClass } from "@/components/shared/Field"; +import { + SimpleFormActions, + SimpleFormReview, + SimpleFormStepIndicator, +} from "@/components/shared/SimpleFormSteps"; +import type { UserSession } from "@/types/finance"; + +export function AccountEditModal({ + onClose, + onSubmit, + open, + saving, + user, +}: { + onClose: () => void; + onSubmit: (payload: { name: string; email: string }) => Promise; + open: boolean; + saving: boolean; + user: UserSession; +}) { + const [review, setReview] = useState(false); + const [name, setName] = useState(user.name); + const [email, setEmail] = useState(user.email); + + useEffect(() => { + if (!open) return; + setReview(false); + setName(user.name); + setEmail(user.email); + }, [open, user.email, user.name]); + + async function submit(event: React.FormEvent) { + event.preventDefault(); + if (!review) { + setReview(true); + return; + } + if (await onSubmit({ name, email })) onClose(); + } + + return ( + + +
+ {review ? ( + + ) : ( +
+ + setName(event.target.value)} + required + value={name} + /> + + + setEmail(event.target.value)} + required + type="email" + value={email} + /> + +
+ )} + setReview(false)} + onCancel={onClose} + review={review} + saving={saving} + /> + +
+ ); +} diff --git a/src/components/profile/AvatarCropModal.tsx b/src/components/profile/AvatarCropModal.tsx new file mode 100644 index 0000000..fb5f8ec --- /dev/null +++ b/src/components/profile/AvatarCropModal.tsx @@ -0,0 +1,306 @@ +// biome-ignore-all lint/performance/noImgElement: blob previews need stable native dimensions for canvas editing +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { LuCrop, LuImagePlus, LuRotateCcw, LuZoomIn } from "react-icons/lu"; +import { EntityFormModal } from "@/components/shared/EntityFormModal"; + +const OUTPUT_SIZE = 512; +type ImageSize = { height: number; width: number }; + +export function AvatarCropModal({ + file, + onClose, + onConfirm, + onSelectFile, + saving, +}: { + file?: File; + onClose: () => void; + onConfirm: (file: File) => Promise; + onSelectFile: (file: File) => void; + saving: boolean; +}) { + const fileInputRef = useRef(null); + const imageRef = useRef(null); + const previewRef = useRef(null); + const dragRef = useRef< + { pointerId: number; x: number; y: number } | undefined + >(undefined); + const [source, setSource] = useState(""); + const [imageSize, setImageSize] = useState(); + const [zoom, setZoom] = useState(1); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [previewSize, setPreviewSize] = useState(384); + + useEffect(() => { + if (!file) return; + const url = URL.createObjectURL(file); + setSource(url); + setImageSize(undefined); + setZoom(1); + setPosition({ x: 0, y: 0 }); + return () => URL.revokeObjectURL(url); + }, [file]); + + useEffect(() => { + const preview = previewRef.current; + if (!preview) return; + const observer = new ResizeObserver(([entry]) => { + if (entry) setPreviewSize(entry.contentRect.width); + }); + observer.observe(preview); + return () => observer.disconnect(); + }, []); + + return ( + +

+ L'immagine parte interamente visibile. Trascinala o aumenta lo zoom solo + se vuoi regolare l'inquadratura. +

+
+
{ + event.currentTarget.setPointerCapture(event.pointerId); + dragRef.current = { + pointerId: event.pointerId, + x: event.clientX, + y: event.clientY, + }; + }} + onPointerMove={(event) => { + const drag = dragRef.current; + if (!drag || drag.pointerId !== event.pointerId) return; + setPosition((current) => + clampPosition( + { + x: current.x + event.clientX - drag.x, + y: current.y + event.clientY - drag.y, + }, + imageSize, + previewSize, + zoom, + ), + ); + dragRef.current = { + pointerId: event.pointerId, + x: event.clientX, + y: event.clientY, + }; + }} + onPointerUp={(event) => { + if (dragRef.current?.pointerId === event.pointerId) { + dragRef.current = undefined; + } + }} + onPointerCancel={() => { + dragRef.current = undefined; + }} + ref={previewRef} + > + {source ? ( + Anteprima ritaglio avatar { + const height = event.currentTarget.naturalHeight; + const width = event.currentTarget.naturalWidth; + setImageSize((current) => { + if (current) return current; + setPosition({ x: 0, y: 0 }); + return { height, width }; + }); + }} + ref={imageRef} + src={source} + style={imageStyle(imageSize, previewSize, zoom, position)} + /> + ) : null} +
+
+
+
+ + Zoom +
+ { + const nextZoom = Number(event.target.value); + setZoom(nextZoom); + setPosition((current) => + clampPosition(current, imageSize, previewSize, nextZoom), + ); + }} + step="0.01" + type="range" + value={zoom} + /> +
+ { + const nextFile = event.target.files?.[0]; + if (nextFile) onSelectFile(nextFile); + event.target.value = ""; + }} + ref={fileInputRef} + type="file" + /> +
+ + +
+
+
+
+ + +
+
+ ); +} + +function imageStyle( + _imageSize: ImageSize | undefined, + _previewSize: number, + zoom: number, + position: { x: number; y: number }, +) { + return { + transform: `translate(${position.x}px, ${position.y}px) scale(${zoom})`, + }; +} + +function clampPosition( + position: { x: number; y: number }, + imageSize: ImageSize | undefined, + previewSize: number, + zoom: number, +) { + if (!imageSize) return position; + const baseScale = initialImageScale(imageSize, previewSize); + const maxX = Math.max( + 0, + (imageSize.width * baseScale * zoom - previewSize) / 2, + ); + const maxY = Math.max( + 0, + (imageSize.height * baseScale * zoom - previewSize) / 2, + ); + return { + x: Math.min(maxX, Math.max(-maxX, position.x)), + y: Math.min(maxY, Math.max(-maxY, position.y)), + }; +} + +async function croppedAvatarFile( + image: HTMLImageElement, + originalFilename: string, + previewSize: number, + zoom: number, + position: { x: number; y: number }, +) { + const canvas = document.createElement("canvas"); + canvas.height = OUTPUT_SIZE; + canvas.width = OUTPUT_SIZE; + const context = canvas.getContext("2d"); + if (!context) throw new Error("Impossibile elaborare l'immagine"); + + const outputScale = OUTPUT_SIZE / previewSize; + const baseScale = initialImageScale( + { height: image.naturalHeight, width: image.naturalWidth }, + previewSize, + ); + const scale = baseScale * zoom * outputScale; + const width = image.naturalWidth * scale; + const height = image.naturalHeight * scale; + context.drawImage( + image, + (OUTPUT_SIZE - width) / 2 + position.x * outputScale, + (OUTPUT_SIZE - height) / 2 + position.y * outputScale, + width, + height, + ); + const blob = await new Promise((resolve, reject) => { + canvas.toBlob( + (result) => + result + ? resolve(result) + : reject(new Error("Esportazione non riuscita")), + "image/webp", + 0.9, + ); + }); + return new File([blob], `${stripExtension(originalFilename)}.webp`, { + type: "image/webp", + }); +} + +function initialImageScale(imageSize: ImageSize, previewSize: number) { + return Math.min( + previewSize / imageSize.width, + previewSize / imageSize.height, + ); +} + +function stripExtension(filename: string) { + return filename.replace(/\.[^.]+$/, "") || "avatar"; +} diff --git a/src/components/profile/ConfirmDangerModal.tsx b/src/components/profile/ConfirmDangerModal.tsx new file mode 100644 index 0000000..dba4149 --- /dev/null +++ b/src/components/profile/ConfirmDangerModal.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { EntityFormModal } from "@/components/shared/EntityFormModal"; + +export function ConfirmDangerModal({ + saving, + onClose, + onConfirm, +}: { + saving: boolean; + onClose: () => void; + onConfirm: () => Promise; +}) { + return ( + +

+ Questa azione elimina definitivamente tutti i tuoi dati finanziari. I + dati di accesso rimangono salvati. +

+
+ + +
+
+ ); +} diff --git a/src/components/profile/DangerZoneCard.tsx b/src/components/profile/DangerZoneCard.tsx new file mode 100644 index 0000000..80c6832 --- /dev/null +++ b/src/components/profile/DangerZoneCard.tsx @@ -0,0 +1,34 @@ +import { ProfileCard } from "@/components/profile/ProfileCard"; + +export function DangerZoneCard({ + onDeleteAccount, + onDeleteAll, +}: { + onDeleteAccount: () => void; + onDeleteAll: () => void; +}) { + return ( + +
+ + +
+
+ ); +} diff --git a/src/components/profile/DeleteAccountModal.tsx b/src/components/profile/DeleteAccountModal.tsx new file mode 100644 index 0000000..79e659f --- /dev/null +++ b/src/components/profile/DeleteAccountModal.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { LuTrash2 } from "react-icons/lu"; +import { EntityFormModal } from "@/components/shared/EntityFormModal"; +import { Field, inputClass } from "@/components/shared/Field"; + +export function DeleteAccountModal({ + onClose, + onConfirm, + open, + saving, +}: { + onClose: () => void; + onConfirm: (currentPassword: string) => Promise; + open: boolean; + saving: boolean; +}) { + const [currentPassword, setCurrentPassword] = useState(""); + + useEffect(() => { + if (open) setCurrentPassword(""); + }, [open]); + + return ( + +

+ Verranno eliminati definitivamente account, sessioni, avatar e tutti i + dati finanziari. +

+

+ Questa operazione non può essere annullata. +

+
{ + event.preventDefault(); + if (await onConfirm(currentPassword)) onClose(); + }} + > + + setCurrentPassword(event.target.value)} + required + type="password" + value={currentPassword} + /> + +
+ + +
+
+
+ ); +} diff --git a/src/components/profile/PasswordEditModal.tsx b/src/components/profile/PasswordEditModal.tsx new file mode 100644 index 0000000..74623df --- /dev/null +++ b/src/components/profile/PasswordEditModal.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { EntityFormModal } from "@/components/shared/EntityFormModal"; +import { Field, inputClass } from "@/components/shared/Field"; +import { + SimpleFormActions, + SimpleFormReview, + SimpleFormStepIndicator, +} from "@/components/shared/SimpleFormSteps"; + +export function PasswordEditModal({ + onClose, + onSubmit, + open, + saving, +}: { + onClose: () => void; + onSubmit: (payload: { + currentPassword: string; + newPassword: string; + confirmPassword: string; + }) => Promise; + open: boolean; + saving: boolean; +}) { + const [review, setReview] = useState(false); + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + useEffect(() => { + if (!open) return; + setReview(false); + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); + }, [open]); + + async function submit(event: React.FormEvent) { + event.preventDefault(); + if (!review) { + setReview(true); + return; + } + if (await onSubmit({ currentPassword, newPassword, confirmPassword })) { + onClose(); + } + } + + return ( + + +
+ {review ? ( + + ) : ( +
+ + setCurrentPassword(event.target.value)} + required + type="password" + value={currentPassword} + /> + +
+ + setNewPassword(event.target.value)} + required + type="password" + value={newPassword} + /> + + + setConfirmPassword(event.target.value)} + required + type="password" + value={confirmPassword} + /> + +
+
+ )} + setReview(false)} + onCancel={onClose} + review={review} + saving={saving} + /> + +
+ ); +} diff --git a/src/components/profile/ProfileCard.tsx b/src/components/profile/ProfileCard.tsx new file mode 100644 index 0000000..0f5e3f9 --- /dev/null +++ b/src/components/profile/ProfileCard.tsx @@ -0,0 +1,25 @@ +export function ProfileCard({ + title, + description, + children, + className = "", + contentClassName = "", +}: { + title: string; + description?: string; + children: React.ReactNode; + className?: string; + contentClassName?: string; +}) { + return ( +
+

{title}

+ {description ? ( +

{description}

+ ) : null} +
{children}
+
+ ); +} diff --git a/src/components/profile/ProfilePageClient.tsx b/src/components/profile/ProfilePageClient.tsx new file mode 100644 index 0000000..6c40243 --- /dev/null +++ b/src/components/profile/ProfilePageClient.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { AccountCard } from "@/components/profile/AccountCard"; +import { AccountEditModal } from "@/components/profile/AccountEditModal"; +import { AvatarCropModal } from "@/components/profile/AvatarCropModal"; +import { ConfirmDangerModal } from "@/components/profile/ConfirmDangerModal"; +import { DangerZoneCard } from "@/components/profile/DangerZoneCard"; +import { DeleteAccountModal } from "@/components/profile/DeleteAccountModal"; +import { PasswordEditModal } from "@/components/profile/PasswordEditModal"; +import { PageShell } from "@/components/shared/PageShell"; +import { notify } from "@/components/shared/Toast"; +import { + apiSave, + invalidateFinancialResources, +} from "@/components/shared/useFinanceData"; +import type { UserSession } from "@/types/finance"; + +export function ProfilePageClient({ user }: { user: UserSession }) { + const router = useRouter(); + const [account, setAccount] = useState(user); + const [savingAccount, setSavingAccount] = useState(false); + const [savingPassword, setSavingPassword] = useState(false); + const [savingAvatar, setSavingAvatar] = useState(false); + const [deletingData, setDeletingData] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(false); + const [deletingAccount, setDeletingAccount] = useState(false); + const [confirmDeleteAccount, setConfirmDeleteAccount] = useState(false); + const [editingAccount, setEditingAccount] = useState(false); + const [editingPassword, setEditingPassword] = useState(false); + const [selectedAvatar, setSelectedAvatar] = useState(); + + async function saveAccount(payload: { name: string; email: string }) { + setSavingAccount(true); + try { + const saved = await apiSave( + "/api/profile/account", + "PATCH", + payload, + ); + setAccount(saved); + router.refresh(); + notify("Dati account aggiornati"); + return true; + } catch (error) { + notify(errorMessage(error), "error"); + return false; + } finally { + setSavingAccount(false); + } + } + + async function savePassword(payload: { + currentPassword: string; + newPassword: string; + confirmPassword: string; + }) { + setSavingPassword(true); + try { + await apiSave("/api/profile/account", "POST", payload); + notify("Password aggiornata"); + return true; + } catch (error) { + notify(errorMessage(error), "error"); + return false; + } finally { + setSavingPassword(false); + } + } + + async function uploadAvatar(file: File) { + setSavingAvatar(true); + try { + const formData = new FormData(); + formData.set("avatar", file); + const saved = await saveAvatarRequest("POST", formData); + setAccount(saved); + router.refresh(); + notify("Immagine profilo aggiornata"); + return true; + } catch (error) { + notify(errorMessage(error), "error"); + return false; + } finally { + setSavingAvatar(false); + } + } + + async function editAvatar() { + if (!account.avatarUrl) return; + setSavingAvatar(true); + try { + const response = await fetch(account.avatarUrl, { cache: "no-store" }); + if (!response.ok) throw new Error("Impossibile caricare l'immagine."); + const blob = await response.blob(); + setSelectedAvatar( + new File([blob], avatarFilename(blob.type), { type: blob.type }), + ); + } catch (error) { + notify(errorMessage(error), "error"); + } finally { + setSavingAvatar(false); + } + } + + async function deleteAvatar() { + setSavingAvatar(true); + try { + const saved = await saveAvatarRequest("DELETE"); + setAccount(saved); + router.refresh(); + notify("Immagine profilo rimossa"); + } catch (error) { + notify(errorMessage(error), "error"); + } finally { + setSavingAvatar(false); + } + } + + async function deleteFinancialData() { + setDeletingData(true); + try { + await apiSave("/api/profile/data", "DELETE"); + invalidateFinancialResources(); + setConfirmDelete(false); + notify("Dati finanziari eliminati"); + } catch (error) { + notify(errorMessage(error), "error"); + } finally { + setDeletingData(false); + } + } + + async function deleteAccount(currentPassword: string) { + setDeletingAccount(true); + try { + await apiSave("/api/profile/account", "DELETE", { currentPassword }); + notify("Account eliminato"); + router.push("/setup"); + router.refresh(); + return true; + } catch (error) { + notify(errorMessage(error), "error"); + return false; + } finally { + setDeletingAccount(false); + } + } + + return ( + +
+ setEditingAccount(true)} + onEditAvatar={editAvatar} + onEditPassword={() => setEditingPassword(true)} + onSelectAvatar={setSelectedAvatar} + savingAvatar={savingAvatar} + user={account} + /> + setConfirmDeleteAccount(true)} + onDeleteAll={() => setConfirmDelete(true)} + /> +
+ setEditingAccount(false)} + onSubmit={saveAccount} + open={editingAccount} + saving={savingAccount} + user={account} + /> + setSelectedAvatar(undefined)} + onConfirm={async (file) => { + if (await uploadAvatar(file)) setSelectedAvatar(undefined); + }} + onSelectFile={setSelectedAvatar} + saving={savingAvatar} + /> + setEditingPassword(false)} + onSubmit={savePassword} + open={editingPassword} + saving={savingPassword} + /> + {confirmDelete ? ( + setConfirmDelete(false)} + onConfirm={deleteFinancialData} + saving={deletingData} + /> + ) : null} + setConfirmDeleteAccount(false)} + onConfirm={deleteAccount} + open={confirmDeleteAccount} + saving={deletingAccount} + /> +
+ ); +} + +function avatarFilename(mimeType: string) { + if (mimeType === "image/png") return "avatar.png"; + if (mimeType === "image/jpeg") return "avatar.jpg"; + return "avatar.webp"; +} + +function errorMessage(error: unknown) { + return error instanceof Error ? error.message : "Operazione non riuscita"; +} + +async function saveAvatarRequest(method: "POST" | "DELETE", body?: FormData) { + const response = await fetch("/api/profile/avatar", { method, body }); + const payload = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(payload.error ?? "Operazione non riuscita"); + return payload as UserSession; +} diff --git a/src/components/reports/BillsInsights.tsx b/src/components/reports/BillsInsights.tsx new file mode 100644 index 0000000..545fecf --- /dev/null +++ b/src/components/reports/BillsInsights.tsx @@ -0,0 +1,66 @@ +import { EmptyState } from "@/components/shared/EmptyState"; +import { formatItalianDate } from "@/lib/date/formatDate"; +import { euroFormatter } from "@/lib/finance/money"; +import type { getBillsInsights } from "@/lib/finance/reportAnalysis"; + +type Insights = ReturnType; + +export function BillsInsights({ + insights, + periodLabel, +}: { + insights: Insights; + periodLabel: string; +}) { + const biggest = insights.topBills[0]; + return ( +
+

Analisi bollette

+

+ Una lettura rapida delle bollette per {periodLabel}. +

+ {biggest ? ( +
+ + + +
+ ) : ( +
+ +
+ )} +
+ ); +} + +function Insight({ + label, + text, + value, +}: { + label: string; + text?: string; + value: string; +}) { + return ( +
+

{label}

+

{value}

+ {text ?

{text}

: null} +
+ ); +} diff --git a/src/components/reports/ExpenseBreakdown.tsx b/src/components/reports/ExpenseBreakdown.tsx new file mode 100644 index 0000000..26edc3c --- /dev/null +++ b/src/components/reports/ExpenseBreakdown.tsx @@ -0,0 +1,41 @@ +import type { ExpenseDistributionItem } from "@/lib/finance/expenseDistribution"; +import { euroFormatter } from "@/lib/finance/money"; + +export function ExpenseBreakdown({ + items, + periodLabel, +}: { + items: ExpenseDistributionItem[]; + periodLabel: string; +}) { + return ( +
+

Dettaglio spese

+

+ Quanto incidono le diverse tipologie sulla spesa reale. +

+
+ {items.map((item) => ( +
+
+ +

{item.label}

+
+

+ {euroFormatter.format(item.amount)} +

+

+ {item.percentage.toFixed(1)}% delle spese per {periodLabel} +

+
+ ))} +
+
+ ); +} diff --git a/src/components/reports/MonthSummary.tsx b/src/components/reports/MonthSummary.tsx new file mode 100644 index 0000000..b787f85 --- /dev/null +++ b/src/components/reports/MonthSummary.tsx @@ -0,0 +1,48 @@ +import { euroFormatter } from "@/lib/finance/money"; + +export function MonthSummary({ + income, + label, + projected, + scope, + spent, +}: { + income: number; + label: string; + projected: number; + scope: "mese" | "anno"; + spent: number; +}) { + const balance = income - spent; + const savingsRate = income ? (balance / income) * 100 : 0; + const cards = [ + [`Entrate ${scope}`, euroFormatter.format(income)], + ["Spese reali", euroFormatter.format(spent)], + ["Spese previste", euroFormatter.format(projected)], + [`Saldo ${scope}`, euroFormatter.format(balance)], + [`Risparmio ${scope}`, `${savingsRate.toFixed(1)}%`], + ]; + return ( +
+
+

+ Periodo selezionato +

+

{label}

+
+
+ {cards.map(([cardLabel, value]) => ( +
+

{cardLabel}

+

+ {value} +

+
+ ))} +
+
+ ); +} diff --git a/src/components/reports/ReportsPageClient.tsx b/src/components/reports/ReportsPageClient.tsx new file mode 100644 index 0000000..fe9e781 --- /dev/null +++ b/src/components/reports/ReportsPageClient.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { ExpenseDistribution } from "@/components/dashboard/ExpenseDistribution"; +import { BillsInsights } from "@/components/reports/BillsInsights"; +import { ExpenseBreakdown } from "@/components/reports/ExpenseBreakdown"; +import { MonthSummary } from "@/components/reports/MonthSummary"; +import { SubscriptionInsights } from "@/components/reports/SubscriptionInsights"; +import { LocalMonthNavigator } from "@/components/shared/LocalMonthNavigator"; +import { PageShell } from "@/components/shared/PageShell"; +import { + useBillsResource, + useIncomeResource, + useLedgerResource, + useSubscriptionPaymentsResource, + useSubscriptionsResource, +} from "@/components/shared/useFinanceData"; +import { monthKey } from "@/lib/date/formatDate"; +import { + getMonthlyExpenseDistribution, + getYearlyExpenseDistribution, +} from "@/lib/finance/expenseDistribution"; +import { + getBillsInsights, + getMonthlyReportSummary, + getSubscriptionInsights, +} from "@/lib/finance/reportAnalysis"; +import { buildYearlyDashboardSummary } from "@/lib/finance/yearlySummary"; + +const fullMonthLabels = [ + "Gennaio", + "Febbraio", + "Marzo", + "Aprile", + "Maggio", + "Giugno", + "Luglio", + "Agosto", + "Settembre", + "Ottobre", + "Novembre", + "Dicembre", +]; + +export function ReportsPageClient({ initialYear }: { initialYear: number }) { + const subscriptions = useSubscriptionsResource(); + const subscriptionPayments = useSubscriptionPaymentsResource(); + const bills = useBillsResource(); + const ledger = useLedgerResource(); + const income = useIncomeResource(); + const [period, setPeriod] = useState(() => defaultMonth(initialYear)); + const selectedYear = Number(period.slice(0, 4)); + const yearlySummary = buildYearlyDashboardSummary({ + year: selectedYear, + ledger: ledger.data ?? [], + income: income.data ?? [], + subscriptions: subscriptions.data ?? [], + subscriptionPayments: subscriptionPayments.data ?? [], + bills: bills.data ?? [], + }); + const resources = [ + subscriptions, + subscriptionPayments, + bills, + ledger, + income, + ]; + const loading = resources.some((resource) => resource.loading); + const error = resources.find((resource) => resource.error)?.error; + const annual = period.length === 4; + const selectedMonthSummary = useMemo( + () => + annual + ? undefined + : getMonthlyReportSummary({ + month: period, + ledger: ledger.data ?? [], + income: income.data ?? [], + }), + [annual, income.data, ledger.data, period], + ); + const selectedMonthlyPoint = annual + ? undefined + : yearlySummary.monthly.find((point) => point.month === period); + const periodSummary = annual + ? { + income: yearlySummary.yearlyIncome, + projected: yearlySummary.projectedSpent, + spent: yearlySummary.realSpent, + } + : { + income: selectedMonthSummary?.income ?? 0, + projected: selectedMonthlyPoint?.projected ?? 0, + spent: selectedMonthSummary?.spent ?? 0, + }; + const distribution = useMemo( + () => + annual + ? getYearlyExpenseDistribution(ledger.data ?? [], selectedYear) + : getMonthlyExpenseDistribution(ledger.data ?? [], period), + [annual, ledger.data, period, selectedYear], + ); + const subscriptionInsights = useMemo( + () => getSubscriptionInsights(subscriptions.data ?? [], period), + [period, subscriptions.data], + ); + const billsInsights = useMemo( + () => getBillsInsights(bills.data ?? [], period), + [bills.data, period], + ); + const populatedMonths = useMemo( + () => + new Set( + yearlySummary.monthly + .filter( + (point) => + point.income > 0 || point.spent > 0 || point.projected > 0, + ) + .map((point) => point.month), + ), + [yearlySummary.monthly], + ); + const selectedPeriodLabel = annual + ? `l'anno ${selectedYear}` + : monthLabel(period); + + return ( + + {loading ? ( +

Caricamento dati...

+ ) : null} + {error ?

{error}

: null} +
+ + +
+ + +
+
+ + +
+
+
+ ); +} + +function defaultMonth(year: number) { + const current = monthKey(); + return current.startsWith(`${year}-`) ? current : `${year}-01`; +} + +function monthLabel(month: string) { + const [year, monthNumber] = month.split("-"); + return `${fullMonthLabels[Number(monthNumber) - 1]} ${year}`; +} diff --git a/src/components/reports/SubscriptionInsights.tsx b/src/components/reports/SubscriptionInsights.tsx new file mode 100644 index 0000000..674c249 --- /dev/null +++ b/src/components/reports/SubscriptionInsights.tsx @@ -0,0 +1,63 @@ +import { EmptyState } from "@/components/shared/EmptyState"; +import { euroFormatter } from "@/lib/finance/money"; +import type { getSubscriptionInsights } from "@/lib/finance/reportAnalysis"; + +type Insight = ReturnType[number]; + +export function SubscriptionInsights({ + insights, + periodLabel, +}: { + insights: Insight[]; + periodLabel: string; +}) { + const maximum = Math.max(1, ...insights.map((item) => item.periodCost)); + return ( +
+

Impatto abbonamenti

+

+ Abbonamenti attivi ordinati per costo stimato nel periodo selezionato. +

+
+ {insights.length ? ( + insights.map(({ subscription, periodCost }) => ( +
+
+ + {subscription.title} + + + {euroFormatter.format(periodCost)} + +
+

+ {euroFormatter.format(subscription.amount)} -{" "} + {cycleLabel(subscription.billingCycle)} +

+
+
+
+
+ )) + ) : ( + + )} +
+

{periodLabel}

+
+ ); +} + +function cycleLabel(cycle: Insight["subscription"]["billingCycle"]) { + return ( + { + monthly: "al mese", + weekly: "a settimana", + yearly: "all'anno", + custom: "personalizzato", + }[cycle] ?? cycle + ); +} diff --git a/src/components/shared/AnnualPageToolbar.tsx b/src/components/shared/AnnualPageToolbar.tsx new file mode 100644 index 0000000..b14f3b0 --- /dev/null +++ b/src/components/shared/AnnualPageToolbar.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { YearSelector } from "@/components/dashboard/YearSelector"; + +export function AnnualPageToolbar({ + year, + onChange, + children, +}: { + year: number; + onChange: (year: number) => void; + children?: React.ReactNode; +}) { + return ( +
+ + {children} +
+ ); +} diff --git a/src/components/shared/Badge.tsx b/src/components/shared/Badge.tsx new file mode 100644 index 0000000..e9b24a5 --- /dev/null +++ b/src/components/shared/Badge.tsx @@ -0,0 +1,26 @@ +type BadgeTone = "green" | "yellow" | "red" | "blue" | "violet" | "neutral"; + +const tones: Record = { + green: "border-emerald-400/30 bg-emerald-400/10 text-emerald-200", + yellow: "border-amber-400/30 bg-amber-400/10 text-amber-200", + red: "border-rose-400/30 bg-rose-400/10 text-rose-200", + blue: "border-sky-400/30 bg-sky-400/10 text-sky-200", + violet: "border-violet-400/30 bg-violet-400/10 text-violet-200", + neutral: "border-white/10 bg-white/5 text-slate-300", +}; + +export function Badge({ + children, + tone = "neutral", +}: { + children: React.ReactNode; + tone?: BadgeTone; +}) { + return ( + + {children} + + ); +} diff --git a/src/components/shared/DateField.tsx b/src/components/shared/DateField.tsx new file mode 100644 index 0000000..b296868 --- /dev/null +++ b/src/components/shared/DateField.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { FaCalendarAlt } from "react-icons/fa"; +import { Field, inputClass } from "@/components/shared/Field"; +import { + addMonths, + formatItalianDate, + fromIso, + todayIso, + toIsoDate, +} from "@/lib/date/formatDate"; +import { parseItalianDate } from "@/lib/date/parseItalianDate"; + +export function DateField({ + label, + name, + value, + onChange, + optional = false, +}: { + label: string; + name: string; + value: string; + onChange: (value: string) => void; + optional?: boolean; +}) { + const [display, setDisplay] = useState(formatItalianDate(value)); + const [open, setOpen] = useState(false); + const [error, setError] = useState(""); + const wrapperRef = useRef(null); + const buttonRef = useRef(null); + const calendarRef = useRef(null); + const [viewMonth, setViewMonth] = useState(value.slice(0, 7)); + const [calendarPosition, setCalendarPosition] = useState({ + left: 0, + top: 0, + }); + + useEffect(() => { + setDisplay(formatItalianDate(value)); + setViewMonth(value.slice(0, 7)); + }, [value]); + + useEffect(() => { + function close(event: MouseEvent) { + const target = event.target as Node; + if ( + !wrapperRef.current?.contains(target) && + !calendarRef.current?.contains(target) + ) { + setOpen(false); + } + } + document.addEventListener("mousedown", close); + return () => document.removeEventListener("mousedown", close); + }, []); + + useEffect(() => { + if (!open) return; + function positionCalendar() { + const rect = buttonRef.current?.getBoundingClientRect(); + if (!rect) return; + const width = 288; + const gap = 8; + const left = Math.min( + Math.max(8, rect.right - width), + window.innerWidth - width - 8, + ); + const fitsBelow = rect.bottom + gap + 336 <= window.innerHeight; + setCalendarPosition({ + left, + top: fitsBelow ? rect.bottom + gap : Math.max(8, rect.top - 336 - gap), + }); + } + positionCalendar(); + window.addEventListener("resize", positionCalendar); + window.addEventListener("scroll", positionCalendar, true); + return () => { + window.removeEventListener("resize", positionCalendar); + window.removeEventListener("scroll", positionCalendar, true); + }; + }, [open]); + + const fallbackMonth = value.slice(0, 7) || todayIso().slice(0, 7); + const activeMonth = viewMonth || fallbackMonth; + const days = useMemo(() => calendarDays(activeMonth), [activeMonth]); + + function commit(raw: string) { + if (optional && !raw.trim()) { + setError(""); + onChange(""); + return; + } + const parsed = parseItalianDate(raw); + if (!parsed) { + setError("Data non valida"); + onChange(""); + return; + } + setError(""); + onChange(parsed); + } + + return ( +
+ +
+ commit(display)} + onChange={(event) => { + setDisplay(event.target.value); + setError(""); + }} + /> + +
+ {error ? {error} : null} +
+ {open + ? createPortal( +
+
+ + + {activeMonth} + + +
+
+ {[ + ["mon", "L"], + ["tue", "M"], + ["wed", "M"], + ["thu", "G"], + ["fri", "V"], + ["sat", "S"], + ["sun", "D"], + ].map(([key, day]) => ( + {day} + ))} +
+
+ {days.map((day) => { + const active = day === value; + const inMonth = day.startsWith(activeMonth); + return ( + + ); + })} +
+
, + document.body, + ) + : null} +
+ ); +} + +function calendarDays(month: string) { + const start = fromIso(`${month}-01`); + const mondayOffset = (start.getDay() + 6) % 7; + const gridStart = new Date(start); + gridStart.setDate(start.getDate() - mondayOffset); + return Array.from({ length: 42 }, (_, index) => { + const date = new Date(gridStart); + date.setDate(gridStart.getDate() + index); + return toIsoDate(date); + }); +} diff --git a/src/components/shared/EmptyState.tsx b/src/components/shared/EmptyState.tsx new file mode 100644 index 0000000..6b97658 --- /dev/null +++ b/src/components/shared/EmptyState.tsx @@ -0,0 +1,8 @@ +export function EmptyState({ title, text }: { title: string; text?: string }) { + return ( +
+

{title}

+ {text ?

{text}

: null} +
+ ); +} diff --git a/src/components/shared/EntityFormModal.tsx b/src/components/shared/EntityFormModal.tsx new file mode 100644 index 0000000..7652911 --- /dev/null +++ b/src/components/shared/EntityFormModal.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +const focusableSelector = [ + "button:not([disabled])", + "input:not([disabled])", + "select:not([disabled])", + "textarea:not([disabled])", + "[tabindex]:not([tabindex='-1'])", +].join(","); + +export function EntityFormModal({ + open, + title, + saving, + onClose, + panelClassName = "max-w-2xl", + children, +}: { + open: boolean; + title: string; + saving: boolean; + onClose: () => void; + panelClassName?: string; + children: React.ReactNode; +}) { + const panelRef = useRef(null); + const triggerRef = useRef(null); + const [mounted, setMounted] = useState(open); + const savingRef = useRef(saving); + const onCloseRef = useRef(onClose); + savingRef.current = saving; + onCloseRef.current = onClose; + + useEffect(() => { + if (open) { + setMounted(true); + return; + } + const timeout = window.setTimeout(() => setMounted(false), 140); + return () => window.clearTimeout(timeout); + }, [open]); + + useEffect(() => { + if (!open || !mounted) return; + triggerRef.current = document.activeElement as HTMLElement | null; + const panel = panelRef.current; + const focusable = panel?.querySelectorAll(focusableSelector); + focusable?.[0]?.focus(); + + function onKeyDown(event: KeyboardEvent) { + if (event.key === "Escape" && !savingRef.current) { + event.preventDefault(); + onCloseRef.current(); + return; + } + if (event.key !== "Tab" || !panel) return; + const elements = Array.from( + panel.querySelectorAll(focusableSelector), + ); + if (!elements.length) return; + const first = elements[0]; + const last = elements[elements.length - 1]; + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + } + + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("keydown", onKeyDown); + triggerRef.current?.focus(); + }; + }, [open, mounted]); + + if (!mounted) return null; + + return ( +
{ + if (event.target === event.currentTarget && !saving) onClose(); + }} + role="dialog" + > +
+
+

+ {title} +

+ +
+ {children} +
+
+ ); +} diff --git a/src/components/shared/Field.tsx b/src/components/shared/Field.tsx new file mode 100644 index 0000000..276510e --- /dev/null +++ b/src/components/shared/Field.tsx @@ -0,0 +1,17 @@ +export function Field({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) { + return ( +
+ {label} + {children} +
+ ); +} + +export const inputClass = + "w-full rounded-md border border-white/10 bg-slate-950 px-3 py-2 text-sm text-slate-100 outline-none transition focus:border-sky-400"; diff --git a/src/components/shared/Header.tsx b/src/components/shared/Header.tsx new file mode 100644 index 0000000..e37f3eb --- /dev/null +++ b/src/components/shared/Header.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import { useRef, useState } from "react"; +import { MobileSidebar } from "@/components/shared/navigation/MobileSidebar"; +import { Sidebar } from "@/components/shared/navigation/Sidebar"; +import { SidebarTrigger } from "@/components/shared/navigation/SidebarTrigger"; +import { notify, Toast } from "@/components/shared/Toast"; +import type { UserSession } from "@/types/finance"; + +export function Header({ + user, + children, +}: { + user: UserSession; + children: React.ReactNode; +}) { + const pathname = usePathname(); + const router = useRouter(); + const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); + const sidebarTriggerRef = useRef(null); + + async function logout() { + await fetch("/api/auth/logout", { method: "POST" }); + notify("Disconnessione effettuata"); + router.push("/login"); + router.refresh(); + } + + return ( +
+ + setMobileSidebarOpen(false)} + onLogout={logout} + open={mobileSidebarOpen} + pathname={pathname} + triggerRef={sidebarTriggerRef} + user={user} + /> +
+
+ setMobileSidebarOpen(true)} + open={mobileSidebarOpen} + ref={sidebarTriggerRef} + /> +
+
{children}
+
+ +
+ ); +} diff --git a/src/components/shared/ListCardAction.tsx b/src/components/shared/ListCardAction.tsx new file mode 100644 index 0000000..ae3602e --- /dev/null +++ b/src/components/shared/ListCardAction.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useState } from "react"; +import type { IconType } from "react-icons"; +import { EntityFormModal } from "@/components/shared/EntityFormModal"; + +export function ListCardAction({ + danger = false, + disabled = false, + confirmDescription, + icon: Icon, + label, + onClick, +}: { + danger?: boolean; + disabled?: boolean; + confirmDescription?: string; + icon: IconType; + label: string; + onClick: () => void | Promise; +}) { + const [confirmOpen, setConfirmOpen] = useState(false); + const [confirming, setConfirming] = useState(false); + + async function runAction() { + if (danger && confirmDescription) { + setConfirmOpen(true); + return; + } + await onClick(); + } + + return ( + <> + + {confirmOpen ? ( + setConfirmOpen(false)} + open + saving={confirming} + title="Conferma eliminazione" + > +

{confirmDescription}

+

+ Questa operazione non può essere annullata. +

+
+ + +
+
+ ) : null} + + ); +} diff --git a/src/components/shared/LocalMonthNavigator.tsx b/src/components/shared/LocalMonthNavigator.tsx new file mode 100644 index 0000000..0af8140 --- /dev/null +++ b/src/components/shared/LocalMonthNavigator.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { + LuCalendarRange, + LuChevronLeft, + LuChevronRight, + LuRotateCcw, +} from "react-icons/lu"; +import { monthKey } from "@/lib/date/formatDate"; + +const monthLabels = [ + "Gen", + "Feb", + "Mar", + "Apr", + "Mag", + "Giu", + "Lug", + "Ago", + "Set", + "Ott", + "Nov", + "Dic", +]; + +export function LocalMonthNavigator({ + month, + onChange, + populatedMonths = [], + allowYearSelection = false, + disabled = false, +}: { + month: string; + onChange: (month: string) => void; + populatedMonths?: Iterable; + allowYearSelection?: boolean; + disabled?: boolean; +}) { + const [year, selectedMonthNumber = monthKey().slice(5)] = month.split("-"); + const currentMonth = monthKey(); + const currentYear = currentMonth.slice(0, 4); + const monthsWithContent = new Set(populatedMonths); + + function changeYear(offset: number) { + const nextYear = String(Number(year) + offset); + onChange(month === year ? nextYear : `${nextYear}-${selectedMonthNumber}`); + } + + return ( +
+
+ {allowYearSelection ? ( + + ) : ( + + + {year} + + )} +
+ + + +
+
+
+ {monthLabels.map((label, index) => { + const value = `${year}-${String(index + 1).padStart(2, "0")}`; + const selected = month === value; + const populated = monthsWithContent.has(value); + const current = currentMonth === value && currentYear === year; + return ( + + ); + })} +
+
+ ); +} diff --git a/src/components/shared/MonthlyPageToolbar.tsx b/src/components/shared/MonthlyPageToolbar.tsx new file mode 100644 index 0000000..2ed7583 --- /dev/null +++ b/src/components/shared/MonthlyPageToolbar.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { LocalMonthNavigator } from "@/components/shared/LocalMonthNavigator"; + +export function MonthlyPageToolbar({ + month, + onChange, + populatedMonths, + allowYearSelection = false, + children, +}: { + month: string; + onChange: (month: string) => void; + populatedMonths?: Iterable; + allowYearSelection?: boolean; + children?: React.ReactNode; +}) { + return ( +
+ + {children} +
+ ); +} diff --git a/src/components/shared/PageShell.tsx b/src/components/shared/PageShell.tsx new file mode 100644 index 0000000..98af09e --- /dev/null +++ b/src/components/shared/PageShell.tsx @@ -0,0 +1,26 @@ +export function PageShell({ + title, + description, + actions, + children, +}: { + title: string; + description?: string; + actions?: React.ReactNode; + children: React.ReactNode; +}) { + return ( +
+
+
+

{title}

+ {description ? ( +

{description}

+ ) : null} +
+ {actions} +
+ {children} +
+ ); +} diff --git a/src/components/shared/PaymentBadge.tsx b/src/components/shared/PaymentBadge.tsx new file mode 100644 index 0000000..3de0f51 --- /dev/null +++ b/src/components/shared/PaymentBadge.tsx @@ -0,0 +1,20 @@ +import { getPaymentVisualStyle } from "@/lib/finance/paymentVisualStyle"; +import type { CalendarEventType, PaymentSourceType } from "@/types/finance"; + +export function PaymentBadge({ + sourceType, + status, + children, +}: { + sourceType: PaymentSourceType | CalendarEventType | "income"; + status?: string; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/components/shared/SectionAddButton.tsx b/src/components/shared/SectionAddButton.tsx new file mode 100644 index 0000000..3b669af --- /dev/null +++ b/src/components/shared/SectionAddButton.tsx @@ -0,0 +1,19 @@ +export function SectionAddButton({ + label, + onClick, +}: { + label: string; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/src/components/shared/SectionPeriodTabs.tsx b/src/components/shared/SectionPeriodTabs.tsx new file mode 100644 index 0000000..9eb5da1 --- /dev/null +++ b/src/components/shared/SectionPeriodTabs.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { LuCalendarDays, LuCalendarRange } from "react-icons/lu"; + +export type SectionPeriodView = "month" | "year"; + +export function SectionPeriodTabs({ + view, + onChange, +}: { + view: SectionPeriodView; + onChange: (view: SectionPeriodView) => void; +}) { + return ( +
+ } + label="Mese" + onClick={() => onChange("month")} + /> + } + label="Anno" + onClick={() => onChange("year")} + /> +
+ ); +} + +function Tab({ + active, + icon, + label, + onClick, +}: { + active: boolean; + icon: React.ReactNode; + label: string; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/src/components/shared/Select.tsx b/src/components/shared/Select.tsx new file mode 100644 index 0000000..7efc4e7 --- /dev/null +++ b/src/components/shared/Select.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { LuCheck, LuChevronDown } from "react-icons/lu"; +import { Field, inputClass } from "@/components/shared/Field"; + +type Option = { value: string; label: string }; + +export function Select({ + label, + value, + onChange, + options, +}: { + label: string; + value: string; + onChange: (value: string) => void; + options: Option[]; +}) { + const [open, setOpen] = useState(false); + const [activeIndex, setActiveIndex] = useState(() => + selectedIndex(options, value), + ); + const wrapperRef = useRef(null); + const buttonRef = useRef(null); + const dropdownRef = useRef(null); + const [dropdownPosition, setDropdownPosition] = useState({ + left: 0, + top: 0, + width: 0, + }); + const selected = options.find((option) => option.value === value); + + useEffect(() => { + setActiveIndex(selectedIndex(options, value)); + }, [options, value]); + + useEffect(() => { + function close(event: MouseEvent) { + const target = event.target as Node; + if ( + !wrapperRef.current?.contains(target) && + !dropdownRef.current?.contains(target) + ) { + setOpen(false); + } + } + document.addEventListener("mousedown", close); + return () => document.removeEventListener("mousedown", close); + }, []); + + useEffect(() => { + if (!open) return; + function positionDropdown() { + const rect = buttonRef.current?.getBoundingClientRect(); + if (!rect) return; + const gap = 8; + const estimatedHeight = Math.min(options.length * 40 + 8, 320); + const fitsBelow = + rect.bottom + gap + estimatedHeight <= window.innerHeight; + setDropdownPosition({ + left: Math.max( + 8, + Math.min(rect.left, window.innerWidth - rect.width - 8), + ), + top: fitsBelow + ? rect.bottom + gap + : Math.max(8, rect.top - estimatedHeight - gap), + width: rect.width, + }); + } + positionDropdown(); + window.addEventListener("resize", positionDropdown); + window.addEventListener("scroll", positionDropdown, true); + return () => { + window.removeEventListener("resize", positionDropdown); + window.removeEventListener("scroll", positionDropdown, true); + }; + }, [open, options.length]); + + function choose(option: Option) { + onChange(option.value); + setOpen(false); + buttonRef.current?.focus(); + } + + function moveActive(direction: number) { + setActiveIndex((current) => + Math.min(Math.max(current + direction, 0), options.length - 1), + ); + } + + return ( +
+ + + + {open + ? createPortal( +
+ {options.map((option, index) => { + const active = index === activeIndex; + const checked = option.value === value; + return ( + + ); + })} +
, + document.body, + ) + : null} +
+ ); +} + +function selectedIndex(options: Option[], value: string) { + return Math.max( + 0, + options.findIndex((option) => option.value === value), + ); +} diff --git a/src/components/shared/SimpleFormSteps.tsx b/src/components/shared/SimpleFormSteps.tsx new file mode 100644 index 0000000..7de4340 --- /dev/null +++ b/src/components/shared/SimpleFormSteps.tsx @@ -0,0 +1,107 @@ +import { LuArrowLeft, LuArrowRight, LuCircleCheck } from "react-icons/lu"; + +export function SimpleFormStepIndicator({ review }: { review: boolean }) { + return ( +
+ {["Dati", "Conferma"].map((label, index) => { + const active = review ? index === 1 : index === 0; + const completed = review && index === 0; + return ( +
+ {index + 1}. {label} +
+ ); + })} +
+ ); +} + +export function SimpleFormActions({ + onBack, + onCancel, + review, + saving, +}: { + onBack: () => void; + onCancel?: () => void; + review: boolean; + saving: boolean; +}) { + return ( +
+
+ {review ? ( + + ) : onCancel ? ( + + ) : null} +
+ +
+ ); +} + +export function SimpleFormReview({ + rows, + title, +}: { + rows: { label: string; value: string }[]; + title: string; +}) { + return ( +
+

+ Riepilogo +

+

{title}

+
+ {rows.map((row) => ( +
+
{row.label}
+
{row.value}
+
+ ))} +
+
+ ); +} diff --git a/src/components/shared/TitleCombobox.tsx b/src/components/shared/TitleCombobox.tsx new file mode 100644 index 0000000..b20ee45 --- /dev/null +++ b/src/components/shared/TitleCombobox.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { Field, inputClass } from "@/components/shared/Field"; +import { normalizeTitleName } from "@/lib/validation/paymentTitles"; +import type { PaymentTitleType } from "@/types/finance"; + +type Title = { $id: string; name: string; paymentType: PaymentTitleType }; + +export function TitleCombobox({ + value, + onChange, + onCreate, + titles, + type, +}: { + value: string; + onChange: (value: string) => void; + onCreate?: (name: string) => Promise; + titles: Title[]; + type: PaymentTitleType; +}) { + const [open, setOpen] = useState(false); + const [activeIndex, setActiveIndex] = useState(0); + const wrapperRef = useRef(null); + const inputRef = useRef(null); + const dropdownRef = useRef(null); + const [dropdownPosition, setDropdownPosition] = useState({ + left: 0, + top: 0, + width: 0, + }); + const normalizedValue = normalizeTitleName(value); + const filtered = useMemo( + () => + titles + .filter( + (title) => title.paymentType === type || title.paymentType === "any", + ) + .filter((title) => + normalizeTitleName(title.name).includes(normalizedValue), + ) + .slice(0, 8), + [titles, type, normalizedValue], + ); + const exact = titles.find( + (title) => + (title.paymentType === type || title.paymentType === "any") && + normalizeTitleName(title.name) === normalizedValue, + ); + + useEffect(() => { + function close(event: MouseEvent) { + const target = event.target as Node; + if ( + !wrapperRef.current?.contains(target) && + !dropdownRef.current?.contains(target) + ) { + setOpen(false); + } + } + document.addEventListener("mousedown", close); + return () => document.removeEventListener("mousedown", close); + }, []); + + useEffect(() => { + if (!open) return; + function positionDropdown() { + const rect = inputRef.current?.getBoundingClientRect(); + if (!rect) return; + const gap = 8; + const estimatedHeight = 320; + const fitsBelow = + rect.bottom + gap + estimatedHeight <= window.innerHeight; + setDropdownPosition({ + left: Math.max( + 8, + Math.min(rect.left, window.innerWidth - rect.width - 8), + ), + top: fitsBelow + ? rect.bottom + gap + : Math.max(8, rect.top - estimatedHeight - gap), + width: rect.width, + }); + } + positionDropdown(); + window.addEventListener("resize", positionDropdown); + window.addEventListener("scroll", positionDropdown, true); + return () => { + window.removeEventListener("resize", positionDropdown); + window.removeEventListener("scroll", positionDropdown, true); + }; + }, [open]); + + async function createOrSelect() { + const selected = filtered[activeIndex]; + if (selected) { + onChange(selected.name); + setOpen(false); + return; + } + if (value.trim()) { + await onCreate?.(value.trim()); + setOpen(false); + } + } + + return ( +
+ + { + onChange(event.target.value); + setActiveIndex(0); + setOpen(true); + }} + onFocus={() => setOpen(true)} + onKeyDown={async (event) => { + if (event.key === "ArrowDown") { + event.preventDefault(); + setActiveIndex((current) => + Math.min(current + 1, Math.max(filtered.length - 1, 0)), + ); + } + if (event.key === "ArrowUp") { + event.preventDefault(); + setActiveIndex((current) => Math.max(current - 1, 0)); + } + if (event.key === "Enter") { + event.preventDefault(); + await createOrSelect(); + } + if (event.key === "Escape") setOpen(false); + }} + /> + + {open + ? createPortal( +
+
+ Titoli salvati +
+ {filtered.map((title, index) => ( + + ))} + {value.trim() && !exact ? ( + + ) : null} + {!filtered.length && !value.trim() ? ( +

+ Inizia a scrivere per cercare o creare. +

+ ) : null} +
, + document.body, + ) + : null} +
+ ); +} diff --git a/src/components/shared/Toast.tsx b/src/components/shared/Toast.tsx new file mode 100644 index 0000000..77677ab --- /dev/null +++ b/src/components/shared/Toast.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type ToastItem = { id: number; message: string; type: "ok" | "error" }; + +export function notify(message: string, type: "ok" | "error" = "ok") { + window.dispatchEvent( + new CustomEvent("appspese:toast", { detail: { message, type } }), + ); +} + +export function Toast() { + const [items, setItems] = useState([]); + + useEffect(() => { + function onToast(event: Event) { + const detail = (event as CustomEvent).detail; + const item = { + id: Date.now(), + message: detail.message, + type: detail.type, + }; + setItems((current) => [...current, item]); + setTimeout( + () => + setItems((current) => + current.filter((entry) => entry.id !== item.id), + ), + 3200, + ); + } + window.addEventListener("appspese:toast", onToast); + return () => window.removeEventListener("appspese:toast", onToast); + }, []); + + return ( +
+ {items.map((item) => ( +
+ {item.message} +
+ ))} +
+ ); +} diff --git a/src/components/shared/UserAvatar.tsx b/src/components/shared/UserAvatar.tsx new file mode 100644 index 0000000..26b7af0 --- /dev/null +++ b/src/components/shared/UserAvatar.tsx @@ -0,0 +1,47 @@ +"use client"; + +import Image from "next/image"; +import { useState } from "react"; +import type { UserSession } from "@/types/finance"; + +export function UserAvatar({ + user, + className = "size-10 text-sm", +}: { + user: UserSession; + className?: string; +}) { + const [failedUrl, setFailedUrl] = useState(); + + return ( + + {user.avatarUrl && user.avatarUrl !== failedUrl ? ( + {`Avatar setFailedUrl(user.avatarUrl ?? undefined)} + src={user.avatarUrl} + unoptimized + width={80} + /> + ) : ( + initials(user.name) + )} + + ); +} + +function initials(name: string) { + return ( + name + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]) + .join("") + .toUpperCase() || "?" + ); +} diff --git a/src/components/shared/financeListCard.ts b/src/components/shared/financeListCard.ts new file mode 100644 index 0000000..1f1024c --- /dev/null +++ b/src/components/shared/financeListCard.ts @@ -0,0 +1,2 @@ +export const financeListCardClass = + "rounded-lg border border-sky-400/15 bg-sky-950/15 p-3 transition"; diff --git a/src/components/shared/navigation/MobileSidebar.tsx b/src/components/shared/navigation/MobileSidebar.tsx new file mode 100644 index 0000000..58952ff --- /dev/null +++ b/src/components/shared/navigation/MobileSidebar.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { + SidebarAccount, + SidebarLinks, +} from "@/components/shared/navigation/Sidebar"; +import { SidebarOverlay } from "@/components/shared/navigation/SidebarOverlay"; +import type { UserSession } from "@/types/finance"; + +const focusableSelector = [ + "a[href]", + "button:not([disabled])", + "[tabindex]:not([tabindex='-1'])", +].join(","); + +export function MobileSidebar({ + open, + pathname, + triggerRef, + onClose, + onLogout, + user, +}: { + open: boolean; + pathname: string; + triggerRef: React.RefObject; + onClose: () => void; + onLogout: () => void; + user: UserSession; +}) { + const panelRef = useRef(null); + const onCloseRef = useRef(onClose); + onCloseRef.current = onClose; + + useEffect(() => { + if (!open) return; + panelRef.current?.querySelector(focusableSelector)?.focus(); + + function onKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") { + event.preventDefault(); + onCloseRef.current(); + return; + } + if (event.key !== "Tab" || !panelRef.current) return; + const elements = Array.from( + panelRef.current.querySelectorAll(focusableSelector), + ); + if (!elements.length) return; + const first = elements[0]; + const last = elements[elements.length - 1]; + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + } + + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("keydown", onKeyDown); + triggerRef.current?.focus(); + }; + }, [open, triggerRef]); + + return ( + <> + + + + ); +} diff --git a/src/components/shared/navigation/Sidebar.tsx b/src/components/shared/navigation/Sidebar.tsx new file mode 100644 index 0000000..ecab951 --- /dev/null +++ b/src/components/shared/navigation/Sidebar.tsx @@ -0,0 +1,122 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { + LuChartNoAxesCombined, + LuCreditCard, + LuLayoutDashboard, + LuLogOut, + LuReceiptText, + LuRefreshCw, + LuTrendingUp, + LuUserRound, +} from "react-icons/lu"; +import { UserAvatar } from "@/components/shared/UserAvatar"; +import type { UserSession } from "@/types/finance"; + +export const navigationLinks = [ + { icon: LuLayoutDashboard, title: "Dashboard", href: "/dashboard" }, + { icon: LuTrendingUp, title: "Entrate", href: "/income" }, + { icon: LuReceiptText, title: "Bollette", href: "/bills" }, + { icon: LuRefreshCw, title: "Abbonamenti", href: "/subscriptions" }, + { + icon: LuCreditCard, + title: "Pagamenti Singoli", + href: "/one-time-payments", + }, + { icon: LuChartNoAxesCombined, title: "Report", href: "/reports" }, + { icon: LuUserRound, title: "Profilo", href: "/profile" }, +]; + +export function SidebarBrand() { + return ( +
+ MyMoney +
MyMoney
+
+ ); +} + +export function SidebarLinks({ + pathname, + onNavigate, + className = "", +}: { + pathname: string; + onNavigate?: () => void; + className?: string; +}) { + return ( + + ); +} + +export function SidebarAccount({ + onLogout, + user, +}: { + onLogout: () => void; + user: UserSession; +}) { + return ( +
+ + +
+

{user.name}

+

{user.email}

+
+ + +
+ ); +} + +export function Sidebar({ + onLogout, + pathname, + user, +}: { + onLogout: () => void; + pathname: string; + user: UserSession; +}) { + return ( + + ); +} diff --git a/src/components/shared/navigation/SidebarOverlay.tsx b/src/components/shared/navigation/SidebarOverlay.tsx new file mode 100644 index 0000000..171626d --- /dev/null +++ b/src/components/shared/navigation/SidebarOverlay.tsx @@ -0,0 +1,19 @@ +export function SidebarOverlay({ + open, + onClose, +}: { + open: boolean; + onClose: () => void; +}) { + return ( + + ); +}); diff --git a/src/components/shared/useFinanceData.ts b/src/components/shared/useFinanceData.ts new file mode 100644 index 0000000..9a7358b --- /dev/null +++ b/src/components/shared/useFinanceData.ts @@ -0,0 +1,231 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { notify } from "@/components/shared/Toast"; +import type { + Bill, + Income, + OneTimePayment, + PaymentLedger, + PaymentTitle, + Subscription, + SubscriptionPayment, + UserSettings, +} from "@/types/finance"; + +type CacheEntry = { + data?: T; + error?: string; + promise?: Promise; + refresh?: () => void; + updatedAt: number; + subscribers: Set<() => void>; +}; + +const staleTime = 30_000; +const cache = new Map>(); + +function entryFor(key: string): CacheEntry { + let entry = cache.get(key) as CacheEntry | undefined; + if (!entry) { + entry = { updatedAt: 0, subscribers: new Set() }; + cache.set(key, entry as CacheEntry); + } + return entry; +} + +function notifySubscribers(key: string) { + const entry = cache.get(key); + if (!entry) return; + for (const subscriber of entry.subscribers) subscriber(); +} + +async function getJson(url: string): Promise { + const response = await fetch(url, { cache: "no-store" }); + const payload = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(payload.error ?? `Errore ${url}`); + return payload; +} + +export function useApiResource(key: string, url: string) { + const [, forceRender] = useState(0); + const entry = entryFor(key); + + const revalidate = useCallback( + async (force = false) => { + const current = entryFor(key); + const fresh = Date.now() - current.updatedAt < staleTime; + if (!force && current.data !== undefined && fresh) return current.data; + if (current.promise) return current.promise; + + current.promise = getJson(url) + .then((data) => { + current.data = data; + current.error = undefined; + current.updatedAt = Date.now(); + return data; + }) + .catch((error) => { + current.error = + error instanceof Error ? error.message : "Errore di caricamento"; + notify(current.error, "error"); + throw error; + }) + .finally(() => { + current.promise = undefined; + notifySubscribers(key); + }); + + notifySubscribers(key); + return current.promise; + }, + [key, url], + ); + + useEffect(() => { + const current = entryFor(key); + const subscriber = () => forceRender((value) => value + 1); + const refresh = () => { + revalidate(true).catch(() => undefined); + }; + current.subscribers.add(subscriber); + current.refresh = refresh; + revalidate(false).catch(() => undefined); + return () => { + current.subscribers.delete(subscriber); + if (current.refresh === refresh) current.refresh = undefined; + }; + }, [key, revalidate]); + + const mutate = useCallback( + ( + updater?: T | ((current: T | undefined) => T | undefined), + revalidateAfter = true, + ) => { + const current = entryFor(key); + if (updater !== undefined) { + current.data = + typeof updater === "function" + ? (updater as (current: T | undefined) => T | undefined)( + current.data, + ) + : updater; + current.updatedAt = Date.now(); + notifySubscribers(key); + } + return revalidateAfter + ? revalidate(true).catch(() => undefined) + : Promise.resolve(current.data); + }, + [key, revalidate], + ); + + return { + data: entry.data, + loading: entry.data === undefined && Boolean(entry.promise), + error: entry.error ?? "", + refresh: () => revalidate(true).catch(() => undefined), + mutate, + }; +} + +export function invalidateResource(key: string) { + const current = cache.get(key); + if (!current) return; + current.updatedAt = 0; + notifySubscribers(key); + current.refresh?.(); +} + +export function evictCachedLedgerSource( + sourceType: PaymentLedger["sourceType"], + sourceId: string, +) { + const current = cache.get("payments-ledger") as + | CacheEntry + | undefined; + if (!current?.data) return; + current.data = current.data.filter( + (item) => item.sourceType !== sourceType || item.sourceId !== sourceId, + ); + current.updatedAt = 0; + notifySubscribers("payments-ledger"); + current.refresh?.(); +} + +export function invalidateFinancialResources() { + for (const key of [ + "subscriptions", + "subscription-payments", + "bills", + "one-time-payments", + "payments-ledger", + "payment-titles", + "income", + ]) { + const current = cache.get(key); + if (!current) continue; + current.data = undefined; + current.updatedAt = 0; + notifySubscribers(key); + } +} + +export function useSubscriptionsResource() { + return useApiResource("subscriptions", "/api/subscriptions"); +} + +export function useSubscriptionPaymentsResource() { + return useApiResource( + "subscription-payments", + "/api/subscription-payments", + ); +} + +export function useBillsResource() { + return useApiResource("bills", "/api/bills"); +} + +export function useOneTimePaymentsResource() { + return useApiResource( + "one-time-payments", + "/api/one-time-payments", + ); +} + +export function useLedgerResource() { + return useApiResource( + "payments-ledger", + "/api/payments-ledger", + ); +} + +export function usePaymentTitlesResource() { + return useApiResource( + "payment-titles", + "/api/payment-titles", + ); +} + +export function useIncomeResource() { + return useApiResource("income", "/api/income"); +} + +export function useUserSettingsResource() { + return useApiResource("user-settings", "/api/profile/settings"); +} + +export async function apiSave( + url: string, + method: "POST" | "PATCH" | "DELETE", + body?: object, +) { + const response = await fetch(url, { + method, + headers: body ? { "Content-Type": "application/json" } : undefined, + body: body ? JSON.stringify(body) : undefined, + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(payload.error ?? "Operazione non riuscita"); + return payload as T; +} diff --git a/src/components/subscriptions/SubscriptionForm.tsx b/src/components/subscriptions/SubscriptionForm.tsx new file mode 100644 index 0000000..353771b --- /dev/null +++ b/src/components/subscriptions/SubscriptionForm.tsx @@ -0,0 +1,508 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { LuArrowLeft, LuArrowRight, LuCircleCheck } from "react-icons/lu"; +import { DateField } from "@/components/shared/DateField"; +import { Field, inputClass } from "@/components/shared/Field"; +import { Select } from "@/components/shared/Select"; +import { TitleCombobox } from "@/components/shared/TitleCombobox"; +import { todayIso } from "@/lib/date/formatDate"; +import { formatMoneyInput, parseMoneyInput } from "@/lib/finance/money"; +import type { + BillingCycle, + PaymentTitle, + RenewalIntervalUnit, + Subscription, + SubscriptionStatus, +} from "@/types/finance"; + +type DurationMode = "indefinite" | "date" | "count" | "both"; + +const steps = ["Servizio", "Rinnovo", "Conferma"]; + +export function SubscriptionForm({ + titles, + editing, + onSubmit, + onCreateTitle, + onCancel, + saving = false, +}: { + titles: PaymentTitle[]; + editing?: Subscription; + onSubmit: (data: object) => Promise; + onCreateTitle?: (name: string) => Promise; + onCancel?: () => void; + saving?: boolean; +}) { + const [step, setStep] = useState(0); + const [title, setTitle] = useState(editing?.title ?? ""); + const [amount, setAmount] = useState(formatMoneyInput(editing?.amount)); + const [activationDate, setActivationDate] = useState( + editing?.activationDate ?? todayIso(), + ); + const [firstPaymentDate, setFirstPaymentDate] = useState( + editing?.firstPaymentDate ?? todayIso(), + ); + const [billingCycle, setBillingCycle] = useState( + editing?.billingCycle ?? "monthly", + ); + const [durationMode, setDurationMode] = useState(durationModeFor(editing)); + const [endDate, setEndDate] = useState(editing?.endDate ?? ""); + const [occurrenceCount, setOccurrenceCount] = useState( + String(editing?.occurrenceCount ?? ""), + ); + const [customIntervalValue, setCustomIntervalValue] = useState( + String(editing?.customIntervalValue ?? ""), + ); + const [customIntervalUnit, setCustomIntervalUnit] = useState( + editing?.customIntervalUnit ?? "months", + ); + const [subscriptionStatus, setSubscriptionStatus] = useState( + editing?.subscriptionStatus ?? "active", + ); + const [error, setError] = useState(""); + + useEffect(() => { + setStep(0); + setTitle(editing?.title ?? ""); + setAmount(formatMoneyInput(editing?.amount)); + setActivationDate(editing?.activationDate ?? todayIso()); + setFirstPaymentDate(editing?.firstPaymentDate ?? todayIso()); + setBillingCycle(editing?.billingCycle ?? "monthly"); + setDurationMode(durationModeFor(editing)); + setEndDate(editing?.endDate ?? ""); + setOccurrenceCount(String(editing?.occurrenceCount ?? "")); + setCustomIntervalValue(String(editing?.customIntervalValue ?? "")); + setCustomIntervalUnit(editing?.customIntervalUnit ?? "months"); + setSubscriptionStatus(editing?.subscriptionStatus ?? "active"); + setError(""); + }, [editing]); + + async function submit() { + setError(""); + await onSubmit({ + title, + amount: parseMoneyInput(amount), + activationDate, + firstPaymentDate, + billingCycle, + endDate: + durationMode === "date" || durationMode === "both" ? endDate : "", + occurrenceCount: + durationMode === "count" || durationMode === "both" + ? occurrenceCount + : "", + customIntervalValue: billingCycle === "custom" ? customIntervalValue : "", + customIntervalUnit: billingCycle === "custom" ? customIntervalUnit : "", + subscriptionStatus, + }); + } + + function advance() { + try { + setError(""); + if (step === 0) validateServiceStep(); + if (step === 1) validateRenewalStep(); + setStep((current) => Math.min(current + 1, steps.length - 1)); + } catch (err) { + setError( + err instanceof Error ? err.message : "Controlla i campi inseriti", + ); + } + } + + function validateServiceStep() { + if (!title.trim()) throw new Error("Inserisci il titolo del servizio"); + parseMoneyInput(amount); + if (!activationDate || !firstPaymentDate) { + throw new Error("Inserisci le date del servizio"); + } + if (firstPaymentDate < activationDate) { + throw new Error("Il primo pagamento non può precedere l'attivazione"); + } + } + + function validateRenewalStep() { + if (billingCycle === "custom" && !customIntervalValue) { + throw new Error("Inserisci l'intervallo di rinnovo"); + } + if ((durationMode === "date" || durationMode === "both") && !endDate) { + throw new Error("Inserisci la data di fine"); + } + if ( + (durationMode === "count" || durationMode === "both") && + !occurrenceCount + ) { + throw new Error("Inserisci il numero di rate"); + } + } + + return ( +
{ + event.preventDefault(); + if (step < steps.length - 1) { + advance(); + return; + } + try { + await submit(); + } catch (err) { + setError( + err instanceof Error ? err.message : "Salvataggio non riuscito", + ); + } + }} + > + +
+ {step === 0 ? ( + + ) : null} + {step === 1 ? ( + + ) : null} + {step === 2 ? ( + + ) : null} +
+ {error ?

{error}

: null} +
+
+ {step > 0 ? ( + + ) : onCancel ? ( + + ) : null} +
+ +
+ + ); +} + +function StepIndicator({ current }: { current: number }) { + return ( +
+ {steps.map((label, index) => ( +
+ {index + 1}. {label} +
+ ))} +
+ ); +} + +function ServiceStep({ + titles, + title, + amount, + activationDate, + firstPaymentDate, + onTitleChange, + onAmountChange, + onActivationDateChange, + onFirstPaymentDateChange, + onCreateTitle, +}: { + titles: PaymentTitle[]; + title: string; + amount: string; + activationDate: string; + firstPaymentDate: string; + onTitleChange: (value: string) => void; + onAmountChange: (value: string) => void; + onActivationDateChange: (value: string) => void; + onFirstPaymentDateChange: (value: string) => void; + onCreateTitle?: (name: string) => Promise; +}) { + return ( +
+
+ +
+ + onAmountChange(event.target.value)} + /> + +
+ + +
+ ); +} + +function RenewalStep({ + billingCycle, + durationMode, + endDate, + occurrenceCount, + customIntervalValue, + customIntervalUnit, + onBillingCycleChange, + onDurationModeChange, + onEndDateChange, + onOccurrenceCountChange, + onCustomIntervalValueChange, + onCustomIntervalUnitChange, +}: { + billingCycle: BillingCycle; + durationMode: DurationMode; + endDate: string; + occurrenceCount: string; + customIntervalValue: string; + customIntervalUnit: RenewalIntervalUnit; + onBillingCycleChange: (value: BillingCycle) => void; + onDurationModeChange: (value: DurationMode) => void; + onEndDateChange: (value: string) => void; + onOccurrenceCountChange: (value: string) => void; + onCustomIntervalValueChange: (value: string) => void; + onCustomIntervalUnitChange: (value: RenewalIntervalUnit) => void; +}) { + return ( +
+ + onCustomIntervalValueChange(event.target.value) + } + /> + + onDurationModeChange(value as DurationMode)} + options={[ + { value: "indefinite", label: "Senza scadenza" }, + { value: "date", label: "Fino a una data" }, + { value: "count", label: "Numero di rate" }, + { value: "both", label: "Data e numero di rate" }, + ]} + /> + {durationMode === "date" || durationMode === "both" ? ( + + ) : null} + {durationMode === "count" || durationMode === "both" ? ( + + onOccurrenceCountChange(event.target.value)} + /> + + ) : null} +

+ Senza scadenza il rinnovo continua finché non sospendi o termini il + contratto. +

+
+ ); +} + +function ReviewStep({ + title, + amount, + billingCycle, + durationMode, + editing, + status, + onStatusChange, +}: { + title: string; + amount: string; + billingCycle: BillingCycle; + durationMode: DurationMode; + editing?: Subscription; + status: SubscriptionStatus; + onStatusChange: (value: SubscriptionStatus) => void; +}) { + return ( +
+
+

+ Riepilogo +

+

{title}

+

+ {amount} · {cycleLabels[billingCycle]} ·{" "} + {durationLabels[durationMode]} +

+
+ {editing ? ( +