diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 5b9bf481..fb2b4049 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -18,6 +18,7 @@ import { import { StrictMode, useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { toast, Toaster } from 'sonner'; +import { update as updateTimezone } from './actions/App/Http/Controllers/Settings/TimezoneController'; import { AppErrorBoundary } from './components/app-error-boundary'; import { EncryptionKeyProvider } from './contexts/encryption-key-context'; import { PrivacyModeProvider } from './contexts/privacy-mode-context'; @@ -214,7 +215,7 @@ createInertiaApp({ hasAttemptedTimezoneBackfill = true; try { - await axios.patch('/settings/timezone', { + await axios.patch(updateTimezone.url(), { timezone: detectedTimezone, }); } catch { diff --git a/resources/js/components/accounts/account-balance-chart.tsx b/resources/js/components/accounts/account-balance-chart.tsx index 7d582c44..6a9bc8f9 100644 --- a/resources/js/components/accounts/account-balance-chart.tsx +++ b/resources/js/components/accounts/account-balance-chart.tsx @@ -1,3 +1,7 @@ +import { + accountBalanceEvolution, + accountDailyBalanceEvolution, +} from '@/actions/App/Http/Controllers/Api/DashboardAnalyticsController'; import { AccountName } from '@/components/accounts/account-name'; import { type ChartCurrencyMode, @@ -378,9 +382,10 @@ export function AccountBalanceChart({ if (currentGranularity === 'daily') { // Fetch DAILY_DAYS + 1 days (extra day for DoD baseline) const from = format(subDays(now, DAILY_DAYS), 'yyyy-MM-dd'); - const params = new URLSearchParams({ from, to }); const response = await fetch( - `/api/dashboard/account/${account.id}/daily-balance-evolution?${params.toString()}`, + accountDailyBalanceEvolution.url(account.id, { + query: { from, to }, + }), ); const data: AccountDailyBalanceData = await response.json(); // Normalize daily data so the rest of the component works uniformly @@ -391,9 +396,10 @@ export function AccountBalanceChart({ }); } else { const from = format(subMonths(now, 12), 'yyyy-MM-dd'); - const params = new URLSearchParams({ from, to }); const response = await fetch( - `/api/dashboard/account/${account.id}/balance-evolution?${params.toString()}`, + accountBalanceEvolution.url(account.id, { + query: { from, to }, + }), ); const data = await response.json(); setBalanceData(data); diff --git a/resources/js/components/categories/category-analysis-drawer.tsx b/resources/js/components/categories/category-analysis-drawer.tsx index 503b6a24..dbc1656f 100644 --- a/resources/js/components/categories/category-analysis-drawer.tsx +++ b/resources/js/components/categories/category-analysis-drawer.tsx @@ -1,3 +1,4 @@ +import monthlyBreakdown from '@/actions/App/Http/Controllers/Api/CategoryMonthlyBreakdownController'; import { CategoryCombobox } from '@/components/shared/category-combobox'; import { AmountDisplay } from '@/components/ui/amount-display'; import { Card, CardContent } from '@/components/ui/card'; @@ -130,7 +131,7 @@ export function CategoryAnalysisDrawer({ setIsLoading(true); setError(null); - fetch(`/api/categories/${categoryId}/monthly-breakdown`, { + fetch(monthlyBreakdown.url(categoryId), { headers: { Accept: 'application/json' }, }) .then((response) => { diff --git a/resources/js/components/dashboard/net-worth-chart.tsx b/resources/js/components/dashboard/net-worth-chart.tsx index ab4c25e1..1352ddc3 100644 --- a/resources/js/components/dashboard/net-worth-chart.tsx +++ b/resources/js/components/dashboard/net-worth-chart.tsx @@ -1,3 +1,5 @@ +import { update as updateLoanPreference } from '@/actions/App/Http/Controllers/Settings/NetWorthChartLoanPreferenceController'; +import { update as updateRealEstatePreference } from '@/actions/App/Http/Controllers/Settings/NetWorthChartRealEstatePreferenceController'; import { AccountName } from '@/components/accounts/account-name'; import { type ChartGranularity, @@ -439,7 +441,7 @@ export function NetWorthChart({ const handleIncludeLoansChange = useCallback((includeLoans: boolean) => { router.patch( - '/settings/net-worth-chart-loan-preference', + updateLoanPreference.url(), { include_loans_in_net_worth_chart: includeLoans, }, @@ -454,7 +456,7 @@ export function NetWorthChart({ const handleIncludeRealEstateChange = useCallback( (includeRealEstate: boolean) => { router.patch( - '/settings/net-worth-chart-real-estate-preference', + updateRealEstatePreference.url(), { include_real_estate_in_net_worth_chart: includeRealEstate, }, diff --git a/resources/js/components/transactions/import-step-preview.tsx b/resources/js/components/transactions/import-step-preview.tsx index 818b0b2f..8ba32c97 100644 --- a/resources/js/components/transactions/import-step-preview.tsx +++ b/resources/js/components/transactions/import-step-preview.tsx @@ -1,3 +1,4 @@ +import { index as transactionsIndex } from '@/actions/App/Http/Controllers/Api/TransactionController'; import { TransactionDescription } from '@/components/transactions/transaction-description'; import { AmountDisplay } from '@/components/ui/amount-display'; import { Badge } from '@/components/ui/badge'; @@ -58,9 +59,11 @@ export function ImportStepPreview({ } axios - .get('/api/transactions', { - params: { account_id: accountId, per_page: 10 }, - }) + .get( + transactionsIndex.url({ + query: { account_id: accountId, per_page: 10 }, + }), + ) .then((response) => { setExistingTransactions(response.data.data ?? []); }) diff --git a/resources/js/components/transactions/import-transactions-button.tsx b/resources/js/components/transactions/import-transactions-button.tsx index baa399bd..bebb013e 100644 --- a/resources/js/components/transactions/import-transactions-button.tsx +++ b/resources/js/components/transactions/import-transactions-button.tsx @@ -1,3 +1,4 @@ +import { index as importDataRoute } from '@/actions/App/Http/Controllers/Api/ImportDataController'; import { Button } from '@/components/ui/button'; import { Tooltip, @@ -30,7 +31,7 @@ export function ImportTransactionsButton() { // Fetch data on-demand when drawer opens setLoading(true); try { - const response = await fetch('/api/import/data'); + const response = await fetch(importDataRoute.url()); if (!response.ok) { throw new Error('Failed to load import data'); } diff --git a/resources/js/components/transactions/import-transactions-drawer.tsx b/resources/js/components/transactions/import-transactions-drawer.tsx index 18ceae2f..4713d3b7 100644 --- a/resources/js/components/transactions/import-transactions-drawer.tsx +++ b/resources/js/components/transactions/import-transactions-drawer.tsx @@ -2,6 +2,7 @@ import { index as indexBalances, store as storeBalance, } from '@/actions/App/Http/Controllers/AccountBalanceController'; +import { categorize } from '@/actions/App/Http/Controllers/TransactionController'; import AlertError from '@/components/alert-error'; import { Drawer, @@ -687,8 +688,7 @@ export function ImportTransactionsDrawer({ uncategorizedCount > 0 ? { label: 'Categorize', - onClick: () => - router.visit('/transactions/categorize'), + onClick: () => router.visit(categorize.url()), } : undefined, }); @@ -703,8 +703,7 @@ export function ImportTransactionsDrawer({ uncategorizedCount > 0 ? { label: 'Categorize', - onClick: () => - router.visit('/transactions/categorize'), + onClick: () => router.visit(categorize.url()), } : undefined, }); @@ -718,8 +717,7 @@ export function ImportTransactionsDrawer({ uncategorizedCount > 0 ? { label: 'Categorize', - onClick: () => - router.visit('/transactions/categorize'), + onClick: () => router.visit(categorize.url()), } : undefined, }); diff --git a/resources/js/components/transactions/saved-filters.tsx b/resources/js/components/transactions/saved-filters.tsx index e73833ba..ff73d16a 100644 --- a/resources/js/components/transactions/saved-filters.tsx +++ b/resources/js/components/transactions/saved-filters.tsx @@ -1,3 +1,9 @@ +import { + destroy as destroySavedFilter, + index as savedFiltersIndex, + store as storeSavedFilter, + update as updateSavedFilter, +} from '@/actions/App/Http/Controllers/Api/SavedFilterController'; import { AlertDialog, AlertDialogAction, @@ -113,7 +119,7 @@ export function SavedFilters({ filters, onLoad }: SavedFiltersProps) { let active = true; axios - .get<{ data: SavedFilter[] }>('/api/saved-filters') + .get<{ data: SavedFilter[] }>(savedFiltersIndex.url()) .then((response) => { if (active) { setSavedFilters(response.data.data); @@ -143,7 +149,7 @@ export function SavedFilters({ filters, onLoad }: SavedFiltersProps) { } try { - await axios.delete(`/api/saved-filters/${savedFilter.id}`); + await axios.delete(destroySavedFilter.url(savedFilter.id)); } catch (error) { console.error('Failed to delete saved filter:', error); setSavedFilters(previous); @@ -154,7 +160,7 @@ export function SavedFilters({ filters, onLoad }: SavedFiltersProps) { async function handleUpdate(savedFilter: SavedFilter) { try { const response = await axios.patch<{ data: SavedFilter }>( - `/api/saved-filters/${savedFilter.id}`, + updateSavedFilter.url(savedFilter.id), { filters: serializeFilters(filters) }, ); @@ -180,7 +186,7 @@ export function SavedFilters({ filters, onLoad }: SavedFiltersProps) { setIsSaving(true); try { const response = await axios.post<{ data: SavedFilter }>( - '/api/saved-filters', + storeSavedFilter.url(), { name: trimmedName, filters: serializeFilters(filters), diff --git a/resources/js/contexts/encryption-key-context.tsx b/resources/js/contexts/encryption-key-context.tsx index fdd430d9..126f9a5e 100644 --- a/resources/js/contexts/encryption-key-context.tsx +++ b/resources/js/contexts/encryption-key-context.tsx @@ -1,3 +1,4 @@ +import { getMessage } from '@/actions/App/Http/Controllers/EncryptionController'; import { clearKey, getStoredKey } from '@/lib/key-storage'; import axios from 'axios'; import { @@ -56,7 +57,7 @@ export function EncryptionKeyProvider({ try { const response = await axios.get( - '/api/encryption/message', + getMessage.url(), ); setEncryptedMessageData(response.data); } catch (err) { diff --git a/resources/js/hooks/use-cashflow-data.ts b/resources/js/hooks/use-cashflow-data.ts index 69fcb745..0955809d 100644 --- a/resources/js/hooks/use-cashflow-data.ts +++ b/resources/js/hooks/use-cashflow-data.ts @@ -1,3 +1,9 @@ +import { + breakdown as cashflowBreakdown, + sankey as cashflowSankey, + summary as cashflowSummary, + trend as cashflowTrend, +} from '@/actions/App/Http/Controllers/Api/CashflowAnalyticsController'; import { Category } from '@/types/category'; import { endOfMonth, format, startOfMonth } from 'date-fns'; import { useCallback, useEffect, useState } from 'react'; @@ -109,30 +115,32 @@ export function useCashflowData({ const fromStr = format(from, 'yyyy-MM-dd'); const toStr = format(to, 'yyyy-MM-dd'); - const periodParams = new URLSearchParams({ - from: fromStr, - to: toStr, - }); - const periodQuery = `?${periodParams.toString()}`; + const periodQuery = { from: fromStr, to: toStr }; const trendQuery = - periodType === 'month' ? `?months=12&to=${toStr}` : periodQuery; + periodType === 'month' + ? { months: 12, to: toStr } + : periodQuery; const [summary, sankey, trend, incomeBreakdown, expenseBreakdown] = await Promise.all([ - fetch(`/api/cashflow/summary${periodQuery}`).then((r) => - r.json(), + fetch(cashflowSummary.url({ query: periodQuery })).then( + (r) => r.json(), ), - fetch(`/api/cashflow/sankey${periodQuery}`).then((r) => - r.json(), + fetch(cashflowSankey.url({ query: periodQuery })).then( + (r) => r.json(), ), - fetch(`/api/cashflow/trend${trendQuery}`).then((r) => + fetch(cashflowTrend.url({ query: trendQuery })).then((r) => r.json(), ), fetch( - `/api/cashflow/breakdown${periodQuery}&type=income`, + cashflowBreakdown.url({ + query: { ...periodQuery, type: 'income' }, + }), ).then((r) => r.json()), fetch( - `/api/cashflow/breakdown${periodQuery}&type=expense`, + cashflowBreakdown.url({ + query: { ...periodQuery, type: 'expense' }, + }), ).then((r) => r.json()), ]); diff --git a/resources/js/hooks/use-decrypt-account-names.ts b/resources/js/hooks/use-decrypt-account-names.ts index fe837712..7884bdb3 100644 --- a/resources/js/hooks/use-decrypt-account-names.ts +++ b/resources/js/hooks/use-decrypt-account-names.ts @@ -1,3 +1,7 @@ +import { + index as accountsIndex, + update as updateAccount, +} from '@/actions/App/Http/Controllers/Api/AccountController'; import { useEncryptionKey } from '@/contexts/encryption-key-context'; import { decrypt, importKey } from '@/lib/crypto'; import { getStoredKey } from '@/lib/key-storage'; @@ -32,8 +36,9 @@ export function useDecryptAccountNames() { return; } - const { data: accounts } = - await axios.get('/api/accounts'); + const { data: accounts } = await axios.get( + accountsIndex.url(), + ); const encryptedAccounts = accounts.filter( (a) => a.encrypted && a.name_iv, @@ -53,7 +58,7 @@ export function useDecryptAccountNames() { account.name_iv!, ); - await axios.put(`/api/accounts/${account.id}`, { + await axios.put(updateAccount.url(account.id), { name: decryptedName, encrypted: false, }); diff --git a/resources/js/hooks/use-decrypt-transactions.ts b/resources/js/hooks/use-decrypt-transactions.ts index c6b5809c..07ee4504 100644 --- a/resources/js/hooks/use-decrypt-transactions.ts +++ b/resources/js/hooks/use-decrypt-transactions.ts @@ -1,3 +1,4 @@ +import { bulkUpdate } from '@/actions/App/Http/Controllers/Api/TransactionController'; import { useEncryptionKey } from '@/contexts/encryption-key-context'; import { decrypt, importKey } from '@/lib/crypto'; import { getStoredKey } from '@/lib/key-storage'; @@ -127,7 +128,7 @@ export function useDecryptTransactions() { for (let i = 0; i < batch.length; i += 50) { const chunk = batch.slice(i, i + 50); await withRetry(() => - axios.patch('/api/transactions/bulk', { + axios.patch(bulkUpdate.url(), { transactions: chunk, }), ); diff --git a/resources/js/lib/import-config-storage.ts b/resources/js/lib/import-config-storage.ts index 57848a5d..8f52fde6 100644 --- a/resources/js/lib/import-config-storage.ts +++ b/resources/js/lib/import-config-storage.ts @@ -1,3 +1,7 @@ +import { + show as showImportConfig, + update as updateImportConfig, +} from '@/actions/App/Http/Controllers/Api/AccountImportConfigController'; import type { BalanceColumnMapping } from '@/types/balance-import'; import { type ColumnMapping, DateFormat } from '@/types/import'; import { type UUID } from '@/types/uuid'; @@ -15,17 +19,13 @@ interface BalanceImportConfig { type ImportConfigType = 'transaction' | 'balance'; -function configUrl(accountId: UUID): string { - return `/api/accounts/${accountId}/import-config`; -} - async function saveConfig( accountId: UUID, type: ImportConfigType, config: ImportConfig | BalanceImportConfig, ): Promise { try { - await axios.put(configUrl(accountId), { type, config }); + await axios.put(updateImportConfig.url(accountId), { type, config }); } catch (error) { console.error(`Failed to save ${type} import configuration:`, error); } @@ -37,8 +37,7 @@ async function loadConfig( ): Promise { try { const { data } = await axios.get<{ data: T | null }>( - configUrl(accountId), - { params: { type } }, + showImportConfig.url(accountId, { query: { type } }), ); const config = data.data; diff --git a/resources/js/pages/settings/appearance.tsx b/resources/js/pages/settings/appearance.tsx index 18d9f78e..c6a7e292 100644 --- a/resources/js/pages/settings/appearance.tsx +++ b/resources/js/pages/settings/appearance.tsx @@ -1,3 +1,4 @@ +import { update as updateChartColorScheme } from '@/actions/App/Http/Controllers/Settings/ChartColorSchemeController'; import { __ } from '@/utils/i18n'; import { Head, router } from '@inertiajs/react'; @@ -38,7 +39,7 @@ export default function Appearance() { updateScheme(newScheme); router.patch( - '/settings/chart-color-scheme', + updateChartColorScheme.url(), { chart_color_scheme: newScheme }, { preserveScroll: true }, ); diff --git a/resources/js/pages/transactions/index.tsx b/resources/js/pages/transactions/index.tsx index 069266f7..584f6f8d 100644 --- a/resources/js/pages/transactions/index.tsx +++ b/resources/js/pages/transactions/index.tsx @@ -1,3 +1,4 @@ +import { bulkUpdate as bulkUpdateTransactions } from '@/actions/App/Http/Controllers/TransactionController'; import { useLocale } from '@/hooks/use-locale'; import { usePollJobStatus } from '@/hooks/use-poll-job-status'; import { __ } from '@/utils/i18n'; @@ -1121,7 +1122,7 @@ export default function Transactions({ if (isSelectingAll) { const toastId = toast.loading(__('Updating transactions...')); const response = await axios.patch<{ count: number }>( - '/transactions/bulk', + bulkUpdateTransactions.url(), { filters: clientFiltersToBackendFilters(filters), category_id: categoryId, @@ -1262,7 +1263,7 @@ export default function Transactions({ if (isSelectingAll) { const toastId = toast.loading(__('Updating transactions...')); const response = await axios.patch<{ count: number }>( - '/transactions/bulk', + bulkUpdateTransactions.url(), { filters: clientFiltersToBackendFilters(filters), label_ids: labelIds, diff --git a/resources/js/services/transaction-sync.ts b/resources/js/services/transaction-sync.ts index 3d6b2653..cd82ea54 100644 --- a/resources/js/services/transaction-sync.ts +++ b/resources/js/services/transaction-sync.ts @@ -1,3 +1,11 @@ +// Aliased because this service's own methods share these names. +import { index as syncTransactions } from '@/actions/App/Http/Controllers/Sync/TransactionSyncController'; +import { + bulkUpdate as bulkUpdateRoute, + destroy as destroyRoute, + store as storeRoute, + update as updateRoute, +} from '@/actions/App/Http/Controllers/TransactionController'; import { db, withDb } from '@/lib/dexie-db'; import { TransactionSyncManager } from '@/lib/sync-manager'; import type { LearnedRuleNotice } from '@/types/automation-rule'; @@ -32,7 +40,7 @@ class TransactionSyncService { constructor() { this.syncManager = new TransactionSyncManager({ - endpoint: '/api/sync/transactions', + endpoint: syncTransactions.url(), transformFromServer: (data) => { const label_ids = data.labels?.map((l: { id: string }) => l.id); // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -69,7 +77,7 @@ class TransactionSyncService { data: Omit, options?: { updateBalance?: boolean }, ): Promise { - const response = await axios.post('/transactions', { + const response = await axios.post(storeRoute.url(), { ...data, ...(options?.updateBalance ? { update_balance: true } : {}), }); @@ -106,7 +114,7 @@ class TransactionSyncService { ): Promise { const { label_ids, ...transactionData } = data; - const response = await axios.patch(`/transactions/${id}`, { + const response = await axios.patch(updateRoute.url(id), { ...transactionData, label_ids, ...(options?.updateBalance ? { update_balance: true } : {}), @@ -134,7 +142,7 @@ class TransactionSyncService { ): Promise { const { label_ids, ...transactionData } = data; - await axios.patch('/transactions/bulk', { + await axios.patch(bulkUpdateRoute.url(), { transaction_ids: ids, label_ids: label_ids, ...transactionData, @@ -178,7 +186,7 @@ class TransactionSyncService { requestFilters.debtor_name = filters.debtorName; } - const response = await axios.patch('/transactions/bulk', { + const response = await axios.patch(bulkUpdateRoute.url(), { filters: requestFilters, label_ids: label_ids, ...transactionData, @@ -191,7 +199,7 @@ class TransactionSyncService { id: string, options?: { updateBalance?: boolean }, ): Promise { - await axios.delete(`/transactions/${id}`, { + await axios.delete(destroyRoute.url(id), { data: options?.updateBalance ? { update_balance: true } : undefined, }); // The API delete above is authoritative; the local cache eviction is