The labelled icon-badge tiles were inconsistent: some used a same-hue light tile + colored glyph (e.g. the Settings 2FA row: success.light tile + success.main glyph — low contrast, the reported "badly visible" icon), and the solid-tile ones used the bright .main fill in dark mode without darkening, so a white glyph on bright green/amber was low-contrast too (DashProfile's success badge was ~1.9:1). Add a single `iconBadgeSx(color)` helper (theme.ts): solid `<color>.main` fill + white glyph, darkened in dark mode (FILLED_DARK_BG) so white stays legible; `default` = neutral grey. Applied to every badge so they can't drift: StatCard, DashActivityFeed, DashProfile (2FA badge), Dashboard (2FA banner icon), AttendanceHistory (month tile), and the Settings 2FA row tile (now solid green when required / grey when optional + white lock, instead of the low-contrast light-green tile). Also switched DashProfile's backup-codes warning callout from warning.light to a subtle warning wash so its amber text stays readable in dark mode. tsc -b --noEmit, npm run build, vitest 152/152 all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
212 lines
5.8 KiB
TypeScript
212 lines
5.8 KiB
TypeScript
import { Link as RouterLink } from "react-router-dom";
|
|
import Box from "@mui/material/Box";
|
|
import Typography from "@mui/material/Typography";
|
|
import { Card, Button, EmptyState } from "../../ui";
|
|
import { iconBadgeSx, type IconBadgeColor } from "../../theme";
|
|
import {
|
|
ENTITY_TYPE_LABELS,
|
|
getActivityIconClass,
|
|
formatActivityTime,
|
|
} from "../../utils/dashboardHelpers";
|
|
|
|
interface Activity {
|
|
id: number | string;
|
|
action: string;
|
|
description: string;
|
|
username?: string;
|
|
entity_type: string;
|
|
created_at: string;
|
|
}
|
|
|
|
interface DashActivityFeedProps {
|
|
activities: Activity[] | null;
|
|
}
|
|
|
|
// Maps the legacy activity-icon class to the MUI palette key used to tint the
|
|
// circular icon badge (danger → error, accent → primary, muted → default).
|
|
const ACTIVITY_BADGE_COLOR: Record<string, string> = {
|
|
success: "success",
|
|
info: "info",
|
|
danger: "error",
|
|
accent: "primary",
|
|
muted: "default",
|
|
};
|
|
|
|
function getActivityIcon(action: string) {
|
|
switch (action) {
|
|
case "create":
|
|
return (
|
|
<svg
|
|
width="15"
|
|
height="15"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<line x1="12" y1="5" x2="12" y2="19" />
|
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
</svg>
|
|
);
|
|
case "update":
|
|
return (
|
|
<svg
|
|
width="15"
|
|
height="15"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
</svg>
|
|
);
|
|
case "delete":
|
|
return (
|
|
<svg
|
|
width="15"
|
|
height="15"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<polyline points="3 6 5 6 21 6" />
|
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
</svg>
|
|
);
|
|
case "login":
|
|
return (
|
|
<svg
|
|
width="15"
|
|
height="15"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
|
|
<polyline points="10 17 15 12 10 7" />
|
|
<line x1="15" y1="12" x2="3" y2="12" />
|
|
</svg>
|
|
);
|
|
default:
|
|
return (
|
|
<svg
|
|
width="15"
|
|
height="15"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="12" y1="16" x2="12" y2="12" />
|
|
<line x1="12" y1="8" x2="12.01" y2="8" />
|
|
</svg>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default function DashActivityFeed({
|
|
activities,
|
|
}: DashActivityFeedProps) {
|
|
if (!activities) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Card sx={{ display: "flex", flexDirection: "column" }}>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
mb: 2,
|
|
}}
|
|
>
|
|
<Typography variant="h6">Audit log</Typography>
|
|
<Button
|
|
component={RouterLink}
|
|
to="/audit-log"
|
|
size="small"
|
|
sx={{ flexShrink: 0 }}
|
|
>
|
|
Detail →
|
|
</Button>
|
|
</Box>
|
|
|
|
{activities.length === 0 ? (
|
|
<EmptyState title="Žádná aktivita" />
|
|
) : (
|
|
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
|
{activities.map((act) => {
|
|
const badgeColor =
|
|
ACTIVITY_BADGE_COLOR[getActivityIconClass(act.action)] ??
|
|
"default";
|
|
const isDefault = badgeColor === "default";
|
|
return (
|
|
<Box
|
|
key={act.id}
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 1.5,
|
|
py: 1.25,
|
|
borderBottom: 1,
|
|
borderColor: "divider",
|
|
"&:last-of-type": { borderBottom: 0 },
|
|
}}
|
|
>
|
|
<Box
|
|
sx={[
|
|
{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: "50%",
|
|
flexShrink: 0,
|
|
},
|
|
iconBadgeSx(
|
|
isDefault ? "default" : (badgeColor as IconBadgeColor),
|
|
),
|
|
]}
|
|
>
|
|
{getActivityIcon(act.action)}
|
|
</Box>
|
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ fontWeight: 500 }}
|
|
noWrap
|
|
title={act.description}
|
|
>
|
|
{act.description}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary" noWrap>
|
|
{act.username || "Systém"} ·{" "}
|
|
{ENTITY_TYPE_LABELS[act.entity_type] || act.entity_type}
|
|
</Typography>
|
|
</Box>
|
|
<Typography
|
|
variant="caption"
|
|
color="text.secondary"
|
|
sx={{
|
|
fontFamily: "'DM Mono', Menlo, monospace",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{formatActivityTime(act.created_at)}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
})}
|
|
</Box>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|