/* * 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 = ( {fieldChangeRowsMap.get(id).map((row, key) => {row})}
Field Old Value New Value
); 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;