import { useState } from 'react'; import { Title, Table, Badge, Group, Button, Stack, Text, Modal, TextInput, Textarea, Select, NumberInput, ActionIcon, Card, Loader, Center, Tooltip, } from '@mantine/core'; import { DateInput } from '@mantine/dates'; import { useForm } from '@mantine/form'; import { useDisclosure } from '@mantine/hooks'; import { notifications } from '@mantine/notifications'; import { IconPlus, IconEye, IconCheck, IconX, IconTrash, IconShieldCheck } from '@tabler/icons-react'; import { AttachmentPanel } from '../../components/attachments/AttachmentPanel'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '../../services/api'; import { useIsReadOnly } from '../../stores/authStore'; interface JournalEntryLine { id?: string; account_id: string; account_name?: string; account_number?: string; debit: number; credit: number; memo: string; } interface JournalEntry { id: string; entry_date: string; description: string; reference_number: string; entry_type: string; is_posted: boolean; is_void: boolean; is_reconciled?: boolean; created_at: string; lines?: JournalEntryLine[]; total_debit?: string; total_credit?: string; } interface Account { id: string; account_number: string; name: string; } export function TransactionsPage() { const [opened, { open, close }] = useDisclosure(false); const [viewId, setViewId] = useState(null); const queryClient = useQueryClient(); const isReadOnly = useIsReadOnly(); const { data: entries = [], isLoading } = useQuery({ queryKey: ['journal-entries'], queryFn: async () => { const { data } = await api.get('/journal-entries'); return data; }, }); const { data: accounts = [] } = useQuery({ queryKey: ['accounts'], queryFn: async () => { const { data } = await api.get('/accounts'); return data; }, }); const { data: viewEntry } = useQuery({ queryKey: ['journal-entry', viewId], queryFn: async () => { const { data } = await api.get(`/journal-entries/${viewId}`); return data; }, enabled: !!viewId, }); const [lines, setLines] = useState([ { account_id: '', debit: 0, credit: 0, memo: '' }, { account_id: '', debit: 0, credit: 0, memo: '' }, ]); const form = useForm({ initialValues: { entry_date: new Date(), description: '', reference_number: '', entry_type: 'manual', }, validate: { description: (v) => (v.length > 0 ? null : 'Required'), }, }); const createMutation = useMutation({ mutationFn: async (values: any) => { const payload = { ...values, entry_date: values.entry_date.toISOString().split('T')[0], lines: lines.filter((l) => l.account_id && (l.debit > 0 || l.credit > 0)), }; return api.post('/journal-entries', payload); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['journal-entries'] }); notifications.show({ message: 'Journal entry created', color: 'green' }); close(); form.reset(); setLines([ { account_id: '', debit: 0, credit: 0, memo: '' }, { account_id: '', debit: 0, credit: 0, memo: '' }, ]); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Error', color: 'red' }); }, }); const postMutation = useMutation({ mutationFn: (id: string) => api.post(`/journal-entries/${id}/post`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['journal-entries'] }); queryClient.invalidateQueries({ queryKey: ['accounts'] }); notifications.show({ message: 'Entry posted', color: 'green' }); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Post failed', color: 'red' }); }, }); const voidMutation = useMutation({ mutationFn: (id: string) => api.post(`/journal-entries/${id}/void`, { reason: 'Voided by user' }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['journal-entries'] }); queryClient.invalidateQueries({ queryKey: ['accounts'] }); notifications.show({ message: 'Entry voided', color: 'yellow' }); }, }); const addLine = () => setLines([...lines, { account_id: '', debit: 0, credit: 0, memo: '' }]); const removeLine = (idx: number) => setLines(lines.filter((_, i) => i !== idx)); const updateLine = (idx: number, field: string, value: any) => { const updated = [...lines]; (updated[idx] as any)[field] = value; setLines(updated); }; const totalDebit = lines.reduce((s, l) => s + (l.debit || 0), 0); const totalCredit = lines.reduce((s, l) => s + (l.credit || 0), 0); const isBalanced = Math.abs(totalDebit - totalCredit) < 0.01 && totalDebit > 0; const accountOptions = accounts.map((a) => ({ value: a.id, label: `${a.account_number} - ${a.name}`, })); const fmt = (v: string | number) => { const n = typeof v === 'string' ? parseFloat(v) : v; return n.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); }; if (isLoading) return
; return ( Journal Entries {!isReadOnly && ( )} Date Description Type Ref # Debit Credit Status Actions {entries.map((e) => ( {new Date(e.entry_date).toLocaleDateString()} {e.description} {e.entry_type} {e.reference_number} {fmt(e.total_debit || '0')} {fmt(e.total_credit || '0')} {e.is_void ? ( Void ) : e.is_posted ? ( Posted ) : ( Draft )} {e.is_reconciled && ( }> Reconciled )} setViewId(e.id)}> {!isReadOnly && !e.is_posted && !e.is_void && ( postMutation.mutate(e.id)}> )} {!isReadOnly && e.is_posted && !e.is_void && ( voidMutation.mutate(e.id)}> )} ))} {entries.length === 0 && ( No journal entries yet )}
{/* New Entry Modal */}
createMutation.mutate(values))}>