Skip to content

Repository files navigation

InvoicePro — Web Client

React single-page application for the InvoicePro invoicing platform: onboarding, company and customer management, an item catalogue, GST-aware invoice creation (CGST / SGST / IGST), browser-side PDF export, dashboards, and reports.

Companion repository — the Express + Prisma API this client consumes lives at rupeshv2121/invoice_generator_server.


Table of Contents


System Architecture

Three tiers, plus Supabase as an external identity provider. This client never talks to the database — every read and write goes through the API, and the browser holds only a short-lived bearer token and the public Supabase anon key.

flowchart TB
    subgraph Browser["Web-Client — Vite SPA"]
        UI["Pages and UI components"]
        CTX["AuthContext / SubscriptionContext"]
        APIL["API layer (axios + useApi hook)"]
        PDF["pdfService (jsPDF + autotable)"]
        UI --> CTX --> APIL
        UI --> PDF
    end

    subgraph Server["API (separate repo)"]
        EXP["Express 5 — auth, company, customer,<br/>item, invoice, settings, subscription"]
        PRISMA["Prisma Client"]
        EXP --> PRISMA
    end

    subgraph Supabase["Supabase (managed)"]
        SBAUTH["Auth — users, JWT issuance"]
        SBPG[("PostgreSQL")]
    end

    APIL -- "HTTPS + Bearer JWT" --> EXP
    EXP -- "token verify" --> SBAUTH
    PRISMA --> SBPG
Loading

Design notes

  • Supabase Auth is the identity source of truth. The client stores the issued access_token and sends it as a bearer header; the API verifies it on every request.
  • Context over Redux. Session and subscription state live in two React contexts; page-level data is fetched per page rather than held in a global store.
  • Client-side limit checks are affordances, not enforcement. SubscriptionContext disables actions before the user hits a rejection, but the API re-checks every limit.
  • Invoice totals shown during entry are a preview. The server recomputes all amounts on write, so the two must agree by using the same formulas — they do.

Stack

Concern Choice
Framework React 18 (StrictMode, function components + hooks)
Build Vite 5 with @vitejs/plugin-react
Routing React Router 6 (BrowserRouter)
Styling Tailwind CSS 3.4 + tailwind-merge, class-variance-authority, clsx
Forms react-hook-form
Charts Recharts, D3
Icons lucide-react
Animation framer-motion
HTTP axios
Auth Supabase JS client + API-issued bearer tokens
PDF jspdf + jspdf-autotable
Notifications react-toastify (mounted globally), react-hot-toast
Dates date-fns

Project Structure

invoice_generator/
├── index.html
├── vite.config.js
├── tailwind.config.js / postcss.config.js
└── src/
    ├── main.jsx                  # React root + global toast CSS
    ├── App.jsx                   # Routes + ToastContainer
    ├── Routes.jsx                # providers, route table, error boundary
    ├── supabaseClient.js         # browser Supabase client (anon key)
    ├── api/                      # one module per backend resource
    │   ├── api.js                # axios instance + useApi() authed client
    │   ├── auth.js  customers.js  dashboard.js
    │   ├── invoice.js  items.js  myCompany.js  subscription.js
    ├── context/
    │   ├── AuthContext.jsx       # session, token, login/register/logout
    │   └── SubscriptionContext.jsx
    ├── components/
    │   ├── ProtectedRoute.jsx  ErrorBoundary.jsx  ScrollToTop.jsx
    │   ├── OauthCallback.jsx   SubscriptionBanner.jsx  SubscriptionStatusCard.jsx
    │   ├── AppIcon.jsx  AppImage.jsx
    │   └── ui/                   # Button, Input, Select, Checkbox, Modal,
    │                             # Header, Breadcrumb, SkeletonLoader, QuickActionButton
    ├── pages/                    # one folder per route, each with components/
    │   ├── landing-page/  login/  register/  pricing/  setup/
    │   ├── dashboard/  invoice-creation/  invoice-list/  pdf-preview/
    │   ├── customer-management/  items-management/  reports/
    │   ├── company-profile/  company-management/  settings/
    │   └── NotFound.jsx
    ├── services/
    │   ├── pdfService.js         # jsPDF invoice renderer
    │   └── settingsService.js    # terms & conditions lookup
    ├── styles/                   # tailwind.css, index.css
    └── utils/                    # cn.js (class merge), numberUtils.js

