Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions apps/erp/app/components/AuditLog/AuditLogDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import type { AuditLogEntry } from "@carbon/database/audit.types";
import {
Badge,
cn,
Drawer,
DrawerBody,
DrawerContent,
DrawerHeader,
DrawerTitle,
HStack,
Skeleton,
VStack
} from "@carbon/react";
import { formatDateTime } from "@carbon/utils";
import { memo, useEffect } from "react";
import { LuFilePen, LuFilePlus, LuFileX, LuHistory } from "react-icons/lu";
import { useFetcher } from "react-router";
import { EmployeeAvatar, Empty } from "~/components";

type AuditLogDrawerProps = {
isOpen: boolean;
onClose: () => void;
entityType: string;
entityId: string;
companyId: string;
};

type AuditLogFetcherData = {
entries: AuditLogEntry[];
};

const operationLabels: Record<
string,
{ label: string; variant: "green" | "blue" | "red"; icon: React.ReactNode }
> = {
INSERT: {
label: "Created",
variant: "green",
icon: <LuFilePlus className="size-3" />
},
UPDATE: {
label: "Updated",
variant: "blue",
icon: <LuFilePen className="size-3" />
},
DELETE: {
label: "Deleted",
variant: "red",
icon: <LuFileX className="size-3" />
}
};

const AuditLogDrawer = memo(
({
isOpen,
onClose,
entityType,
entityId,
companyId
}: AuditLogDrawerProps) => {
const fetcher = useFetcher<AuditLogFetcherData>();

// Load audit log data when drawer opens
useEffect(() => {
if (
isOpen &&
entityType &&
entityId &&
fetcher.state === "idle" &&
!fetcher.data
) {
const params = new URLSearchParams({
entityType,
entityId,
companyId
});
fetcher.load(`/api/audit-log?${params.toString()}`);
}
}, [isOpen, entityType, entityId, companyId, fetcher]);

// Reset when drawer closes
useEffect(() => {
if (!isOpen) {
// The fetcher will be reset on next open due to the data check above
}
}, [isOpen]);

const entries = fetcher.data?.entries ?? [];
const isLoading = fetcher.state === "loading";

return (
<Drawer
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose();
}}
>
<DrawerContent size="md" position="left">
<DrawerHeader>
<DrawerTitle className="flex items-center gap-2">
<LuHistory className="size-5" />
History
</DrawerTitle>
</DrawerHeader>
<DrawerBody>
{isLoading ? (
<VStack spacing={2}>
<Skeleton className="w-full h-[151px]" />
<Skeleton className="w-full h-[151px]" />
</VStack>
) : entries.length === 0 ? (
<Empty />
) : (
<VStack spacing={2}>
{entries.map((entry) => (
<AuditLogEntryCard key={entry.id} entry={entry} />
))}
</VStack>
)}
</DrawerBody>
</DrawerContent>
</Drawer>
);
}
);

AuditLogDrawer.displayName = "AuditLogDrawer";
export default AuditLogDrawer;

type AuditLogEntryCardProps = {
entry: AuditLogEntry;
};

const AuditLogEntryCard = memo(({ entry }: AuditLogEntryCardProps) => {
const opInfo = operationLabels[entry.operation] ?? {
label: entry.operation,
variant: "secondary" as const,
icon: null
};

const diffKeys = entry.diff ? Object.keys(entry.diff) : [];

return (
<div className="border bg-muted/40 rounded-lg p-4 w-full">
<HStack className="justify-between items-start mb-3">
<VStack spacing={1}>
{entry.actorId ? (
<EmployeeAvatar employeeId={entry.actorId} />
) : (
<span className="font-medium">System</span>
)}
<span
className={cn(
"text-xs text-muted-foreground",
entry.actorId && "pl-8"
)}
>
{formatDateTime(entry.createdAt)}
</span>
</VStack>
<Badge variant={opInfo.variant} className="flex-shrink-0">
<HStack className="gap-1">
{opInfo.icon}
<span>{opInfo.label}</span>
</HStack>
</Badge>
</HStack>

<div className="mt-3 pt-3 border-t">
<p className="text-sm font-medium mb-2">Changes</p>
{diffKeys.length > 0 ? (
<div className="space-y-1">
{diffKeys.map((key) => {
const change = entry.diff![key];
return (
<div
key={key}
className="flex items-center gap-2 font-mono text-sm py-1"
>
<span className="text-muted-foreground font-medium min-w-[120px]">
{key}:
</span>
{change.old !== undefined && (
<span className="px-2 py-0.5 rounded bg-red-500/10 text-red-500">
{formatValue(change.old)}
</span>
)}
{change.old !== undefined && change.new !== undefined && (
<span className="text-muted-foreground">→</span>
)}
{change.new !== undefined && (
<span className="px-2 py-0.5 rounded bg-green-500/10 text-green-500">
{formatValue(change.new)}
</span>
)}
</div>
);
})}
</div>
) : (
<p className="text-sm text-muted-foreground italic">
{entry.operation === "INSERT"
? "New record created"
: entry.operation === "DELETE"
? "Record deleted"
: "No changes recorded"}
</p>
)}
</div>
</div>
);
});

AuditLogEntryCard.displayName = "AuditLogEntryCard";

function formatValue(value: unknown): string {
if (value === null) return "null";
if (value === undefined) return "undefined";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean")
return String(value);
return JSON.stringify(value);
}
1 change: 1 addition & 0 deletions apps/erp/app/components/AuditLog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as AuditLogDrawer } from "./AuditLogDrawer";
4 changes: 2 additions & 2 deletions apps/erp/app/components/Chat/PromptInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
useRef,
useState
} from "react";
import { LuPaperclip, LuPlus, LuSend, LuSquare, LuX } from "react-icons/lu";
import { LuArrowUp, LuPaperclip, LuPlus, LuSquare, LuX } from "react-icons/lu";

type AttachmentsContextType = {
files: (FileUIPart & { id: string })[];
Expand Down Expand Up @@ -606,7 +606,7 @@ export const PromptInputSubmit = ({
children,
...props
}: PromptInputSubmitProps) => {
let Icon = <LuSend />;
let Icon = <LuArrowUp />;

if (status === "streaming") {
Icon = <LuSquare />;
Expand Down
3 changes: 2 additions & 1 deletion apps/erp/app/components/Form/AddressAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const AddressAutocomplete = ({
<FormControl isInvalid={!!error}>
<FormLabel htmlFor={address1Field}>Address Line 1</FormLabel>
<div className="relative w-full" ref={containerRef}>
<Command shouldFilter={false}>
<Command shouldFilter={false} className="bg-transparent">
<CommandInputTextField
id={address1Field}
name={address1Field}
Expand All @@ -157,6 +157,7 @@ const AddressAutocomplete = ({
onFocus={handleInputFocus}
onKeyDown={handleKeyDown}
autoComplete="off"
className="bg-transparent"
/>
{open && suggestions.length > 0 && (
<CommandList className="absolute w-full top-10 z-[9999] rounded-md border bg-popover text-popover-foreground shadow-md p-0">
Expand Down
Loading