feat(platform): add audit logs and activity feed
This commit is contained in:
parent
92cb8d1286
commit
c816e9869d
34 changed files with 1592 additions and 43 deletions
|
|
@ -5,6 +5,7 @@ import Link from "next/link";
|
|||
|
||||
import SummaryCard from "@/components/common/SummaryCard";
|
||||
import { api } from "@/lib/api";
|
||||
import type { ActivityFeedItem, ApiSuccess } from "@/types/audit";
|
||||
import type { DashboardSummary, EmptyWidget } from "@/types/dashboard";
|
||||
|
||||
function getErrorMessage(error: unknown) {
|
||||
|
|
@ -17,13 +18,19 @@ function getErrorMessage(error: unknown) {
|
|||
|
||||
export default function DashboardPage() {
|
||||
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
||||
const [activities, setActivities] = useState<ActivityFeedItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [activityError, setActivityError] = useState("");
|
||||
|
||||
const loadSummary = useCallback(async () => {
|
||||
try {
|
||||
const response = await api.get<DashboardSummary>("/dashboard/summary");
|
||||
setSummary(response.data);
|
||||
const [summaryResponse, activityResponse] = await Promise.all([
|
||||
api.get<DashboardSummary>("/dashboard/summary"),
|
||||
api.get<ApiSuccess<ActivityFeedItem[]>>("/activity-feed?limit=10"),
|
||||
]);
|
||||
setSummary(summaryResponse.data);
|
||||
setActivities(activityResponse.data.data);
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err));
|
||||
} finally {
|
||||
|
|
@ -31,6 +38,16 @@ export default function DashboardPage() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const loadActivities = useCallback(async () => {
|
||||
setActivityError("");
|
||||
try {
|
||||
const response = await api.get<ApiSuccess<ActivityFeedItem[]>>("/activity-feed?limit=10");
|
||||
setActivities(response.data.data);
|
||||
} catch (err) {
|
||||
setActivityError(getErrorMessage(err));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
void loadSummary();
|
||||
|
|
@ -92,7 +109,12 @@ export default function DashboardPage() {
|
|||
</section>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<EmptyPanel widget={summary.activities} />
|
||||
<ActivityPanel
|
||||
widget={summary.activities}
|
||||
activities={activities}
|
||||
error={activityError}
|
||||
onReload={() => void loadActivities()}
|
||||
/>
|
||||
<EmptyPanel widget={summary.tasks} />
|
||||
<EmptyPanel widget={summary.tickets} />
|
||||
<EmptyPanel widget={summary.projects} />
|
||||
|
|
@ -101,6 +123,54 @@ export default function DashboardPage() {
|
|||
);
|
||||
}
|
||||
|
||||
function ActivityPanel({
|
||||
widget,
|
||||
activities,
|
||||
error,
|
||||
onReload,
|
||||
}: {
|
||||
widget: EmptyWidget;
|
||||
activities: ActivityFeedItem[];
|
||||
error: string;
|
||||
onReload: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-white p-5 md:col-span-2 xl:col-span-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="font-semibold text-slate-950">{widget.title}</h2>
|
||||
<button type="button" className="text-sm font-medium text-slate-600 hover:text-slate-950" onClick={onReload}>
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
) : activities.length === 0 ? (
|
||||
<p className="text-sm text-slate-500">{widget.message}</p>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{activities.map((activity) => (
|
||||
<div key={activity.id} className="py-3">
|
||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="font-medium text-slate-950">{activity.title}</p>
|
||||
<time className="text-xs text-slate-500" dateTime={activity.created_at}>
|
||||
{new Intl.DateTimeFormat("de-DE", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
}).format(new Date(activity.created_at))}
|
||||
</time>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{activity.description} · {activity.actor || "System"}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyPanel({ widget }: { widget: EmptyWidget }) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-white p-5">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue