### Supabase Client Setup for Next.js Source: https://context7.com/agusputra69/fintrack-web/llms.txt Provides functions for initializing the Supabase client for both server-side and client-side usage in a Next.js application. It leverages NextAuth for authentication and ensures Row Level Security (RLS) by passing JWT tokens, isolating user data. ```typescript // lib/supabase.ts import { createClient, SupabaseClient } from "@supabase/supabase-js" import { useSession } from "next-auth/react" import { useMemo } from "react" const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || "" const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "" // Server-side client for API routes export function getServerSupabase(supabaseAccessToken: string) { return createClient(supabaseUrl, supabaseAnonKey, { global: { headers: { Authorization: `Bearer ${supabaseAccessToken}`, }, }, auth: { persistSession: false }, }) } // Client-side hook with token-based isolation export function useSupabase(): SupabaseClient { const { data: session } = useSession() const accessToken = (session as any)?.supabaseAccessToken as string | undefined return useMemo(() => { return createClient(supabaseUrl, supabaseAnonKey, { global: { headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {}, }, auth: { persistSession: false }, }) }, [accessToken]) } ``` -------------------------------- ### GET /api/export/excel Source: https://context7.com/agusputra69/fintrack-web/llms.txt Generates a multi-sheet Excel workbook containing financial summaries and transaction data. ```APIDOC ## GET /api/export/excel ### Description Generates a downloadable Excel (.xlsx) workbook with separate sheets for financial summaries and transaction logs. ### Method GET ### Endpoint /api/export/excel ### Parameters #### Query Parameters - **month** (integer) - Optional - The month to export (1-12). Defaults to current month. - **year** (integer) - Optional - The year to export. Defaults to current year. ### Request Example GET /api/export/excel?month=10&year=2023 ### Response #### Success Response (200) - **Content-Type** (header) - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet - **Body** (binary) - Excel file stream #### Response Example [Binary Excel File] ``` -------------------------------- ### GET /api/export/pdf Source: https://context7.com/agusputra69/fintrack-web/llms.txt Generates a PDF report for a specified month and year containing a detailed transaction list. ```APIDOC ## GET /api/export/pdf ### Description Generates a downloadable PDF document containing a list of financial transactions for the requested month and year. ### Method GET ### Endpoint /api/export/pdf ### Parameters #### Query Parameters - **month** (integer) - Optional - The month to export (1-12). Defaults to current month. - **year** (integer) - Optional - The year to export. Defaults to current year. ### Request Example GET /api/export/pdf?month=10&year=2023 ### Response #### Success Response (200) - **Content-Type** (header) - application/pdf - **Body** (binary) - PDF file stream #### Response Example [Binary PDF File] ``` -------------------------------- ### Fetch Monthly Budgets with useBudgets Hook Source: https://context7.com/agusputra69/fintrack-web/llms.txt Retrieves monthly budget allocations for a specific user using SWR for caching. It depends on Supabase for data persistence and a period context to determine the target month. ```typescript export function useBudgets() { const supabase = useSupabase() const { period } = usePeriod() const { data: session } = useSession() const userId = (session?.user as any)?.id const fetcher = async () => { if (!userId) return [] const monthStr = `${period.year}-${String(period.month).padStart(2, '0')}-01` const { data, error } = await supabase .from('budgets') .select('*, category:categories(*)') .eq('user_id', userId) .eq('month', monthStr) if (error) throw error return data as Budget[] } return useSWR(userId ? `budgets-${userId}-${period.year}-${period.month}` : null, fetcher) } ``` -------------------------------- ### Fetch Currency Exchange Rates with Caching Source: https://context7.com/agusputra69/fintrack-web/llms.txt Provides a service to fetch live exchange rates from the Frankfurter API. It implements a 1-hour TTL cache and includes a fallback mechanism for hardcoded rates if the API request fails. ```typescript export async function fetchExchangeRate(from: string, to: string): Promise { if (from === to) return 1 const cacheKey = `${from}-${to}` const cached = rateCache.get(cacheKey) if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.rate } try { const res = await fetch(`https://api.frankfurter.dev/v1/latest?from=${from}&to=${to}`) if (!res.ok) throw new Error(`Frankfurter API error: ${res.status}`) const data = await res.json() const rate = data.rates?.[to] if (!rate) throw new Error(`No rate found for ${from} → ${to}`) rateCache.set(cacheKey, { rate, timestamp: Date.now() }) return rate } catch (error) { const fallbackRates: Record = { 'USD-IDR': 16200, 'SGD-IDR': 12100, 'MYR-IDR': 3650, 'EUR-IDR': 17500, 'JPY-IDR': 108, 'AUD-IDR': 10500, 'GBP-IDR': 20400, } return fallbackRates[cacheKey] ?? 1 } } ``` -------------------------------- ### Configure NextAuth with Supabase Adapter Source: https://context7.com/agusputra69/fintrack-web/llms.txt This configuration sets up NextAuth with Google OAuth and a Supabase adapter. It includes a custom session callback to generate JWT tokens compatible with Supabase Row Level Security. ```typescript import { NextAuthOptions } from "next-auth" import GoogleProvider from "next-auth/providers/google" import { SupabaseAdapter } from "@auth/supabase-adapter" import jwt from "jsonwebtoken" export const authOptions: NextAuthOptions = { providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }), ], adapter: SupabaseAdapter({ url: process.env.NEXT_PUBLIC_SUPABASE_URL as string, secret: process.env.SUPABASE_SERVICE_ROLE_KEY as string, }) as any, session: { strategy: "jwt" }, callbacks: { async session({ session, token }) { const signingSecret = process.env.SUPABASE_JWT_SECRET if (signingSecret && token.sub) { const payload = { aud: "authenticated", exp: Math.floor(new Date(session.expires).getTime() / 1000), sub: token.sub, email: session.user?.email, role: "authenticated", } session.supabaseAccessToken = jwt.sign(payload, signingSecret) if (session.user) (session.user as any).id = token.sub } return session }, }, pages: { signIn: "/login" }, } ``` -------------------------------- ### Initialize Database Schema and RLS Policies Source: https://context7.com/agusputra69/fintrack-web/llms.txt Defines the PostgreSQL tables for transactions, budgets, and categories. It also enables Row Level Security (RLS) to ensure users can only access their own data. ```sql CREATE TABLE transactions ( id uuid NOT NULL DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, amount numeric NOT NULL, amount_idr numeric NOT NULL, currency text DEFAULT 'IDR', exchange_rate numeric DEFAULT 1, category_id uuid REFERENCES categories(id) ON DELETE SET NULL, note text, receipt_url text, source text DEFAULT 'web', type text NOT NULL, date date NOT NULL, created_at timestamp with time zone DEFAULT now(), CONSTRAINT transactions_pkey PRIMARY KEY (id) ); CREATE TABLE budgets ( id uuid NOT NULL DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, category_id uuid NOT NULL REFERENCES categories(id) ON DELETE CASCADE, amount numeric NOT NULL, month date NOT NULL, CONSTRAINT budgets_pkey PRIMARY KEY (id) ); CREATE TABLE categories ( id uuid NOT NULL DEFAULT uuid_generate_v4(), user_id uuid REFERENCES users(id) ON DELETE CASCADE, name text NOT NULL, icon text, color text, type text NOT NULL, CONSTRAINT categories_pkey PRIMARY KEY (id) ); ALTER TABLE transactions ENABLE ROW LEVEL SECURITY; CREATE POLICY "Can manage own transactions" ON transactions FOR ALL USING (auth.uid() = user_id); ALTER TABLE budgets ENABLE ROW LEVEL SECURITY; CREATE POLICY "Can manage own budgets" ON budgets FOR ALL USING (auth.uid() = user_id); INSERT INTO categories (name, icon, color, type) VALUES ('Makanan & Minuman', '🍔', '#DA7756', 'expense'), ('Transportasi', '🚗', '#5BAD7F', 'expense'), ('Gaji', '💰', '#5BAD7F', 'income'); ``` -------------------------------- ### Format and Parse Indonesian Rupiah (TypeScript) Source: https://context7.com/agusputra69/fintrack-web/llms.txt Provides functions to format numbers into Indonesian Rupiah currency strings (e.g., 'Rp 1.234.567') and parse user input strings back into numbers. It also includes helpers for compact currency display and budget percentage calculations. ```typescript // lib/utils.ts // Format number as IDR currency: "Rp 1.234.567" export function formatIDR(amount: number): string { return 'Rp ' + Math.abs(amount).toLocaleString('id-ID') } // Format as compact: "Rp 1,23M" or "Rp 450 rb" export function formatIDRCompact(amount: number): string { if (amount >= 1_000_000_000) return `Rp ${(amount / 1_000_000_000).toFixed(1)}M` if (amount >= 1_000_000) return `Rp ${(amount / 1_000_000).toFixed(2)}M` if (amount >= 1_000) return `Rp ${(amount / 1_000).toFixed(0)} rb` return formatIDR(amount) } // Parse Indonesian formatted money input: "1.500.000" → 1500000 export function parseMoneyInput(displayValue: string): number { if (!displayValue) return 0 const cleaned = displayValue.replace(/\./g, '').replace(',', '.') const num = parseFloat(cleaned) return isNaN(num) ? 0 : num } // Format user input with thousand separators export function handleMoneyChange(inputValue: string): string { const stripped = inputValue.replace(/[^0-9,]/g, '') if (!stripped) return '' const parts = stripped.split(',') const intNum = parseInt(parts[0].replace(/\./g, ''), 10) const formattedInt = isNaN(intNum) ? '' : intNum.toLocaleString('id-ID') return parts[1] !== undefined ? `${formattedInt},${parts[1]}` : formattedInt } // Budget status helpers export function calcBudgetPercent(spent: number, budget: number): number { if (budget === 0) return 0 return Math.round((spent / budget) * 100) } export function getBudgetColor(percent: number): string { if (percent >= 90) return 'var(--danger)' if (percent >= 70) return 'var(--warning)' return 'var(--income)' } ``` -------------------------------- ### Typed Internationalization (i18n) System (TypeScript) Source: https://context7.com/agusputra69/fintrack-web/llms.txt Implements a translation system supporting Indonesian ('ID') and English ('EN') with type-safe dictionary keys. This ensures that only valid translation keys can be used, preventing runtime errors. ```typescript // lib/i18n.ts export type Lang = 'ID' | 'EN' const dict = { nav_dashboard: { ID: 'Dashboard', EN: 'Dashboard' }, nav_add: { ID: 'Tambah Transaksi', EN: 'Add Transaction' }, nav_history: { ID: 'Riwayat', EN: 'History' }, nav_budget: { ID: 'Anggaran', EN: 'Budget' }, kpi_total_expense:{ ID: 'Total Pengeluaran',EN: 'Total Expenses' }, kpi_total_income: { ID: 'Total Pemasukan', EN: 'Total Income' }, budget_status_safe: { ID: 'Aman', EN: 'Safe' }, budget_status_near: { ID: 'Hampir Habis', EN: 'Near Limit' }, budget_status_over: { ID: 'Melebihi', EN: 'Over Budget' }, // ... more translations } as const export type DictKey = keyof typeof dict export function t(key: DictKey, lang: Lang): string { const entry = dict[key] return (entry as any)[lang] as string } export function getMonths(lang: Lang): string[] { return dict.months[lang] as unknown as string[] } // Usage in component: // const { lang } = useLanguage() //

