wip
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 doesn’t appear within a few minutes, check your spam folder.",
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user