-
+
This will take less than 5 minutes
diff --git a/resources/js/components/pending-operations-dialog.tsx b/resources/js/components/pending-operations-dialog.tsx
deleted file mode 100644
index 50965be8..00000000
--- a/resources/js/components/pending-operations-dialog.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import { Button } from '@/components/ui/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from '@/components/ui/table';
-import type { SyncStatus } from '@/contexts/sync-context';
-import type { PendingChange } from '@/lib/dexie-db';
-import { formatDistanceToNow } from 'date-fns';
-import { RefreshCw } from 'lucide-react';
-
-interface PendingOperationsDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- operations: PendingChange[];
- onSyncNow: () => void;
- syncStatus: SyncStatus;
- isOnline: boolean;
-}
-
-function getOperationBadgeClass(operation: PendingChange['operation']): string {
- switch (operation) {
- case 'create':
- return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
- case 'update':
- return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
- case 'delete':
- return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
- default:
- return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
- }
-}
-
-function formatDataPreview(data: Record): string {
- if (data.id) {
- const preview = `ID: ${String(data.id).slice(0, 8)}...`;
- if (data.name) {
- return `${preview}, Name: ${data.name}`;
- }
- if (data.description) {
- return `${preview}, ${String(data.description).slice(0, 20)}...`;
- }
- return preview;
- }
- return JSON.stringify(data).slice(0, 50) + '...';
-}
-
-export function PendingOperationsDialog({
- open,
- onOpenChange,
- operations,
- onSyncNow,
- syncStatus,
- isOnline,
-}: PendingOperationsDialogProps) {
- const isSyncing = syncStatus === 'syncing';
- const canSync = isOnline && !isSyncing;
- return (
-
- );
-}
diff --git a/resources/js/components/shared/label-combobox.tsx b/resources/js/components/shared/label-combobox.tsx
index 8970c857..9d34508b 100644
--- a/resources/js/components/shared/label-combobox.tsx
+++ b/resources/js/components/shared/label-combobox.tsx
@@ -1,3 +1,4 @@
+import { store } from '@/actions/App/Http/Controllers/Settings/LabelController';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
@@ -13,7 +14,6 @@ import {
PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
-import { labelSyncService } from '@/services/label-sync';
import { getLabelColorClasses, LABEL_COLORS, type Label } from '@/types/label';
import { Check, ChevronsUpDown, Plus, Tag, X } from 'lucide-react';
import { useState } from 'react';
@@ -80,10 +80,31 @@ export function LabelCombobox({
try {
const randomColor =
LABEL_COLORS[Math.floor(Math.random() * LABEL_COLORS.length)];
- const newLabel = await labelSyncService.findOrCreate(
- inputValue.trim(),
- randomColor,
- );
+
+ const response = await fetch(store.url(), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-XSRF-TOKEN': decodeURIComponent(
+ document.cookie
+ .split('; ')
+ .find((row) => row.startsWith('XSRF-TOKEN='))
+ ?.split('=')[1] || '',
+ ),
+ Accept: 'application/json',
+ },
+ body: JSON.stringify({
+ name: inputValue.trim(),
+ color: randomColor,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to create label');
+ }
+
+ const data = await response.json();
+ const newLabel = data.data || data;
if (newLabel) {
onValueChange([...value, newLabel.id]);
diff --git a/resources/js/components/sync-status-button.tsx b/resources/js/components/sync-status-button.tsx
index c2d8b4c1..72ae698a 100644
--- a/resources/js/components/sync-status-button.tsx
+++ b/resources/js/components/sync-status-button.tsx
@@ -9,27 +9,12 @@ import {
} from '@/components/ui/dropdown-menu';
import { useSyncContext } from '@/contexts/sync-context';
import { formatDistanceToNow } from 'date-fns';
-import {
- CloudAlert,
- CloudCheck,
- CloudOff,
- List,
- RefreshCw,
-} from 'lucide-react';
+import { CloudAlert, CloudCheck, CloudOff, RefreshCw } from 'lucide-react';
import { useState } from 'react';
-import { PendingOperationsDialog } from './pending-operations-dialog';
export function SyncStatusButton() {
- const {
- syncStatus,
- lastSyncTime,
- isOnline,
- sync,
- error,
- pendingOperationsCount,
- pendingOperations,
- } = useSyncContext();
- const [showPendingDialog, setShowPendingDialog] = useState(false);
+ const { syncStatus, lastSyncTime, isOnline, sync, error } =
+ useSyncContext();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const getIcon = () => {
@@ -73,68 +58,34 @@ export function SyncStatusButton() {
};
return (
- <>
-
-
-
-
-
-
-
-
- {getStatusText()}
-
-
- {pendingOperationsCount} pending{' '}
- {pendingOperationsCount === 1
- ? 'operation'
- : 'operations'}
-
-
-
-
- {
- e.preventDefault();
- handleSyncNow();
- }}
- disabled={syncStatus === 'syncing' || !isOnline}
- >
-
- Sync now
-
- setShowPendingDialog(true)}
- >
-
- View pending operations
-
-
-
-
-
- >
+
+
+
+
+
+
+ {getStatusText()}
+
+
+ {
+ e.preventDefault();
+ handleSyncNow();
+ }}
+ disabled={syncStatus === 'syncing' || !isOnline}
+ >
+
+ Sync now
+
+
+
);
}
diff --git a/resources/js/components/transactions/edit-transaction-dialog.tsx b/resources/js/components/transactions/edit-transaction-dialog.tsx
index 2b94eeb6..0e6b94b5 100644
--- a/resources/js/components/transactions/edit-transaction-dialog.tsx
+++ b/resources/js/components/transactions/edit-transaction-dialog.tsx
@@ -1,3 +1,7 @@
+import {
+ index as indexBalances,
+ store as storeBalance,
+} from '@/actions/App/Http/Controllers/AccountBalanceController';
import { LabelCombobox } from '@/components/shared/label-combobox';
import { CategorySelect } from '@/components/transactions/category-select';
import { AmountInput } from '@/components/ui/amount-input';
@@ -22,12 +26,11 @@ import {
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { useEncryptionKey } from '@/contexts/encryption-key-context';
+import { useSyncContext } from '@/contexts/sync-context';
import { decrypt, encrypt, importKey } from '@/lib/crypto';
import { getStoredKey } from '@/lib/key-storage';
import { evaluateRulesForNewTransaction } from '@/lib/rule-engine';
import { appendNoteIfNotPresent } from '@/lib/utils';
-import { accountBalanceSyncService } from '@/services/account-balance-sync';
-import { automationRuleSyncService } from '@/services/automation-rule-sync';
import { transactionSyncService } from '@/services/transaction-sync';
import {
filterTransactionalAccounts,
@@ -48,6 +51,7 @@ interface EditTransactionDialogProps {
accounts: Account[];
banks: Bank[];
labels: Label[];
+ automationRules?: AutomationRule[];
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: (transaction: DecryptedTransaction) => void;
@@ -60,6 +64,7 @@ export function EditTransactionDialog({
accounts,
banks,
labels,
+ automationRules = [],
open,
onOpenChange,
onSuccess,
@@ -69,6 +74,7 @@ export function EditTransactionDialog({
'whisper_money_update_balance_on_transaction';
const { isKeySet } = useEncryptionKey();
+ const { sync } = useSyncContext();
const [transactionDate, setTransactionDate] = useState('');
const [description, setDescription] = useState('');
const [amount, setAmount] = useState(0);
@@ -80,9 +86,6 @@ export function EditTransactionDialog({
const [decryptedAccountNames, setDecryptedAccountNames] = useState<
Map
>(new Map());
- const [automationRules, setAutomationRules] = useState(
- [],
- );
const [updateAccountBalance, setUpdateAccountBalance] = useState(() => {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(STORAGE_KEY_UPDATE_BALANCE);
@@ -161,21 +164,6 @@ export function EditTransactionDialog({
decryptAccountNames();
}, [open, mode, accounts]);
- useEffect(() => {
- if (!open || mode !== 'create') return;
-
- async function loadAutomationRules() {
- try {
- const rules = await automationRuleSyncService.getAll();
- setAutomationRules(rules);
- } catch (error) {
- console.error('Failed to load automation rules:', error);
- }
- }
-
- loadAutomationRules();
- }, [open, mode]);
-
async function checkAndApplyAutomationRules() {
if (mode !== 'create' || automationRules.length === 0) {
return {
@@ -256,26 +244,60 @@ export function EditTransactionDialog({
transactionDateStr: string,
transactionAmount: number,
) {
+ const xsrfToken = decodeURIComponent(
+ document.cookie
+ .split('; ')
+ .find((row) => row.startsWith('XSRF-TOKEN='))
+ ?.split('=')[1] || '',
+ );
+
try {
- const allBalances = await accountBalanceSyncService.getAll();
- const accountBalances = allBalances
- .filter((b) => b.account_id === accountIdToUpdate)
- .sort(
- (a, b) =>
- new Date(b.balance_date).getTime() -
- new Date(a.balance_date).getTime(),
- );
+ // Fetch balances from backend
+ const balancesResponse = await fetch(
+ indexBalances.url(accountIdToUpdate),
+ {
+ headers: {
+ Accept: 'application/json',
+ },
+ },
+ );
+
+ if (!balancesResponse.ok) {
+ throw new Error('Failed to fetch balances');
+ }
+
+ const balancesData = await balancesResponse.json();
+ const accountBalances = (balancesData.data || []).sort(
+ (a: { balance_date: string }, b: { balance_date: string }) =>
+ new Date(b.balance_date).getTime() -
+ new Date(a.balance_date).getTime(),
+ );
const latestBalance =
accountBalances.length > 0 ? accountBalances[0].balance : 0;
const newBalance = latestBalance + transactionAmount;
- await accountBalanceSyncService.updateOrCreate(
- accountIdToUpdate as unknown as number,
- transactionDateStr,
- newBalance,
+ // Store new balance via backend
+ const storeResponse = await fetch(
+ storeBalance.url(accountIdToUpdate),
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-XSRF-TOKEN': xsrfToken,
+ Accept: 'application/json',
+ },
+ body: JSON.stringify({
+ balance_date: transactionDateStr,
+ balance: newBalance,
+ }),
+ },
);
+
+ if (!storeResponse.ok) {
+ throw new Error('Failed to store balance');
+ }
} catch (error) {
console.error('Failed to update account balance:', error);
toast.error('Transaction created, but failed to update balance');
@@ -408,6 +430,9 @@ export function EditTransactionDialog({
onSuccess(newTransaction);
onOpenChange(false);
+
+ // Sync to update IndexedDB
+ sync();
} else {
if (!transaction) {
return;
@@ -493,6 +518,9 @@ export function EditTransactionDialog({
toast.success('Transaction updated successfully');
onSuccess(updatedTransaction);
onOpenChange(false);
+
+ // Sync to update IndexedDB
+ sync();
}
} catch (error) {
console.error('Failed to save transaction:', error);
@@ -679,7 +707,10 @@ export function EditTransactionDialog({
onValueChange={setAccountId}
disabled={isSubmitting}
>
-
+
@@ -748,7 +779,11 @@ export function EditTransactionDialog({
>
Cancel
-