{t('kpi_total_expense', lang)}

// "Total Expenses" or "Total Pengeluaran" ``` -------------------------------- ### Create Transaction with IDR Conversion Source: https://context7.com/agusputra69/fintrack-web/llms.txt Handles the saving of new transactions to Supabase. It automatically calculates the IDR equivalent for foreign currencies and triggers an SWR cache invalidation upon success. ```typescript const handleSave = async () => { if (!amount || !category) { showToast('Amount & Category are required.', 'error') return } const rawAmount = parseMoneyInput(amount) const amountIdr = currency === 'IDR' ? rawAmount : Math.round(rawAmount * exchangeRate) const userId = (session?.user as any)?.id const { error } = await supabase.from('transactions').insert({ user_id: userId, amount: rawAmount, amount_idr: amountIdr, currency, exchange_rate: exchangeRate, category_id: category, note: note || '', type, source: 'web', date, }) if (error) throw error mutate(`transactions-${userId}-${new Date().getFullYear()}-${new Date().getMonth() + 1}`) showToast('Saved!', 'success') } ``` -------------------------------- ### Generate Excel Financial Report API Source: https://context7.com/agusputra69/fintrack-web/llms.txt This API route creates a multi-sheet Excel workbook containing a financial summary and detailed transaction list. It uses the SheetJS (XLSX) library to structure the data and returns the workbook as an Excel file download. ```typescript import { NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { getServerSupabase } from '@/lib/supabase' import * as XLSX from 'xlsx' export async function GET(request: Request) { const session = await getServerSession(authOptions) if (!session?.supabaseAccessToken) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const { searchParams } = new URL(request.url) const month = parseInt(searchParams.get('month') ?? String(new Date().getMonth() + 1)) const year = parseInt(searchParams.get('year') ?? String(new Date().getFullYear())) const supabase = getServerSupabase(session.supabaseAccessToken as string) const userId = (session.user as any)?.id const { data: transactions = [] } = await supabase .from('transactions') .select('*, category:categories(*)') .eq('user_id', userId) const wb = XLSX.utils.book_new() const summaryData = [ ['FinTrack — Monthly Report'], [`Period: ${month}/${year}`], [], ['Metric', 'Value'], ['Total Income', `Rp ${totalIncome.toLocaleString('id-ID')}`], ['Total Expenses', `Rp ${totalExpense.toLocaleString('id-ID')}`], ] XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(summaryData), 'Summary') const txHeaders = ['Date', 'Description', 'Category', 'Type', 'Amount (IDR)'] const txRows = transactions.map(t => [t.date, t.note, t.category?.name, t.type, t.amount_idr]) XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet([txHeaders, ...txRows]), 'Transactions') const buffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) return new NextResponse(buffer, { headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'Content-Disposition': `attachment; filename="FinTrack_Report_${month}_${year}.xlsx"`, }, }) } ``` -------------------------------- ### Period Context for React Applications (TypeScript) Source: https://context7.com/agusputra69/fintrack-web/llms.txt A React context provider and hook for managing the currently selected month and year across the application. It initializes with the current date and allows components to easily access and update the selected period. ```typescript // lib/context/PeriodContext.tsx import React, { createContext, useContext, useState, ReactNode } from 'react' interface Period { month: number; year: number } interface PeriodContextType { period: Period setPeriod: (period: Period) => void } const PeriodContext = createContext(undefined) export function PeriodProvider({ children }: { children: ReactNode }) { const currentDate = new Date() const [period, setPeriod] = useState({ month: currentDate.getMonth() + 1, // 1-12 year: currentDate.getFullYear() }) return ( {children} ) } export function usePeriod() { const context = useContext(PeriodContext) if (!context) throw new Error('usePeriod must be used within a PeriodProvider') return context } // Usage: // const { period, setPeriod } = usePeriod() // setPeriod({ month: 3, year: 2026 }) // Navigate to March 2026 ``` -------------------------------- ### useTransactions Hook for SWR Data Fetching Source: https://context7.com/agusputra69/fintrack-web/llms.txt An SWR-powered custom hook to fetch user transactions from Supabase, filtered by the currently selected month and year from the PeriodContext. It requires user authentication via NextAuth and uses the Supabase client for data retrieval. ```typescript // lib/hooks/useData.ts import useSWR from 'swr' import { useSupabase } from '@/lib/supabase' import { usePeriod } from '@/lib/context/PeriodContext' import { Transaction } from '@/lib/types' import { useSession } from 'next-auth/react' export function useTransactions() { const supabase = useSupabase() const { period } = usePeriod() const { data: session } = useSession() const userId = (session?.user as any)?.id const fetcher = async () => { if (!userId) return [] const startOfMonth = new Date(period.year, period.month - 1, 1).toISOString() const endOfMonth = new Date(period.year, period.month, 0, 23, 59, 59, 999).toISOString() const { data, error } = await supabase .from('transactions') .select('*, category:categories(*)') .eq('user_id', userId) .gte('date', startOfMonth) .lte('date', endOfMonth) .order('date', { ascending: false }) if (error) throw error return data as Transaction[] } return useSWR(userId ? `transactions-${userId}-${period.year}-${period.month}` : null, fetcher) } // Usage in component: // const { data: transactions = [], isLoading } = useTransactions() // const totalExpenses = transactions.filter(tx => tx.type === 'expense').reduce((s, tx) => s + tx.amount_idr, 0) ``` -------------------------------- ### Generate PDF Financial Report API Source: https://context7.com/agusputra69/fintrack-web/llms.txt This API route fetches transaction data for a specific month and year, then generates a formatted PDF report using the jsPDF library. It requires an active user session and returns the file as a downloadable attachment. ```typescript import { NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { getServerSupabase } from '@/lib/supabase' import jsPDF from 'jspdf' import autoTable from 'jspdf-autotable' export async function GET(request: Request) { const session = await getServerSession(authOptions) if (!session?.supabaseAccessToken) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const { searchParams } = new URL(request.url) const month = parseInt(searchParams.get('month') ?? String(new Date().getMonth() + 1)) const year = parseInt(searchParams.get('year') ?? String(new Date().getFullYear())) const supabase = getServerSupabase(session.supabaseAccessToken as string) const userId = (session.user as any)?.id const startOfMonth = new Date(year, month - 1, 1).toISOString() const endOfMonth = new Date(year, month, 0, 23, 59, 59, 999).toISOString() const { data: transactions = [] } = await supabase .from('transactions') .select('*, category:categories(*)') .eq('user_id', userId) .gte('date', startOfMonth) .lte('date', endOfMonth) .order('date', { ascending: false }) const doc = new jsPDF() doc.setFontSize(22) doc.text('FinTrack', 14, 22) autoTable(doc, { startY: 80, head: [['Date', 'Description', 'Category', 'Type', 'Currency', 'Amount (IDR)']], body: transactions.map(t => [ t.date, t.note || '-', t.category?.name || '-', t.type === 'income' ? 'Income' : 'Expense', t.currency || 'IDR', `Rp ${t.amount_idr.toLocaleString('id-ID')}` ]), theme: 'striped', }) const buffer = doc.output('arraybuffer') return new NextResponse(Buffer.from(buffer), { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="FinTrack_Report_${month}_${year}.pdf"`, }, }) } ``` -------------------------------- ### TypeScript Interfaces for FinTrack Data Models Source: https://context7.com/agusputra69/fintrack-web/llms.txt Defines the core TypeScript interfaces for transactions, categories, budgets, and KPI data used within the FinTrack application. These types ensure data consistency and provide a clear structure for the application's domain model. ```typescript // lib/types.ts export type TransactionType = 'expense' | 'income' export type TransactionSource = 'web' | 'telegram' export type Language = 'ID' | 'EN' export type Currency = 'IDR' | 'USD' | 'SGD' | 'MYR' | 'EUR' | 'JPY' | 'AUD' | 'GBP' export interface Transaction { id: string user_id: string amount: number amount_idr: number currency: Currency exchange_rate: number category_id: string category?: Category note: string receipt_url?: string source: TransactionSource type: TransactionType date: string // ISO date string created_at: string } export interface Category { id: string user_id?: string | null name: string icon: string color: string type: 'expense' | 'income' | 'both' } export interface Budget { id: string user_id: string category_id: string category?: Category amount: number month: string // first day of month e.g. "2026-03-01" spent?: number // computed field } export interface KpiData { total_expenses: number total_income: number net_balance: number budget_used_percent: number expense_trend: number // percentage change vs last month income_trend: number } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.