feat: add General Ledger integration and financial reporting features
- Implemented General Ledger client component for managing GL integration. - Created a dedicated page for General Ledger configuration. - Developed Finance Report client component for generating various financial reports. - Added report blocks for structured display of financial data. - Introduced API endpoints for fetching GL accounts and financial reports. - Updated navigation to include links for GL integration and financial reports. - Enhanced permissions to manage GL and view financial reports. - Updated appsettings to include General Ledger API configuration.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
export type FinanceReportType =
|
||||
| 'TrialBalance'
|
||||
| 'GeneralLedger'
|
||||
| 'ProfitAndLoss'
|
||||
| 'BalanceSheet'
|
||||
| 'CashFlow'
|
||||
| 'BudgetVsActual'
|
||||
| 'TaxSummary'
|
||||
|
||||
export interface FinanceReportParams {
|
||||
asOfDate?: string
|
||||
periodStart?: string
|
||||
periodEnd?: string
|
||||
accountCode?: string
|
||||
budgetId?: number
|
||||
}
|
||||
|
||||
export type FileFormat = 'Pdf' | 'Csv'
|
||||
|
||||
const cleanParams = (params: FinanceReportParams) =>
|
||||
Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined && v !== ''))
|
||||
|
||||
// Reports come from the General Ledger service through the Distribution backend (/reports/finance/{type}).
|
||||
export interface GlAccountOption {
|
||||
accountCode: string
|
||||
accountName: string
|
||||
}
|
||||
|
||||
export const financeReports = {
|
||||
/** Active GL accounts for the account dropdown. */
|
||||
accounts: () => api.get<GlAccountOption[]>('/reports/finance/accounts').then(r => r.data),
|
||||
|
||||
get: (type: FinanceReportType, params: FinanceReportParams) =>
|
||||
api.get<unknown>(`/reports/finance/${type}`, { params: cleanParams(params) }).then(r => r.data),
|
||||
|
||||
/** Downloads the GL-rendered PDF or CSV and hands it to the browser. */
|
||||
async download(type: FinanceReportType, params: FinanceReportParams, format: FileFormat) {
|
||||
const res = await api.get<Blob>(`/reports/finance/${type}`, {
|
||||
params: { ...cleanParams(params), outputFormat: format },
|
||||
responseType: 'blob',
|
||||
})
|
||||
const disposition = String(res.headers['content-disposition'] ?? '')
|
||||
const name = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition)?.[1] ?? `${type}.${format.toLowerCase()}`
|
||||
const url = URL.createObjectURL(res.data)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = decodeURIComponent(name)
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
},
|
||||
}
|
||||
|
||||
export interface GlStatus {
|
||||
glReachable: boolean
|
||||
setupDone: boolean
|
||||
openingBalancePosted: boolean
|
||||
closedYears: number[]
|
||||
mapping: Record<string, string | null>
|
||||
posted: number
|
||||
failed: number
|
||||
}
|
||||
|
||||
export interface GlPostingRow {
|
||||
id: string
|
||||
event: string
|
||||
sourceId: string
|
||||
reference: string
|
||||
status: 'Pending' | 'Posted' | 'Failed'
|
||||
glJournalNo: string | null
|
||||
error: string | null
|
||||
attempts: number
|
||||
lastAttemptAt: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface OpeningBalanceLine {
|
||||
/** A key such as 'Inventory' or 'ShareCapital', or 'BankAccount' together with bankAccountId. */
|
||||
account: string
|
||||
bankAccountId?: string
|
||||
/** The account's normal balance (>= 0). Debit/credit side is decided by the backend. */
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface YearEndPreview {
|
||||
year: number
|
||||
startDate: string
|
||||
endDate: string
|
||||
totalIncome: number
|
||||
totalExpenses: number
|
||||
netProfit: number
|
||||
closable: boolean
|
||||
reason: string | null
|
||||
alreadyClosed: boolean
|
||||
}
|
||||
|
||||
export const generalLedger = {
|
||||
yearEndPreview: (year: number) =>
|
||||
api.get<YearEndPreview>('/gl/year-end/preview', { params: { year } }).then(r => r.data),
|
||||
closeYear: (year: number) =>
|
||||
api.post<{ year: number; glJournalNo: string | null; netProfit: number; periodsLocked: number }>('/gl/year-end/close', { year }).then(r => r.data),
|
||||
postOpeningBalances: (date: string, narration: string, lines: OpeningBalanceLine[]) =>
|
||||
api.post<{ status: string; glJournalNo: string | null }>('/gl/opening-balances', { date, narration, lines }).then(r => r.data),
|
||||
status: () => api.get<GlStatus>('/gl/status').then(r => r.data),
|
||||
setup: (year?: number) => api.post<{ done: boolean; actions: string[] }>('/gl/setup', { year }).then(r => r.data),
|
||||
postings: (status?: GlPostingRow['status']) =>
|
||||
api.get<GlPostingRow[]>('/gl/postings', { params: { status } }).then(r => r.data),
|
||||
retry: () => api.post<{ posted: number }>('/gl/postings/retry').then(r => r.data),
|
||||
}
|
||||
Reference in New Issue
Block a user