1
0
This commit is contained in:
Ahmed Bouhuolia
2026-05-17 19:50:00 +02:00
parent 98713f8bf5
commit 00feae58a7
62 changed files with 4374 additions and 23 deletions
+3 -3
View File
@@ -3,9 +3,9 @@
"version": "0.10.2",
"private": true,
"dependencies": {
"@bigcapital/email-components": "*",
"@bigcapital/pdf-templates": "*",
"@bigcapital/utils": "*",
"@bigcapital/email-components": "workspace:*",
"@bigcapital/pdf-templates": "workspace:*",
"@bigcapital/utils": "workspace:*",
"@blueprintjs-formik/core": "^0.3.7",
"@blueprintjs-formik/datetime": "^0.4.0",
"@blueprintjs-formik/select": "^0.4.5",
@@ -44,6 +44,7 @@ export function FinancialSheet({
() => getBasisLabel(basis),
[getBasisLabel, basis],
);
const hasHead = companyName || sheetType || dateText;
return (
<FinancialSheetRoot
@@ -51,10 +52,13 @@ export function FinancialSheet({
fullWidth={fullWidth}
className={className}
>
{companyName && <FinancialSheetTitle>{companyName}</FinancialSheetTitle>}
{sheetType && <FinancialSheetType>{sheetType}</FinancialSheetType>}
{dateText && <FinancialSheetDate>{dateText}</FinancialSheetDate>}
{hasHead && (
<div>
{companyName && <FinancialSheetTitle>{companyName}</FinancialSheetTitle>}
{sheetType && <FinancialSheetType>{sheetType}</FinancialSheetType>}
{dateText && <FinancialSheetDate>{dateText}</FinancialSheetDate>}
</div>
)}
<FinancialSheetTable>{children}</FinancialSheetTable>
<FinancialSheetAccountingBasis>
@@ -12,6 +12,7 @@ export const FinancialSheetRoot = styled.div`
min-height: 400px;
display: flex;
flex-direction: column;
gap: 24px;
${(props) =>
props.fullWidth &&
@@ -73,9 +74,7 @@ export const FinancialSheetFooter = styled.div`
padding-left: 10px;
}
`;
export const FinancialSheetTable = styled.div`
margin-top: 24px;
`;
export const FinancialSheetTable = styled.div``;
export const FinancialSheetFooterBasis = styled.span``;
export const FinancialSheetFooterCurrentTime = styled.span``;
@@ -23,6 +23,7 @@ export const AbilitySubject = {
Project: 'Project',
TaxRate: 'TaxRate',
BankRule: 'BankRule',
AuditLog: 'AuditLog',
};
export const ItemAction = {
@@ -202,3 +203,7 @@ export const BankRuleAction = {
Edit: 'Edit',
Delete: 'Delete',
};
export const AuditLogAction = {
View: 'View',
};
@@ -1,7 +1,7 @@
// @ts-nocheck
import React from 'react';
import { FormattedMessage as T } from '@/components';
import { ReportsAction, AbilitySubject } from '@/constants/abilityOption';
import { ReportsAction, AbilitySubject, AuditLogAction } from '@/constants/abilityOption';
export const financialReportMenus = [
{
@@ -194,4 +194,16 @@ export const financialReportMenus = [
},
],
},
{
sectionTitle: <T id={'system_reports'} />,
reports: [
{
title: <T id={'audit_log_report'} />,
desc: <T id={'audit_log_report_desc'} />,
link: '/financial-reports/audit-log',
subject: AbilitySubject.AuditLog,
ability: AuditLogAction.View,
},
],
},
];
@@ -0,0 +1,47 @@
// @ts-nocheck
import React from 'react';
import { Button, Classes, NavbarGroup, NavbarDivider } from '@blueprintjs/core';
import classNames from 'classnames';
import { DashboardActionsBar, Icon } from '@/components';
import { useAuditLogContext } from './AuditLogProvider';
/**
* Audit Log Actions Bar
*/
function AuditLogActionsBar({
isFilterDrawerOpen,
toggleFilterDrawer,
}) {
const { sheetRefresh } = useAuditLogContext();
const handleCustomizeClick = () => {
toggleFilterDrawer();
};
const handleRecalcReport = () => {
sheetRefresh();
};
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL)}
text={"Reload"}
onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />}
/>
<NavbarDivider />
<Button
className={classNames(Classes.MINIMAL)}
icon={<Icon icon="cog-16" iconSize={16} />}
text={"Filter"}
onClick={handleCustomizeClick}
active={isFilterDrawerOpen}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default AuditLogActionsBar;
@@ -0,0 +1,27 @@
// @ts-nocheck
import React from 'react';
import { Spinner } from '@blueprintjs/core';
import { FinancialReportBody } from '../FinancialReportPage';
import { useAuditLogContext } from './AuditLogProvider';
import AuditLogTable from './AuditLogTable';
/**
* Audit Log Body
*/
function AuditLogBody() {
const { isLoading } = useAuditLogContext();
return (
<FinancialReportBody>
{isLoading ? (
<div style={{ padding: 20 }}>
<Spinner size={24} />
</div>
) : (
<AuditLogTable />
)}
</FinancialReportBody>
);
}
export { AuditLogBody };
@@ -0,0 +1,218 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import moment from 'moment';
import { Button, Tabs, Tab, DrawerSize, Position } from '@blueprintjs/core';
import styled from 'styled-components';
import { Formik, Form } from 'formik';
import {
FormattedMessage as T,
FFormGroup,
FDateInput,
} from '@/components';
import { FMultiSelect } from '@/components/Forms';
import { useAuditLogFilterOptionsQuery } from '@/hooks/query';
import { saveInvoke, transformToForm } from '@/utils';
import FinancialStatementHeader from '../FinancialStatementHeader';
import { getDefaultAuditLogQuery, getAuditLogQuerySchema } from './common';
function normalizeStringListField(value) {
return Array.isArray(value) ? value : value ? [value] : [];
}
const auditLogSelectItemPredicate = (query, item) => {
const q = (query || '').toLowerCase();
const name = (item?.name ?? '').toLowerCase();
return name.includes(q);
};
const AuditLogDrawerHeader = styled(FinancialStatementHeader)`
.bp4-drawer {
max-height: 350px;
}
`;
/**
* Audit Log Header - Filter drawer
*/
function AuditLogHeader({ onSubmitFilter, pageFilter, isFilterDrawerOpen, toggleFilterDrawer }) {
const { data: filterOptions, isLoading: isFilterOptionsLoading } =
useAuditLogFilterOptionsQuery({
enabled: isFilterDrawerOpen,
});
const subjectSelectItems = useMemo(() => {
const byValue = new Map();
for (const s of filterOptions.subjects ?? []) {
byValue.set(s.key, { value: s.key, name: s.label });
}
for (const s of normalizeStringListField(pageFilter.subject)) {
if (s && !byValue.has(s)) {
byValue.set(s, { value: s, name: s });
}
}
return Array.from(byValue.values()).sort((a, b) =>
a.name.localeCompare(b.name),
);
}, [filterOptions.subjects, pageFilter.subject]);
const actionSelectItems = useMemo(() => {
const byValue = new Map();
for (const a of filterOptions.actions ?? []) {
byValue.set(a.key, { value: a.key, name: a.label });
}
for (const act of normalizeStringListField(pageFilter.action)) {
if (act && !byValue.has(act)) {
byValue.set(act, { value: act, name: act });
}
}
return Array.from(byValue.values()).sort((a, b) =>
a.name.localeCompare(b.name),
);
}, [filterOptions.actions, pageFilter.action]);
const defaultValues = getDefaultAuditLogQuery();
const initialValues = transformToForm(
{
...defaultValues,
...pageFilter,
fromDate: pageFilter.fromDate ? moment(pageFilter.fromDate).toDate() : '',
toDate: pageFilter.toDate ? moment(pageFilter.toDate).toDate() : '',
},
defaultValues
);
const validationSchema = getAuditLogQuerySchema();
const handleSubmit = (values, { setSubmitting }) => {
const parsedFilter = {
...values,
subject: normalizeStringListField(values.subject),
action: normalizeStringListField(values.action),
fromDate: values.fromDate ? moment(values.fromDate).format('YYYY-MM-DD') : '',
toDate: values.toDate ? moment(values.toDate).format('YYYY-MM-DD') : '',
};
saveInvoke(onSubmitFilter, parsedFilter);
toggleFilterDrawer(false);
setSubmitting(false);
};
const handleCancelClick = () => {
toggleFilterDrawer(false);
};
const handleDrawerClose = () => {
toggleFilterDrawer(false);
};
return (
<AuditLogDrawerHeader
isOpen={isFilterDrawerOpen}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={
<div style={{ maxWidth: '400px' }}>
<FFormGroup
name="subject"
label={intl.get('audit_log.filter_subject')}
fastField
>
<FMultiSelect
name="subject"
items={subjectSelectItems}
valueAccessor="value"
textAccessor="name"
tagAccessor="name"
itemPredicate={auditLogSelectItemPredicate}
placeholder={intl.get('all')}
popoverProps={{ minimal: true }}
disabled={isFilterOptionsLoading}
fill
resetOnSelect
fastField
/>
</FFormGroup>
<FFormGroup
name="action"
label={intl.get('audit_log.filter_action')}
fastField
>
<FMultiSelect
name="action"
items={actionSelectItems}
valueAccessor="value"
textAccessor="name"
tagAccessor="name"
itemPredicate={auditLogSelectItemPredicate}
placeholder={intl.get('all')}
popoverProps={{ minimal: true }}
disabled={isFilterOptionsLoading}
fill
resetOnSelect
fastField
/>
</FFormGroup>
<FFormGroup
name="fromDate"
label={intl.get('audit_log.filter_from')}
fastField
>
<FDateInput
name="fromDate"
popoverProps={{ position: Position.BOTTOM, minimal: true }}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{ fill: true }}
fastField
/>
</FFormGroup>
<FFormGroup
name="toDate"
label={intl.get('audit_log.filter_to')}
fill
fastField
>
<FDateInput
name="toDate"
type="date"
popoverProps={{ position: Position.BOTTOM, minimal: true }}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{ fill: true }}
fastField
/>
</FFormGroup>
</div>
}
/>
</Tabs>
<div className="financial-header-drawer__footer">
<Button className={'mr1'} intent="primary" type="submit">
<T id={'calculate_report'} />
</Button>
<Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</AuditLogDrawerHeader>
);
}
export default AuditLogHeader;
@@ -0,0 +1,82 @@
// @ts-nocheck
import React, { createContext, useCallback, useContext, useMemo } from 'react';
import { flatten, map } from 'lodash';
import { useAuditLogsInfinityQuery } from '@/hooks/query';
import { IntersectionObserver } from '@/components';
function flattenInfinityPagesData(data) {
return flatten(map(data.pages, (page) => page.data));
}
// Context for Audit Log
const AuditLogContext = React.createContext();
const useAuditLogContext = () => useContext(AuditLogContext);
/**
* Audit Log Provider
*/
function toHttpStringList(value) {
if (value == null || value === '') return undefined;
if (Array.isArray(value)) return value.length ? value : undefined;
return [value];
}
function AuditLogProvider({ query, children }) {
const httpQuery = useMemo(() => {
return {
pageSize: 20,
subject: toHttpStringList(query.subject),
action: toHttpStringList(query.action),
from: query.fromDate || undefined,
to: query.toDate || undefined,
};
}, [query]);
const {
data: auditLogsPages,
isLoading,
isFetching,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
refetch,
} = useAuditLogsInfinityQuery(httpQuery);
const auditLogs = useMemo(
() =>
auditLogsPages
? flattenInfinityPagesData(auditLogsPages)
: [],
[auditLogsPages],
);
const handleObserverInteract = useCallback(() => {
if (!isFetching && hasNextPage) {
fetchNextPage();
}
}, [isFetching, hasNextPage, fetchNextPage]);
const provider = {
auditLogs,
isLoading,
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
handleObserverInteract,
sheetRefresh: refetch,
httpQuery,
};
return (
<AuditLogContext.Provider value={provider}>
{children}
<IntersectionObserver
onIntersect={handleObserverInteract}
/>
</AuditLogContext.Provider>
);
}
export { AuditLogProvider, useAuditLogContext };
@@ -0,0 +1,89 @@
// @ts-nocheck
import React, { useCallback, useEffect, useState } from 'react';
import intl from 'react-intl-universal';
import { NonIdealState } from '@blueprintjs/core';
import {
Card,
Can,
DashboardPageContent,
FinancialStatement,
} from '@/components';
import { AbilitySubject, AuditLogAction } from '@/constants/abilityOption';
import { AuditLogProvider } from './AuditLogProvider';
import AuditLogHeader from './AuditLogHeader';
import AuditLogActionsBar from './AuditLogActionsBar';
import { AuditLogLoadingBar } from './components';
import { AuditLogBody } from './AuditLogBody';
import { useAuditLogQuery } from './common';
/**
* Audit Log Report Content
*/
function AuditLogReportContent() {
const { query, setLocationQuery } = useAuditLogQuery();
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
const handleFilterSubmit = useCallback(
(filter) => {
setLocationQuery(filter);
},
[setLocationQuery]
);
const toggleFilterDrawer = useCallback((toggle) => {
setIsFilterDrawerOpen((prev) =>
typeof toggle !== 'undefined' ? toggle : !prev
);
}, []);
// Hide filter drawer on unmount
useEffect(() => {
return () => setIsFilterDrawerOpen(false);
}, []);
return (
<AuditLogProvider query={query}>
<AuditLogActionsBar
isFilterDrawerOpen={isFilterDrawerOpen}
toggleFilterDrawer={toggleFilterDrawer}
/>
<DashboardPageContent>
<FinancialStatement>
<AuditLogHeader
pageFilter={query}
onSubmitFilter={handleFilterSubmit}
isFilterDrawerOpen={isFilterDrawerOpen}
toggleFilterDrawer={toggleFilterDrawer}
/>
<AuditLogLoadingBar />
<AuditLogBody />
</FinancialStatement>
</DashboardPageContent>
</AuditLogProvider>
);
}
/**
* Audit Log Report page (in Financial Reports section).
*/
function AuditLogReport() {
return (
<>
<Can I={AuditLogAction.View} a={AbilitySubject.AuditLog}>
<AuditLogReportContent />
</Can>
<Can not I={AuditLogAction.View} a={AbilitySubject.AuditLog}>
<DashboardPageContent>
<Card style={{ padding: 20 }}>
<NonIdealState title={intl.get('audit_log.no_access')} />
</Card>
</DashboardPageContent>
</Can>
</>
);
}
export default AuditLogReport;
@@ -0,0 +1,129 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import { Spinner } from '@blueprintjs/core';
import styled from 'styled-components';
import {
FinancialSheet,
ReportDataTable,
TableFastCell,
TableVirtualizedListRows,
IntersectionObserver,
} from '@/components';
import { TableStyle } from '@/constants';
import { useAuditLogContext } from './AuditLogProvider';
// Dynamic columns for audit log
const useAuditLogTableColumns = () => {
return useMemo(
() => [
{
Header: intl.get('audit_log.col_time'),
accessor: 'created_at_formatted',
width: 180,
textOverview: true,
},
{
Header: intl.get('audit_log.col_user'),
accessor: 'user_name',
width: 150,
textOverview: true,
},
{
Header: intl.get('audit_log.col_action'),
accessor: 'action',
width: 100,
textOverview: true,
},
{
Header: intl.get('audit_log.col_subject'),
accessor: 'subject',
width: 120,
textOverview: true,
},
{
Header: intl.get('audit_log.col_summary'),
accessor: 'summary',
width: 350,
textOverview: true,
Cell: ({ value }) => (
<div
style={{
maxWidth: 330,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={value || ''}
>
{value || ''}
</div>
),
},
{
Header: intl.get('audit_log.col_ip'),
accessor: 'ip',
width: 120,
textOverview: true,
Cell: ({ value }) => value || '—',
},
],
[]
);
};
const AuditLogDataTable = styled(ReportDataTable)`
--color-table-text-color: #252a31;
--color-table-border-color: #ececec;
.bp4-dark & {
--color-table-text-color: var(--color-light-gray1);
--color-table-border-color: var(--color-dark-gray4);
}
.tbody {
.tr .td {
padding-top: 0.2rem;
padding-bottom: 0.2rem;
}
.tr:not(.no-results) .td:not(:first-of-type) {
border-left: 1px solid var(--color-table-border-color);
}
.tr:last-child .td {
border-bottom: 1px solid var(--color-table-border-color);
}
}
`;
/**
* Audit Log Table
*/
function AuditLogTable() {
const { auditLogs, isLoading, isFetchingNextPage, handleObserverInteract } = useAuditLogContext();
const columns = useAuditLogTableColumns();
return (
<FinancialSheet
loading={isLoading}
fullWidth={true}
currentDate={false}
>
<AuditLogDataTable
noResults={intl.get('audit_log.empty')}
columns={columns}
data={auditLogs}
virtualizedRows={true}
fixedItemSize={30}
fixedSizeHeight={1000}
sticky={true}
TableRowsRenderer={TableVirtualizedListRows}
vListrowHeight={28}
vListOverscanRowCount={2}
TableCellRenderer={TableFastCell}
styleName={TableStyle.Constrant}
/>
</FinancialSheet>
);
}
export default AuditLogTable;
@@ -0,0 +1,41 @@
// @ts-nocheck
import React, { useMemo, useState } from 'react';
import * as Yup from 'yup';
import { transformToForm } from '@/utils';
// Default query for audit log
export const getDefaultAuditLogQuery = () => ({
subject: [],
action: [],
fromDate: '',
toDate: '',
});
// Validation schema
export const getAuditLogQuerySchema = () => {
return Yup.object().shape({
fromDate: Yup.date().optional(),
toDate: Yup.date().min(Yup.ref('fromDate')).optional(),
});
};
// Parse query from URL
const parseAuditLogQuery = (locationQuery) => {
const defaultQuery = getDefaultAuditLogQuery();
return {
...defaultQuery,
...transformToForm(locationQuery, defaultQuery),
};
};
// Hook for managing query state
export const useAuditLogQuery = () => {
const [locationQuery, setLocationQuery] = useState({});
const query = useMemo(
() => parseAuditLogQuery(locationQuery),
[locationQuery]
);
return { query, setLocationQuery };
};
@@ -0,0 +1,18 @@
// @ts-nocheck
import React from 'react';
import { useAuditLogContext } from './AuditLogProvider';
import FinancialLoadingBar from '../FinancialLoadingBar';
/**
* Audit Log Loading Bar
*/
export function AuditLogLoadingBar() {
const { isFetching, isFetchingNextPage } = useAuditLogContext();
if (!isFetching || isFetchingNextPage) return null;
return (
<div className={'financial-progressbar'}>
<FinancialLoadingBar />
</div>
);
}
@@ -0,0 +1,103 @@
// @ts-nocheck
import * as qs from 'qs';
import { useInfiniteQuery } from 'react-query';
import { useRequestQuery } from '../useQueryRequest';
import useApiRequest from '../useRequest';
import { normalizeApiPath } from '@/utils';
import t from './types';
const qsArrayOptions = { skipNulls: true, arrayFormat: 'repeat' as const };
/** Normalize subject/action to a non-empty string[] or omit from query. */
function auditLogStringListParam(value) {
if (value == null || value === '') return undefined;
if (Array.isArray(value)) return value.length ? value : undefined;
return [value];
}
/**
* Paginated audit log list (financial domain events).
*/
export function useAuditLogsQuery(filters, props) {
const query = qs.stringify(
{
page: filters.page ?? 1,
pageSize: filters.pageSize ?? 20,
subject: auditLogStringListParam(filters.subject),
action: auditLogStringListParam(filters.action),
userId: filters.userId || undefined,
from: filters.from || undefined,
to: filters.to || undefined,
},
qsArrayOptions,
);
return useRequestQuery(
[t.AUDIT_LOGS, filters],
{ method: 'get', url: `audit-logs?${query}` },
{
select: (res) => res.data,
keepPreviousData: true,
...props,
},
);
}
/**
* Distinct subject/action values for audit log filter dropdowns.
*/
export function useAuditLogFilterOptionsQuery(props) {
return useRequestQuery(
[t.AUDIT_LOG_FILTER_OPTIONS],
{ method: 'get', url: 'audit-logs/filter-options' },
{
defaultData: { subjects: [], actions: [] },
select: (res) => ({
subjects: res.data?.subjects ?? [],
actions: res.data?.actions ?? [],
}),
staleTime: 5 * 60 * 1000,
...props,
},
);
}
/**
* Infinite audit log list with page-based pagination.
*/
export function useAuditLogsInfinityQuery(filters, infinityProps) {
const apiRequest = useApiRequest();
return useInfiniteQuery(
[t.AUDIT_LOGS, filters],
async ({ pageParam = 1 }) => {
const query = qs.stringify(
{
page: pageParam,
pageSize: filters.pageSize ?? 20,
subject: auditLogStringListParam(filters.subject),
action: auditLogStringListParam(filters.action),
userId: filters.userId || undefined,
from: filters.from || undefined,
to: filters.to || undefined,
},
qsArrayOptions,
);
const response = await apiRequest.http({
method: 'get',
url: `/api/${normalizeApiPath(`audit-logs?${query}`)}`,
});
return response.data;
},
{
getNextPageParam: (lastPage) => {
const { pagination } = lastPage;
return pagination.total > pagination.page_size * pagination.page
? pagination.page + 1
: undefined;
},
...infinityProps,
},
);
}
@@ -39,3 +39,4 @@ export * from './warehousesTransfers';
export * from './plaid';
export * from './FinancialReports';
export * from './apiKeys';
export * from './auditLogs';
+2 -2
View File
@@ -124,7 +124,7 @@ export function useActivateItem(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.post(`items/${id}/activate`), {
return useMutation((id) => apiRequest.patch(`items/${id}/activate`), {
onSuccess: (res, id) => {
// Invalidate specific item.
queryClient.invalidateQueries([t.ITEM, id]);
@@ -143,7 +143,7 @@ export function useInactivateItem(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.post(`items/${id}/inactivate`), {
return useMutation((id) => apiRequest.patch(`items/${id}/inactivate`), {
onSuccess: (res, id) => {
// Invalidate specific item.
queryClient.invalidateQueries([t.ITEM, id]);
+10
View File
@@ -245,6 +245,14 @@ export const API_KEYS = {
API_KEYS: 'API_KEYS',
};
const AUDIT_LOGS = {
AUDIT_LOGS: 'AUDIT_LOGS',
};
const AUDIT_LOG_FILTER_OPTIONS = {
AUDIT_LOG_FILTER_OPTIONS: 'AUDIT_LOG_FILTER_OPTIONS',
};
export default {
...Authentication,
...ACCOUNTS,
@@ -281,4 +289,6 @@ export default {
...TAX_RATES,
...EXCHANGE_RATE,
...API_KEYS,
...AUDIT_LOGS,
...AUDIT_LOG_FILTER_OPTIONS,
};
+20
View File
@@ -242,6 +242,26 @@
"new_expenses": "New Expenses",
"preferences": "Preferences",
"auditing_system": "Auditing System",
"audit_log.no_access": "You do not have permission to view the audit log.",
"audit_log.empty": "No audit entries yet.",
"audit_log.filter_subject": "Subject",
"audit_log.filter_action": "Action",
"audit_log.filter_from": "From",
"audit_log.filter_to": "To",
"audit_log.apply_filters": "Apply",
"audit_log.col_time": "Time",
"audit_log.col_user": "User",
"audit_log.col_action": "Action",
"audit_log.col_subject": "Subject",
"audit_log.col_id": "ID",
"audit_log.col_summary": "Summary",
"audit_log.col_ip": "IP",
"audit_log.pagination": "Page {page} of {pages} ({total} total)",
"audit_log.prev": "Previous",
"audit_log.next": "Next",
"audit_log_report": "Audit Log",
"audit_log_report_desc": "View system audit log entries for financial transactions and configuration changes",
"system_reports": "System Reports",
"all": "All",
"organization": "Organization.",
"check_your_email_for_a_link_to_reset": "Check your email for a link to reset your password. If it doesnt appear within a few minutes, check your spam folder.",
+11
View File
@@ -496,6 +496,17 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/audit-log`,
component: lazy(
() => import('@/containers/FinancialStatements/AuditLog/AuditLogReport'),
),
breadcrumb: intl.get('audit_log_report'),
pageTitle: intl.get('audit_log_report'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: '/financial-reports',
component: lazy(