Getting Started

Prerequisites: Node.js 18+, npm, and a running instance of the API server.

git clone https://github.com/rupeshv2121/invoice_generator.git
cd invoice_generator
npm install
# create .env — see below
npm start          # http://localhost:5173

The backend must be running (default http://localhost:3001) for anything past the landing page to work. Clone and start it from invoice_generator_server, and set its FRONTEND_URL to http://localhost:5173 so CORS passes in the other direction.


Environment Variables

Create .env in the repository root. Only VITE_-prefixed variables are exposed to the bundle.

VITE_API_URL="http://localhost:3001"
VITE_SUPABASE_URL="https://<project-ref>.supabase.co"
VITE_SUPABASE_ANON_KEY="<anon-key>"
Variable Required Purpose
VITE_API_URL yes API base URL. Falls back to http://localhost:3001.
VITE_SUPABASE_URL yes Supabase project URL.
VITE_SUPABASE_ANON_KEY yes Public anon key — safe in the browser. Never put the service-role key here.

VITE_API_URL must have no trailing slash, and must match the server's FRONTEND_URL CORS setting in the other direction. .env is git-ignored — never commit real keys.


Scripts

Script Action
npm start Vite dev server with HMR
npm run build vite build --sourcemap → dist/
npm run serve Preview the production build locally

Application Architecture

flowchart TB
    M["main.jsx — createRoot + StrictMode"] --> A["App.jsx"]
    A --> T["ToastContainer (global)"]
    A --> R["Routes.jsx"]
    R --> BR["BrowserRouter"]
    BR --> AP["AuthProvider"]
    AP --> SP["SubscriptionProvider"]
    SP --> EB["ErrorBoundary"]
    EB --> STT["ScrollToTop"]
    STT --> RT["Route table"]
    RT --> PUB["Public: /, /login, /register, /pricing, /auth/callback"]
    RT --> PR["ProtectedRoute"]
    PR --> APP["App pages: dashboard, invoices, customers,<br/>items, reports, settings, company-profile, setup"]
Loading

Providers nest deliberately: SubscriptionProvider sits inside AuthProvider because it reads user from auth context and refetches whenever the session changes.

Data flow for a typical page

sequenceDiagram
    participant P as Page (index.jsx)
    participant H as useXService() hook
    participant AX as useApi() axios client
    participant AC as AuthContext
    participant API as Express API

    P->>H: getCustomers()
    H->>AX: api.get("/api/customer")
    AX->>AC: read token
    AC-->>AX: access_token
    AX->>API: GET + Authorization: Bearer
    API-->>AX: JSON
    AX-->>H: response.data
    H-->>P: setState → render table
Loading

Routing

Defined in src/Routes.jsx.

Path Page Access
/ Landing page Public
/login Login Public
/register Registration Public
/pricing Plan comparison Public
/auth/callback OAuth callback handler Public
/setup Company-profile wizard Semi-protected — session required, profile not
/dashboard Metrics, charts, reminders Protected
/invoice-creation Invoice builder Protected
/invoice-list Invoice table + filters Protected
/pdf-preview Full-page invoice preview Protected
/customer-management Customer CRUD Protected
/items-management Item catalogue CRUD Protected
/company-profile Business details, GST, banking, logo Protected
/settings Invoice prefix, tax defaults, terms Protected
/reports Revenue, customer, GST reports Protected
* NotFound —

State Management

Owns the session and exposes { session, token, user, loading, setSession, login, register, loginWithProvider, logout }.

  • Restore on load — reads access_token from localStorage, validates it with GET /api/auth/me, and clears it on failure.
  • Persist — an effect mirrors session.access_token into localStorage on every change.
  • login / register — post to /api/auth/* and apply the returned session.
  • logout — clears storage and redirects to /login.
  • loginWithProvider — currently throws; OAuth is not configured.

Fetches GET /api/subscription/current whenever user changes and derives UI state: hasActiveSubscription(), canCreateInvoice(), getRemainingInvoices(), getDaysRemaining(), getSubscriptionPlan(), getSubscriptionStatus(), isTrialActive(), isTrialExpiring() (≤ 2 days left), hasTrialExpired(), isSubscriptionExpired().

These drive SubscriptionBanner and SubscriptionStatusCard and disable actions before the user hits a server-side rejection. They are UI affordances only — the server re-checks every limit, so bypassing them client-side gains nothing.

Plans the API reports (-1 = unlimited):

Plan ₹/month Invoices Customers Items
FREE (7-day trial) 0 10 50 100
BASIC 499 100 200 500
PROFESSIONAL 999 ∞ ∞ ∞
ENTERPRISE 2499 ∞ ∞ ∞

API Layer

src/api/api.js exports two things:

  • apiClient — a plain axios instance for unauthenticated calls (login, register, session restore).
  • useApi() — a hook returning an axios instance with a request interceptor that attaches the current AuthContext token as Authorization: Bearer <token> on every call.

Each resource module wraps useApi() in its own hook, so pages call domain methods rather than URLs:

Module Hook Endpoints
auth.js (plain functions) POST /api/auth/login, POST /api/auth/register, logout()
myCompany.js useMyCompanyService GET/POST /api/company, PATCH /api/company/:id
customers.js useCustomersService GET/POST /api/customer, GET /api/customer/:id, PATCH, DELETE, GET /api/customer/stats
items.js useItemService GET/POST /api/item, GET /api/item/:id, PATCH, DELETE, GET /api/item/stats
invoice.js useInvoiceService GET/POST /api/invoice, GET /api/invoice/:id, PATCH, DELETE
subscription.js useSubscriptionService /api/subscription/current, /status, /plans, POST /trial, POST /cancel
dashboard.js useDashboardService /api/dashboard/stats, /api/dashboard/overdue, recent invoices — ⚠️ see Known Gaps

Feature Modules

Landing & marketing

landing-page/ composes HeroSection, FeaturesSection, BenefitsSection, DemoSection, SocialProofSection, PricingSection, CTASection, Navigation, Footer. /pricing renders plan cards from GET /api/subscription/plans.

Onboarding

register/ → setup/ collects business identity, GST registration, address, and banking details, creates the company profile, then starts the 7-day trial.

Dashboard

MetricsCard, InvoiceChart, RevenueChart (Recharts), GSTSummary, RecentInvoicesTable, PaymentReminders, QuickActions.

Invoice creation

The most involved page. CompanyCustomerSelector picks the parties; CompanyDetailsSection / CustomerDetailsSection / BankDetailsSection render the document header; InvoiceDetailsSection captures number, dates, and shipping metadata (marka, date of supply, state code, transportation); InvoiceItemsTable + ItemSelector manage lines with catalogue autocomplete or ad-hoc entry; InvoiceTotalsSection shows live subtotal, CGST, SGST, IGST, and grand total; InvoicePreviewModal and ExportInvoiceTemplate handle preview and export.

Invoice list

InvoiceFilters (search, status, customer, date range), InvoiceTable, InvoiceStatusBadge, InvoiceActionMenu, BulkActions, InvoicePagination. Status lifecycle is DRAFT → SENT → PAID / OVERDUE / CANCELLED.

Customers & items

Matching CRUD modules: *Filters, *Table, *Stats, and a create/edit modal. CustomerHistoryModal shows a customer's invoice history.

Company profile

CompanyDetailsForm, GSTComplianceSection (GSTIN, PAN, IEC, ARN), BankDetailsSection, LogoUploadSection, InvoiceTemplateSettings.

Reports

ReportTypeSelector, DateRangeFilter, RevenueChart, CustomerAnalysis, GSTComplianceSection, ExportControls, backed by /api/settings/reports/*.


Core Pipelines

Onboarding

flowchart LR
    R["/register"] --> SB["Supabase user created via API"]
    SB --> SETUP["/setup — company profile wizard"]
    SETUP --> CP["POST /api/company"]
    CP --> TRIAL["POST /api/subscription/trial<br/>7-day FREE trial"]
    TRIAL --> DASH["/dashboard"]
Loading

Invoice creation

The client composes the document; the server re-computes every number before persisting.

flowchart TD
    A["Invoice creation page"] --> B["Select company + customer"]
    B --> C["Add line items (ItemSelector or manual)"]
    C --> D["InvoiceTotalsSection — live preview"]
    D --> E["POST /api/invoice"]
    E --> F{"API response"}
    F -- "403 SUBSCRIPTION_REQUIRED" --> F1["Prompt to subscribe"]
    F -- "403 LIMIT_REACHED" --> F2["Show remaining + upgrade CTA"]
    F -- "400" --> F3["Surface field errors"]
    F -- "201" --> G["Invoice saved with server number<br/>INV-0001 and final totals"]
    G --> H["Preview / PDF export"]
Loading

Tax maths used for the on-screen preview, matching the server exactly:

amount      = quantity × rate
cgst        = amount × cgstRate / 100
sgst        = amount × sgstRate / 100
igst        = amount × igstRate / 100
lineTotal   = amount + cgst + sgst + igst
totalAmount = Σ amount + Σ cgst + Σ sgst + Σ igst

Defaults are 9% CGST + 9% SGST intra-state and 18% IGST inter-state, overridable per company in Settings and per line at entry time. Invoice numbers are assigned by the server (prefix + zero-padded counter), not the client.

PDF generation

Rendering happens entirely in the browser via jsPDF and jspdf-autotable — no server round-trip, no headless Chrome.

flowchart LR
    I["Invoice data"] --> S["settingsService<br/>terms and conditions"]
    I --> N["numberToWordsIndian()<br/>Crore / Lakh / Thousand"]
    I --> F["formatIndianNumber()<br/>1,23,456.00 grouping"]
    S --> P["pdfService"]
    N --> P
    F --> P
    P --> O["Download / print-ready invoice"]
Loading

src/services/pdfService.js provides:

  • formatIndianNumber(num) — two decimals with Indian digit grouping (1,23,456.00).
  • numberToWordsIndian(num) — amount in words on the Crore / Lakh / Thousand scale, as required on a GST invoice.
  • Terms and conditions pulled through settingsService.getTermsAndConditions().

/pdf-preview renders the same layout as HTML so the document can be checked on screen before export.


UI System

  • Tailwind CSS with a minimal config; composition helper cn() in src/utils/cn.js merges clsx output through tailwind-merge so later classes win predictably.
  • components/ui/ holds the shared primitives — Button (variant-driven via class-variance-authority), Input, Select, Checkbox, Modal, Header, Breadcrumb, QuickActionButton, SkeletonLoader.
  • AppIcon wraps lucide-react for consistent icon sizing; AppImage handles image fallbacks.
  • Feedback — ToastContainer is mounted once in App.jsx (top-right, 3s autoclose); SkeletonLoader covers loading states.
  • Motion — framer-motion for page and modal transitions.
  • Layouts are responsive down to mobile widths throughout.

Deployment

Static build, deployed to Vercel.

npm run build      # → dist/ (with sourcemaps)
npm run serve      # verify the production bundle locally

Checklist:

  1. Set VITE_API_URL, VITE_SUPABASE_URL, and VITE_SUPABASE_ANON_KEY in the hosting project — Vite inlines them at build time, so a change requires a rebuild.
  2. Point VITE_API_URL at the deployed API and set the server's FRONTEND_URL to this app's origin (no trailing slash) so CORS passes.
  3. This is an SPA using BrowserRouter — all paths must rewrite to index.html, or a hard refresh on /dashboard 404s.
  4. dist/ is build output and is git-ignored; keep it that way.

Known Gaps

Documented deliberately so they are not rediscovered as bugs.

Gap Detail
Dashboard endpoints do not exist src/api/dashboard.js calls /api/dashboard/stats and /api/dashboard/overdue; the server mounts no /api/dashboard router. The equivalent data is at /api/settings/reports/dashboard.
company-management page unrouted The folder exists under src/pages/company-management/ but no route references it.
OAuth scaffolded, not wired /auth/callback and OauthCallback.jsx exist, but loginWithProvider() throws "not configured".
Redux installed, unused
Two toast libraries Both react-toastify (mounted globally) and react-hot-toast are installed.
Token in localStorage Convenient for session restore, but readable by any script on the origin; consider httpOnly cookies if XSS risk matters for your deployment.
No tests Testing Library packages are installed but there is no test script or test file.
Pinned react-router-dom 6.0.2 An early v6 release; upgrading unlocks newer router APIs.

License

No license file is present. Add one before distributing.