el-contador 1.2.5 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frontend/src/hooks/useBank.ts +4 -3
- package/frontend/src/hooks/useContacts.ts +3 -0
- package/frontend/src/hooks/useExpenses.ts +13 -0
- package/frontend/src/hooks/useSettings.ts +84 -0
- package/frontend/src/index.css +4 -4
- package/frontend/src/layouts/ProtectedLayout.tsx +7 -5
- package/frontend/src/pages/Bank.tsx +84 -6
- package/frontend/src/pages/Contacts.tsx +39 -17
- package/frontend/src/pages/Dashboard.tsx +24 -24
- package/frontend/src/pages/Expenses.tsx +71 -38
- package/frontend/src/pages/Reconciliation.tsx +51 -15
- package/frontend/src/pages/Settings.tsx +165 -83
- package/package.json +1 -1
- package/server/db/init.js +18 -1
- package/server/db/schema.sql +229 -1
- package/server/index.js +4 -0
- package/server/package.json +1 -1
- package/server/routes/account-groups.js +20 -0
- package/server/routes/accounts.js +180 -0
- package/server/routes/bank.js +99 -38
- package/server/routes/dashboard.js +23 -0
- package/server/routes/expenses.js +59 -11
- package/server/routes/invoice-config.js +4 -2
- package/server/routes/public.js +10 -2
- package/server/routes/reconciliation.js +133 -5
- package/server/routes/sales.js +6 -0
- package/server/routes/suppliers.js +37 -11
- package/server/routes/vat-reports.js +8 -6
- package/server/routes/webhooks.js +19 -5
- package/server/services/journal-posting.js +165 -0
|
@@ -31,8 +31,9 @@ export function useBankImportPreview() {
|
|
|
31
31
|
export function useConfirmBankImport() {
|
|
32
32
|
const queryClient = useQueryClient();
|
|
33
33
|
return useMutation({
|
|
34
|
-
mutationFn: async (rows: BankImportPreviewRow[]) => {
|
|
35
|
-
const {
|
|
34
|
+
mutationFn: async (payload: { rows: BankImportPreviewRow[]; accountId?: string | null }) => {
|
|
35
|
+
const { rows, accountId } = payload;
|
|
36
|
+
const { data } = await api.post('/bank-transactions/import/confirm', { rows, accountId: accountId || undefined });
|
|
36
37
|
return data;
|
|
37
38
|
},
|
|
38
39
|
onSuccess: () => {
|
|
@@ -45,7 +46,7 @@ export function useConfirmBankImport() {
|
|
|
45
46
|
export function useUpdateBankTransaction() {
|
|
46
47
|
const queryClient = useQueryClient();
|
|
47
48
|
return useMutation({
|
|
48
|
-
mutationFn: async ({ id, ...payload }: { id: string; date?: string; type?: string; amount?: number; reference?: string; description?: string }) => {
|
|
49
|
+
mutationFn: async ({ id, ...payload }: { id: string; date?: string; type?: string; amount?: number; reference?: string; description?: string; accountId?: string | null }) => {
|
|
49
50
|
const { data } = await api.patch(`/bank-transactions/${encodeURIComponent(id)}`, payload);
|
|
50
51
|
return data;
|
|
51
52
|
},
|
|
@@ -21,6 +21,19 @@ export function useExpenseCategories() {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Expense accounts (chart of accounts 400-799) for expense dropdowns */
|
|
25
|
+
export function useExpenseAccounts() {
|
|
26
|
+
return useQuery({
|
|
27
|
+
queryKey: ['accounts', 'expense'],
|
|
28
|
+
queryFn: async () => {
|
|
29
|
+
const { data } = await api.get('/accounts?type=expense');
|
|
30
|
+
return data;
|
|
31
|
+
},
|
|
32
|
+
refetchOnMount: true,
|
|
33
|
+
staleTime: 0,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
export function useCreateExpense() {
|
|
25
38
|
const queryClient = useQueryClient();
|
|
26
39
|
return useMutation({
|
|
@@ -23,6 +23,21 @@ export function useSaveInvoiceConfig() {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function useUploadLogo() {
|
|
27
|
+
const queryClient = useQueryClient();
|
|
28
|
+
return useMutation({
|
|
29
|
+
mutationFn: async (file: File) => {
|
|
30
|
+
const formData = new FormData();
|
|
31
|
+
formData.append('file', file);
|
|
32
|
+
const { data } = await api.post<{ logoPath: string }>('/invoice-config/logo', formData, {
|
|
33
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
34
|
+
});
|
|
35
|
+
return data;
|
|
36
|
+
},
|
|
37
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['invoice-config'] }),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
// Integrations
|
|
27
42
|
export function useIntegrationsSettings() {
|
|
28
43
|
return useQuery({
|
|
@@ -136,3 +151,72 @@ export function useDeleteExpenseCategory() {
|
|
|
136
151
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['expense-categories'] }),
|
|
137
152
|
});
|
|
138
153
|
}
|
|
154
|
+
|
|
155
|
+
// Chart of accounts: account groups and accounts
|
|
156
|
+
export function useAccountGroups() {
|
|
157
|
+
return useQuery({
|
|
158
|
+
queryKey: ['account-groups'],
|
|
159
|
+
queryFn: async () => {
|
|
160
|
+
const { data } = await api.get('/account-groups');
|
|
161
|
+
return data;
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function useAccounts(params?: { groupId?: string; type?: string }) {
|
|
167
|
+
return useQuery({
|
|
168
|
+
queryKey: ['accounts', params?.groupId, params?.type],
|
|
169
|
+
queryFn: async () => {
|
|
170
|
+
const sp = new URLSearchParams();
|
|
171
|
+
if (params?.groupId) sp.set('group_id', params.groupId);
|
|
172
|
+
if (params?.type) sp.set('type', params.type);
|
|
173
|
+
const { data } = await api.get('/accounts' + (sp.toString() ? '?' + sp.toString() : ''));
|
|
174
|
+
return data;
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function useAccountsAll() {
|
|
180
|
+
return useQuery({
|
|
181
|
+
queryKey: ['accounts', 'all'],
|
|
182
|
+
queryFn: async () => {
|
|
183
|
+
const { data } = await api.get('/accounts/all');
|
|
184
|
+
return data;
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function useSaveAccount() {
|
|
190
|
+
const queryClient = useQueryClient();
|
|
191
|
+
return useMutation({
|
|
192
|
+
mutationFn: async (account: any) => {
|
|
193
|
+
if (account.id) {
|
|
194
|
+
const { data } = await api.put(`/accounts/${account.id}`, account);
|
|
195
|
+
return data;
|
|
196
|
+
} else {
|
|
197
|
+
const { data } = await api.post('/accounts', account);
|
|
198
|
+
return data;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
onSuccess: () => {
|
|
202
|
+
queryClient.invalidateQueries({ queryKey: ['accounts'] });
|
|
203
|
+
queryClient.invalidateQueries({ queryKey: ['account-groups'] });
|
|
204
|
+
queryClient.invalidateQueries({ queryKey: ['expense-categories'] });
|
|
205
|
+
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function useDeleteAccount() {
|
|
211
|
+
const queryClient = useQueryClient();
|
|
212
|
+
return useMutation({
|
|
213
|
+
mutationFn: async (id: string) => {
|
|
214
|
+
await api.delete(`/accounts/${id}`);
|
|
215
|
+
},
|
|
216
|
+
onSuccess: () => {
|
|
217
|
+
queryClient.invalidateQueries({ queryKey: ['accounts'] });
|
|
218
|
+
queryClient.invalidateQueries({ queryKey: ['account-groups'] });
|
|
219
|
+
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
package/frontend/src/index.css
CHANGED
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
--chart-6: oklch(0.72 0.19 165);
|
|
35
35
|
--chart-7: oklch(0.6 0.22 25);
|
|
36
36
|
--chart-8: oklch(0.45 0.2 265);
|
|
37
|
-
--chart-revenue:
|
|
38
|
-
--chart-expenses:
|
|
37
|
+
--chart-revenue: #42d4a6;
|
|
38
|
+
--chart-expenses: #f13f66;
|
|
39
39
|
/* Reconciliation match block */
|
|
40
40
|
--reconciliation-match-bg: #eff6ff;
|
|
41
41
|
--reconciliation-match-border: lch(88.74% 26.13 221.7);
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
--chart-6: oklch(0.72 0.19 165);
|
|
80
80
|
--chart-7: oklch(0.6 0.22 25);
|
|
81
81
|
--chart-8: oklch(0.45 0.2 265);
|
|
82
|
-
--chart-revenue:
|
|
83
|
-
--chart-expenses:
|
|
82
|
+
--chart-revenue: #10b981;
|
|
83
|
+
--chart-expenses: #f87171;
|
|
84
84
|
--reconciliation-match-bg: #1e3a5f;
|
|
85
85
|
--reconciliation-match-border: #2563eb;
|
|
86
86
|
--reconciliation-match-muted-bg: rgba(30, 58, 95, 0.4);
|
|
@@ -63,11 +63,12 @@ export default function ProtectedLayout() {
|
|
|
63
63
|
|
|
64
64
|
const sidebarContent = (
|
|
65
65
|
<>
|
|
66
|
-
<div className="p-4 flex items-center
|
|
66
|
+
<div className="p-4 flex items-center min-h-[7rem]">
|
|
67
67
|
{logoUrl ? (
|
|
68
|
-
<
|
|
68
|
+
<div className="rounded-lg bg-white p-2 w-full h-20 flex items-center justify-center">
|
|
69
|
+
<img src={logoUrl} alt="" className="max-w-full max-h-full w-auto h-auto object-contain" />
|
|
70
|
+
</div>
|
|
69
71
|
) : null}
|
|
70
|
-
<h1 className="text-xl font-bold text-white truncate">{companyName}</h1>
|
|
71
72
|
</div>
|
|
72
73
|
<nav className="flex-1 px-2 py-4 space-y-1">
|
|
73
74
|
{navLinks}
|
|
@@ -106,9 +107,10 @@ export default function ProtectedLayout() {
|
|
|
106
107
|
</SheetContent>
|
|
107
108
|
</Sheet>
|
|
108
109
|
{logoUrl ? (
|
|
109
|
-
<
|
|
110
|
+
<div className="rounded-lg bg-white p-2 h-10 w-24 flex items-center justify-center shrink-0">
|
|
111
|
+
<img src={logoUrl} alt="" className="max-w-full max-h-full w-auto h-auto object-contain" />
|
|
112
|
+
</div>
|
|
110
113
|
) : null}
|
|
111
|
-
<h1 className="text-lg font-bold truncate">{companyName}</h1>
|
|
112
114
|
</header>
|
|
113
115
|
|
|
114
116
|
{/* Desktop: sidebar */}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
2
|
import { Link } from 'react-router-dom';
|
|
3
3
|
import { useBankTransactions, useBankImportPreview, useConfirmBankImport, useUpdateBankTransaction, useDeleteBankTransaction, type BankImportPreviewRow } from '../hooks/useBank';
|
|
4
|
+
import { useAccountsAll } from '../hooks/useSettings';
|
|
4
5
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
|
5
6
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
6
7
|
import { Button } from '@/components/ui/button';
|
|
@@ -61,26 +62,41 @@ function SortableTh({
|
|
|
61
62
|
|
|
62
63
|
type ReviewRow = BankImportPreviewRow & { selected: boolean };
|
|
63
64
|
|
|
65
|
+
const ASSET_CODE_MIN = 100;
|
|
66
|
+
const ASSET_CODE_MAX = 199;
|
|
67
|
+
|
|
64
68
|
export default function Bank() {
|
|
65
69
|
const { data: transactions, isLoading } = useBankTransactions();
|
|
70
|
+
const { data: allAccounts } = useAccountsAll();
|
|
66
71
|
const previewMutation = useBankImportPreview();
|
|
67
72
|
const confirmImportMutation = useConfirmBankImport();
|
|
68
73
|
const updateMutation = useUpdateBankTransaction();
|
|
69
74
|
const deleteMutation = useDeleteBankTransaction();
|
|
70
75
|
|
|
76
|
+
const assetAccounts = (allAccounts ?? []).filter(
|
|
77
|
+
(a: { code: number }) => a.code >= ASSET_CODE_MIN && a.code <= ASSET_CODE_MAX
|
|
78
|
+
).sort((a: { code: number }, b: { code: number }) => a.code - b.code);
|
|
79
|
+
const defaultBankAccountId = assetAccounts.length > 0 ? assetAccounts[0].id : null;
|
|
80
|
+
|
|
71
81
|
const [searchTerm, setSearchTerm] = useState('');
|
|
72
82
|
const [filterType, setFilterType] = useState('all');
|
|
83
|
+
const [filterAccountId, setFilterAccountId] = useState<string>('all');
|
|
73
84
|
const [page, setPage] = useState(0);
|
|
74
85
|
const [editingTx, setEditingTx] = useState<any>(null);
|
|
75
86
|
const [deletingTx, setDeletingTx] = useState<any>(null);
|
|
76
87
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
77
|
-
const [editForm, setEditForm] = useState({ date: '', type: 'out' as 'in' | 'out', amount: '', reference: '', description: '' });
|
|
88
|
+
const [editForm, setEditForm] = useState({ date: '', type: 'out' as 'in' | 'out', amount: '', reference: '', description: '', accountId: '' as string });
|
|
78
89
|
const [importReviewOpen, setImportReviewOpen] = useState(false);
|
|
79
90
|
const [reviewRows, setReviewRows] = useState<ReviewRow[]>([]);
|
|
91
|
+
const [importAccountId, setImportAccountId] = useState<string | null>(null);
|
|
80
92
|
const [importError, setImportError] = useState<string | null>(null);
|
|
81
93
|
const [sortBy, setSortBy] = useState<SortKey>('date');
|
|
82
94
|
const [sortDir, setSortDir] = useState<SortDir>('desc');
|
|
83
95
|
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (defaultBankAccountId && importAccountId === null) setImportAccountId(defaultBankAccountId);
|
|
98
|
+
}, [defaultBankAccountId, importAccountId]);
|
|
99
|
+
|
|
84
100
|
const handleSort = (key: SortKey) => {
|
|
85
101
|
if (sortBy === key) {
|
|
86
102
|
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
@@ -98,6 +114,7 @@ export default function Bank() {
|
|
|
98
114
|
amount: tx.amount != null ? String(Math.abs(Number(tx.amount))) : '',
|
|
99
115
|
reference: tx.reference ?? '',
|
|
100
116
|
description: tx.description ?? '',
|
|
117
|
+
accountId: tx.accountId ?? '',
|
|
101
118
|
});
|
|
102
119
|
};
|
|
103
120
|
|
|
@@ -111,6 +128,7 @@ export default function Bank() {
|
|
|
111
128
|
amount,
|
|
112
129
|
reference: editForm.reference,
|
|
113
130
|
description: editForm.description,
|
|
131
|
+
accountId: editForm.accountId || undefined,
|
|
114
132
|
});
|
|
115
133
|
setEditingTx(null);
|
|
116
134
|
};
|
|
@@ -164,7 +182,7 @@ export default function Bank() {
|
|
|
164
182
|
if (toImport.length === 0) return;
|
|
165
183
|
setImportError(null);
|
|
166
184
|
try {
|
|
167
|
-
await confirmImportMutation.mutateAsync(toImport);
|
|
185
|
+
await confirmImportMutation.mutateAsync({ rows: toImport, accountId: importAccountId || defaultBankAccountId });
|
|
168
186
|
setImportReviewOpen(false);
|
|
169
187
|
setReviewRows([]);
|
|
170
188
|
} catch (err: any) {
|
|
@@ -178,12 +196,11 @@ export default function Bank() {
|
|
|
178
196
|
const amountStr = String(Math.abs(Number(tx.amount)) ?? '');
|
|
179
197
|
const term = searchTerm.toLowerCase().trim();
|
|
180
198
|
const matchesSearch = !term || desc.includes(term) || ref.includes(term) || amountStr.includes(term.replace(',', '.'));
|
|
181
|
-
|
|
199
|
+
if (filterAccountId !== 'all' && tx.accountId !== filterAccountId) return false;
|
|
182
200
|
if (filterType === 'unreconciled' && tx.reconciled) return false;
|
|
183
201
|
if (filterType === 'reconciled' && !tx.reconciled) return false;
|
|
184
202
|
if (filterType === 'in' && tx.type !== 'in') return false;
|
|
185
203
|
if (filterType === 'out' && tx.type !== 'out') return false;
|
|
186
|
-
|
|
187
204
|
return matchesSearch;
|
|
188
205
|
});
|
|
189
206
|
|
|
@@ -258,6 +275,22 @@ export default function Bank() {
|
|
|
258
275
|
<SelectItem value="out">Money Out</SelectItem>
|
|
259
276
|
</SelectContent>
|
|
260
277
|
</Select>
|
|
278
|
+
<Select value={filterAccountId} onValueChange={(val) => setFilterAccountId(val || 'all')}>
|
|
279
|
+
<SelectTrigger className="w-[180px]">
|
|
280
|
+
<span className="truncate">
|
|
281
|
+
{filterAccountId === 'all' ? 'All accounts' : (() => {
|
|
282
|
+
const sel = assetAccounts.find((a: { id: string }) => a.id === filterAccountId);
|
|
283
|
+
return sel ? `${sel.code} ${sel.name}` : 'Account...';
|
|
284
|
+
})()}
|
|
285
|
+
</span>
|
|
286
|
+
</SelectTrigger>
|
|
287
|
+
<SelectContent>
|
|
288
|
+
<SelectItem value="all">All accounts</SelectItem>
|
|
289
|
+
{assetAccounts.map((a: { id: string; code: number; name: string }) => (
|
|
290
|
+
<SelectItem key={a.id} value={a.id}>{a.code} {a.name}</SelectItem>
|
|
291
|
+
))}
|
|
292
|
+
</SelectContent>
|
|
293
|
+
</Select>
|
|
261
294
|
</div>
|
|
262
295
|
</CardHeader>
|
|
263
296
|
<CardContent>
|
|
@@ -275,6 +308,7 @@ export default function Bank() {
|
|
|
275
308
|
<SortableTh label="Date" sortKey="date" currentSortKey={sortBy} sortDir={sortDir} onSort={handleSort} className="w-24" />
|
|
276
309
|
<SortableTh label="Reference" sortKey="reference" currentSortKey={sortBy} sortDir={sortDir} onSort={handleSort} className="w-32" />
|
|
277
310
|
<SortableTh label="Description" sortKey="description" currentSortKey={sortBy} sortDir={sortDir} onSort={handleSort} className="w-32" />
|
|
311
|
+
<TableHead className="w-28">Account</TableHead>
|
|
278
312
|
<SortableTh label="Amount" sortKey="amount" currentSortKey={sortBy} sortDir={sortDir} onSort={handleSort} className="w-24 text-right" />
|
|
279
313
|
<SortableTh label="Status" sortKey="status" currentSortKey={sortBy} sortDir={sortDir} onSort={handleSort} className="w-24" />
|
|
280
314
|
<TableHead className="w-[200px] whitespace-nowrap">Actions</TableHead>
|
|
@@ -283,7 +317,7 @@ export default function Bank() {
|
|
|
283
317
|
<TableBody>
|
|
284
318
|
{totalCount === 0 ? (
|
|
285
319
|
<TableRow>
|
|
286
|
-
<TableCell colSpan={
|
|
320
|
+
<TableCell colSpan={7} className="text-center py-4 text-muted-foreground">
|
|
287
321
|
No transactions found.
|
|
288
322
|
</TableCell>
|
|
289
323
|
</TableRow>
|
|
@@ -297,6 +331,10 @@ export default function Bank() {
|
|
|
297
331
|
<TableCell className="w-32 truncate" title={tx.description || undefined}>
|
|
298
332
|
{truncate(tx.description)}
|
|
299
333
|
</TableCell>
|
|
334
|
+
<TableCell className="w-28 truncate" title={tx.accountName || (tx.accountCode != null ? String(tx.accountCode) : undefined)}>
|
|
335
|
+
{tx.accountCode != null ? `${tx.accountCode}` : '\u2014'}
|
|
336
|
+
{tx.accountName ? ` ${truncate(tx.accountName, 12)}` : ''}
|
|
337
|
+
</TableCell>
|
|
300
338
|
<TableCell className={`w-24 text-right font-medium whitespace-nowrap ${tx.type === 'in' ? 'text-emerald-600' : 'text-slate-900'}`}>
|
|
301
339
|
{tx.type === 'in' ? '+' : '-'}€{Math.abs(Number(tx.amount)).toFixed(2)}
|
|
302
340
|
</TableCell>
|
|
@@ -416,6 +454,26 @@ export default function Bank() {
|
|
|
416
454
|
onChange={(e) => setEditForm((f) => ({ ...f, description: e.target.value }))}
|
|
417
455
|
/>
|
|
418
456
|
</div>
|
|
457
|
+
{assetAccounts.length > 0 && (
|
|
458
|
+
<div className="grid grid-cols-4 items-center gap-2">
|
|
459
|
+
<Label>Account</Label>
|
|
460
|
+
<Select value={editForm.accountId || defaultBankAccountId || ''} onValueChange={(v) => setEditForm((f) => ({ ...f, accountId: v }))}>
|
|
461
|
+
<SelectTrigger className="col-span-3">
|
|
462
|
+
<span className="truncate">
|
|
463
|
+
{(() => {
|
|
464
|
+
const sel = assetAccounts.find((a: { id: string }) => a.id === (editForm.accountId || defaultBankAccountId));
|
|
465
|
+
return sel ? `${sel.code} ${sel.name}` : 'Select account...';
|
|
466
|
+
})()}
|
|
467
|
+
</span>
|
|
468
|
+
</SelectTrigger>
|
|
469
|
+
<SelectContent>
|
|
470
|
+
{assetAccounts.map((a: { id: string; code: number; name: string }) => (
|
|
471
|
+
<SelectItem key={a.id} value={a.id}>{a.code} {a.name}</SelectItem>
|
|
472
|
+
))}
|
|
473
|
+
</SelectContent>
|
|
474
|
+
</Select>
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
419
477
|
</div>
|
|
420
478
|
<DialogFooter showCloseButton>
|
|
421
479
|
<Button onClick={submitEdit} disabled={updateMutation.isPending || !editForm.date || !editForm.amount}>
|
|
@@ -456,9 +514,29 @@ export default function Bank() {
|
|
|
456
514
|
<DialogHeader>
|
|
457
515
|
<DialogTitle>Review import</DialogTitle>
|
|
458
516
|
<DialogDescription>
|
|
459
|
-
Uncheck rows to exclude from import.
|
|
517
|
+
Uncheck rows to exclude from import. Choose the account to import into, then click Import selected.
|
|
460
518
|
</DialogDescription>
|
|
461
519
|
</DialogHeader>
|
|
520
|
+
{assetAccounts.length > 0 && (
|
|
521
|
+
<div className="flex items-center gap-2 py-2 border-b">
|
|
522
|
+
<Label className="shrink-0">Import to account</Label>
|
|
523
|
+
<Select value={importAccountId || defaultBankAccountId || ''} onValueChange={(v) => setImportAccountId(v)}>
|
|
524
|
+
<SelectTrigger className="w-[280px]">
|
|
525
|
+
<span className="truncate">
|
|
526
|
+
{(() => {
|
|
527
|
+
const sel = assetAccounts.find((a: { id: string }) => a.id === (importAccountId || defaultBankAccountId));
|
|
528
|
+
return sel ? `${sel.code} ${sel.name}` : 'Select account...';
|
|
529
|
+
})()}
|
|
530
|
+
</span>
|
|
531
|
+
</SelectTrigger>
|
|
532
|
+
<SelectContent>
|
|
533
|
+
{assetAccounts.map((a: { id: string; code: number; name: string }) => (
|
|
534
|
+
<SelectItem key={a.id} value={a.id}>{a.code} {a.name}</SelectItem>
|
|
535
|
+
))}
|
|
536
|
+
</SelectContent>
|
|
537
|
+
</Select>
|
|
538
|
+
</div>
|
|
539
|
+
)}
|
|
462
540
|
<div className="flex items-center gap-2 py-2 border-b">
|
|
463
541
|
<Button variant="outline" size="sm" onClick={() => setReviewRows((prev) => prev.map((r) => ({ ...r, selected: true })))}>
|
|
464
542
|
Select all
|
|
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|
|
2
2
|
import { useCustomers, useCreateCustomer, useUpdateCustomer, useDeleteCustomer, type Customer } from '../hooks/useContacts';
|
|
3
3
|
import { useSuppliers, useCreateSupplier, useUpdateSupplier, useDeleteSupplier, type Supplier } from '../hooks/useContacts';
|
|
4
4
|
import { usePayees, useCreatePayee, useUpdatePayee, useDeletePayee, type Payee } from '../hooks/useContacts';
|
|
5
|
-
import { useExpenseCategories as useExpenseCategoriesHook } from '../hooks/useExpenses';
|
|
5
|
+
import { useExpenseCategories as useExpenseCategoriesHook, useExpenseAccounts } from '../hooks/useExpenses';
|
|
6
6
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
|
7
7
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
8
8
|
import { Button } from '@/components/ui/button';
|
|
@@ -32,6 +32,7 @@ export default function Contacts() {
|
|
|
32
32
|
const [editing, setEditing] = useState<Customer | Supplier | Payee | null>(null);
|
|
33
33
|
const [form, setForm] = useState(emptyContact);
|
|
34
34
|
const [supplierCategoryId, setSupplierCategoryId] = useState<string | null>(null);
|
|
35
|
+
const [supplierAccountId, setSupplierAccountId] = useState<string | null>(null);
|
|
35
36
|
|
|
36
37
|
const { data: customers, isLoading: loadingCustomers } = useCustomers();
|
|
37
38
|
const createCustomer = useCreateCustomer();
|
|
@@ -40,6 +41,8 @@ export default function Contacts() {
|
|
|
40
41
|
|
|
41
42
|
const { data: suppliers, isLoading: loadingSuppliers } = useSuppliers();
|
|
42
43
|
const { data: categories } = useExpenseCategoriesHook();
|
|
44
|
+
const { data: expenseAccounts } = useExpenseAccounts();
|
|
45
|
+
const useAccountForSupplier = expenseAccounts && expenseAccounts.length > 0;
|
|
43
46
|
const createSupplier = useCreateSupplier();
|
|
44
47
|
const updateSupplier = useUpdateSupplier();
|
|
45
48
|
const deleteSupplier = useDeleteSupplier();
|
|
@@ -62,9 +65,11 @@ export default function Contacts() {
|
|
|
62
65
|
notes: editing.notes ?? '',
|
|
63
66
|
});
|
|
64
67
|
setSupplierCategoryId((editing as Supplier).categoryId ?? null);
|
|
68
|
+
setSupplierAccountId((editing as Supplier).accountId ?? (editing as Supplier).categoryId ?? null);
|
|
65
69
|
} else {
|
|
66
70
|
setForm(emptyContact);
|
|
67
71
|
setSupplierCategoryId(null);
|
|
72
|
+
setSupplierAccountId(null);
|
|
68
73
|
}
|
|
69
74
|
}, [editing]);
|
|
70
75
|
|
|
@@ -72,6 +77,7 @@ export default function Contacts() {
|
|
|
72
77
|
setEditing(null);
|
|
73
78
|
setForm(emptyContact);
|
|
74
79
|
setSupplierCategoryId(null);
|
|
80
|
+
setSupplierAccountId(null);
|
|
75
81
|
setDialogOpen(true);
|
|
76
82
|
};
|
|
77
83
|
|
|
@@ -95,7 +101,9 @@ export default function Contacts() {
|
|
|
95
101
|
await createCustomer.mutateAsync(form);
|
|
96
102
|
}
|
|
97
103
|
} else if (activeTab === 'supplier') {
|
|
98
|
-
const body =
|
|
104
|
+
const body = useAccountForSupplier
|
|
105
|
+
? { ...form, accountId: supplierAccountId || undefined }
|
|
106
|
+
: { ...form, categoryId: supplierCategoryId || undefined };
|
|
99
107
|
if (editing?.id) {
|
|
100
108
|
await updateSupplier.mutateAsync({ id: editing.id, ...body });
|
|
101
109
|
} else {
|
|
@@ -228,7 +236,7 @@ export default function Contacts() {
|
|
|
228
236
|
<TableRow key={row.id}>
|
|
229
237
|
<TableCell className="font-medium">{row.name}</TableCell>
|
|
230
238
|
<TableCell>{row.accountNumber || '-'}</TableCell>
|
|
231
|
-
<TableCell>{(row as Supplier).categoryName || '-'}</TableCell>
|
|
239
|
+
<TableCell>{(row as Supplier).accountName || (row as Supplier).categoryName || '-'}</TableCell>
|
|
232
240
|
<TableCell>{row.email || '-'}</TableCell>
|
|
233
241
|
<TableCell>{row.phone || '-'}</TableCell>
|
|
234
242
|
<TableCell>
|
|
@@ -348,20 +356,34 @@ export default function Contacts() {
|
|
|
348
356
|
</div>
|
|
349
357
|
{activeTab === 'supplier' && (
|
|
350
358
|
<div className="space-y-2">
|
|
351
|
-
<label className="text-sm font-medium">Default category</label>
|
|
352
|
-
|
|
353
|
-
<
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
{
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
</
|
|
364
|
-
|
|
359
|
+
<label className="text-sm font-medium">{useAccountForSupplier ? 'Default account' : 'Default category'}</label>
|
|
360
|
+
{useAccountForSupplier ? (
|
|
361
|
+
<Select value={supplierAccountId ?? ''} onValueChange={(v) => setSupplierAccountId(v || null)}>
|
|
362
|
+
<SelectTrigger>
|
|
363
|
+
<SelectValue placeholder="Optional" />
|
|
364
|
+
</SelectTrigger>
|
|
365
|
+
<SelectContent>
|
|
366
|
+
<SelectItem value="">None</SelectItem>
|
|
367
|
+
{(expenseAccounts ?? []).sort((a: any, b: any) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0) || (a.code - b.code)).map((acc: any) => (
|
|
368
|
+
<SelectItem key={acc.id} value={acc.id}>{acc.code} - {acc.name}</SelectItem>
|
|
369
|
+
))}
|
|
370
|
+
</SelectContent>
|
|
371
|
+
</Select>
|
|
372
|
+
) : (
|
|
373
|
+
<Select value={supplierCategoryId ?? ''} onValueChange={(v) => setSupplierCategoryId(v || null)}>
|
|
374
|
+
<SelectTrigger>
|
|
375
|
+
<SelectValue placeholder="Optional" />
|
|
376
|
+
</SelectTrigger>
|
|
377
|
+
<SelectContent>
|
|
378
|
+
<SelectItem value="">None</SelectItem>
|
|
379
|
+
{(categories ?? []).sort((a: { sortOrder?: number }, b: { sortOrder?: number }) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)).map((cat: { id: string; name: string }) => (
|
|
380
|
+
<SelectItem key={cat.id} value={cat.id}>
|
|
381
|
+
{cat.name}
|
|
382
|
+
</SelectItem>
|
|
383
|
+
))}
|
|
384
|
+
</SelectContent>
|
|
385
|
+
</Select>
|
|
386
|
+
)}
|
|
365
387
|
</div>
|
|
366
388
|
)}
|
|
367
389
|
<div className="space-y-2">
|