/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon/Icon";
import ToggleButton from "@mui/material/ToggleButton";
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import QContext from "QContext";
import Client from "qqq/utils/qqq/Client";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
import React, {useContext, useEffect, useState} from "react";
interface Props
{
tableMetaData: QTableMetaData;
recordId: any;
record: QRecord;
}
AuditBody.defaultProps =
{};
const qController = Client.getInstance();
function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
{
const [audits, setAudits] = useState([] as QRecord[]);
const [total, setTotal] = useState(null as number);
const [limit, setLimit] = useState(1000);
const [statusString, setStatusString] = useState("Loading audits...");
const [auditsByDate, setAuditsByDate] = useState([] as QRecord[][]);
const [auditDetailMap, setAuditDetailMap] = useState(null as Map);
const [fieldChangeMap, setFieldChangeMap] = useState(null as Map);
const [sortDirection, setSortDirection] = useState(localStorage.getItem("audit.sortDirection") === "true");
const {accentColor} = useContext(QContext);
function wrapValue(value: any): JSX.Element
{
return {value};
}
function wasValue(value: any): JSX.Element
{
return {value};
}
function getAuditDetailFieldChangeRow(qRecord: QRecord): JSX.Element | null
{
const message = qRecord.values.get("auditDetail.message");
const fieldName = qRecord.values.get("auditDetail.fieldName");
const oldValue = qRecord.values.get("auditDetail.oldValue");
const newValue = qRecord.values.get("auditDetail.newValue");
if (fieldName && (oldValue !== null || newValue !== null))
{
const fieldLabel = tableMetaData?.fields?.get(fieldName)?.label ?? fieldName;
return (
{fieldLabel} |
{oldValue} |
{newValue} |
);
}
return (null);
}
function getAuditDetailElement(qRecord: QRecord): JSX.Element | null
{
const message = qRecord.values.get("auditDetail.message");
const fieldName = qRecord.values.get("auditDetail.fieldName");
const oldValue = qRecord.values.get("auditDetail.oldValue");
const newValue = qRecord.values.get("auditDetail.newValue");
if (fieldName && (oldValue !== null || newValue !== null))
{
const fieldLabel = tableMetaData?.fields?.get(fieldName)?.label ?? fieldName;
if (oldValue !== undefined && newValue !== undefined)
{
return (<>{fieldLabel}: Changed from {(oldValue)} to {(newValue)}>);
}
else if (newValue !== undefined)
{
return (<>{fieldLabel}: Set to {(newValue)}>);
}
else if (oldValue !== undefined)
{
return (<>{fieldLabel}: Removed value {(oldValue)}>);
}
else if (message)
{
return (<>{message}>);
}
/*
const fieldLabel = {tableMetaData?.fields?.get(fieldName)?.label ?? fieldName};
if(oldValue !== undefined && newValue !== undefined)
{
return (<>Changed {fieldLabel} from {wrapValue(oldValue)} to {wrapValue(newValue)}>);
}
else if(newValue !== undefined)
{
return (<>Set {fieldLabel} to {wrapValue(newValue)}>);
}
else if(oldValue !== undefined)
{
return (<>Removed {fieldLabel} value {wrapValue(oldValue)}>);
}
*/
/*
const fieldLabel = {tableMetaData?.fields?.get(fieldName)?.label ?? fieldName}:;
if(oldValue !== undefined && newValue !== undefined)
{
return (<>{fieldLabel} {wrapValue(newValue)} (was {oldValue})>);
}
else if(newValue !== undefined)
{
return (<>{fieldLabel} {wrapValue(newValue)} (was --)>);
}
else if(oldValue !== undefined)
{
return (<>{fieldLabel} {wrapValue("--")} (was {oldValue})>);
}
*/
/*
const fieldLabel = {tableMetaData?.fields?.get(fieldName)?.label ?? fieldName}:;
if(oldValue !== undefined && newValue !== undefined)
{
return (<>{fieldLabel} {newValue} {wasValue(`(was ${oldValue})`)}>);
}
else if(newValue !== undefined)
{
return (<>{fieldLabel} {newValue} {wasValue("(was --)")}>);
}
else if(oldValue !== undefined)
{
return (<>{fieldLabel} -- {wasValue(`(was ${oldValue})`)}>);
}
*/
/*
const fieldLabel = {tableMetaData?.fields?.get(fieldName)?.label ?? fieldName}:;
if(oldValue !== undefined && newValue !== undefined)
{
return (<>{fieldLabel} Changed to {wrapValue(newValue)} (was {oldValue})>);
}
else if(newValue !== undefined)
{
return (<>{fieldLabel} Set to {wrapValue(newValue)}>);
}
else if(oldValue !== undefined)
{
return (<>{fieldLabel} Removed value (was {oldValue})>);
}
*/
}
else if (message)
{
return (<>{message}>);
}
return (null);
}
useEffect(() =>
{
(async () =>
{
/////////////////////////////////
// setup filter to load audits //
/////////////////////////////////
const filter = new QQueryFilter([
new QFilterCriteria("auditTable.name", QCriteriaOperator.EQUALS, [tableMetaData.name]),
new QFilterCriteria("recordId", QCriteriaOperator.EQUALS, [recordId]),
], [
new QFilterOrderBy("timestamp", sortDirection),
new QFilterOrderBy("id", sortDirection),
new QFilterOrderBy("auditDetail.id", true)
], null, "AND", 0, limit);
///////////////////////////////
// fetch audits in try-catch //
///////////////////////////////
let audits = [] as QRecord[];
try
{
audits = await qController.query("audit", filter, [new QueryJoin("auditDetail", true, "LEFT")]);
setAudits(audits);
}
catch (e)
{
if (e instanceof QException)
{
if ((e as QException).status === 403)
{
setStatusString("You do not have permission to view audits");
return;
}
}
setStatusString("Error loading audits");
}
// if we fetched the limit
if (audits.length == limit)
{
const [count, distinctCount] = await qController.count("audit", filter, null, true); // todo validate distinct working here!
setTotal(distinctCount);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// group the audits by auditId (e.g., this is a list that joined audit & auditDetail, so un-flatten it) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
const unflattenedAudits: QRecord[] = [];
const detailMap: Map = new Map();
const fieldChangeRowsMap: Map = new Map();
for (let i = 0; i < audits.length; i++)
{
let id = audits[i].values.get("id");
if (i == 0 || unflattenedAudits[unflattenedAudits.length - 1].values.get("id") != id)
{
unflattenedAudits.push(audits[i]);
}
let auditDetail = getAuditDetailElement(audits[i]);
if (auditDetail)
{
if (!detailMap.has(id))
{
detailMap.set(id, []);
}
detailMap.get(id).push(auditDetail);
}
// table version, probably not to commit
let fieldChangeRow = getAuditDetailFieldChangeRow(audits[i]);
if (auditDetail)
{
if (!fieldChangeRowsMap.has(id))
{
fieldChangeRowsMap.set(id, []);
}
// fieldChangeRowsMap.get(id).push(fieldChangeRow)
}
}
audits = unflattenedAudits;
setAuditDetailMap(detailMap);
const fieldChangeMap: Map = new Map();
for (let i = 0; i < unflattenedAudits.length; i++)
{
let id = unflattenedAudits[i].values.get("id");
if (fieldChangeRowsMap.has(id) && fieldChangeRowsMap.get(id).length > 0)
{
const fieldChangeTable = (
Field |
Old Value |
New Value |
{fieldChangeRowsMap.get(id).map((row, key) => {row})}
);
fieldChangeMap.set(id, fieldChangeTable);
}
}
setFieldChangeMap(fieldChangeMap);
//////////////////////////////
// group the audits by date //
//////////////////////////////
const auditsByDate = [];
let thisDatesAudits = null as QRecord[];
let lastDate = null;
for (let i = 0; i < audits.length; i++)
{
const audit = audits[i];
const date = ValueUtils.formatDateTime(audit.values.get("timestamp")).split(" ")[0];
if (date != lastDate)
{
thisDatesAudits = [];
auditsByDate.push(thisDatesAudits);
lastDate = date;
}
thisDatesAudits.push(audit);
}
setAuditsByDate(auditsByDate);
///////////////////////////
// set the status string //
///////////////////////////
if (audits.length == 0)
{
setStatusString("No audits were found for this record.");
}
else
{
if (total)
{
setStatusString(`Showing first ${limit?.toLocaleString()} of ${total?.toLocaleString()} audits for this record`);
}
else
{
if (audits.length == 1)
{
setStatusString("Showing the only audit for this record");
}
else if (audits.length == 2)
{
setStatusString("Showing the only 2 audits for this record");
}
else
{
setStatusString(`Showing all ${audits.length?.toLocaleString()} audits for this record`);
}
}
}
}
)();
}, [sortDirection]);
const changeSortDirection = () =>
{
setAudits([]);
const newSortDirection = !sortDirection;
setSortDirection(newSortDirection);
localStorage.setItem("audit.sortDirection", String(newSortDirection));
};
const todayFormatted = ValueUtils.formatDateTime(new Date()).split(" ")[0];
const yesterday = new Date();
yesterday.setTime(yesterday.getTime() - 24 * 60 * 60 * 1000);
const yesterdayFormatted = ValueUtils.formatDateTime(yesterday).split(" ")[0];
return (
Audit for {tableMetaData.label}: {record?.recordLabel ?? recordId}
{statusString}
Sort
arrow_upward
arrow_downward
{
auditsByDate.length ? auditsByDate.map((audits) =>
{
if (audits.length)
{
const audit0 = audits[0];
const formattedTimestamp = ValueUtils.formatDateTime(audit0.values.get("timestamp"));
const timestampParts = formattedTimestamp.split(" ");
return (
{ValueUtils.getFullWeekday(audit0.values.get("timestamp"))} {timestampParts[0]}
{timestampParts[0] == todayFormatted ? " (Today)" : ""}
{timestampParts[0] == yesterdayFormatted ? " (Yesterday)" : ""}
{
audits.map((audit) =>
{
const formattedTimestamp = ValueUtils.formatDateTime(audit.values.get("timestamp"));
const timestampParts = formattedTimestamp.split(" ");
return (
check
{timestampParts[1]} {timestampParts[2]} {timestampParts[3]} {audit.displayValues.get("auditUserId")}
{audit.values.get("message")}
{
auditDetailMap.get(audit.values.get("id"))?.map((detail, key) =>
{
return (- {detail}
);
})
}
{
fieldChangeMap.has(audit.values.get("id")) && fieldChangeMap.get(audit.values.get("id"))
}
);
})
}
);
}
else
{
return <>>;
}
}) : <>>
}
);
}
export default AuditBody;