feat: implement activity logging for user actions and create activities table
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 44s

This commit is contained in:
Kaimbacher 2026-06-24 15:03:17 +02:00
commit 7e2f320b4f
12 changed files with 420 additions and 160 deletions

View file

@ -209,4 +209,13 @@ export interface Identifier {
// STATE_DISABLED = 0,
// STATE_VALIDATED = 1,
// STATE_2FA_AUTHENTICATED = 1,
// }
// }
// resources/js/Dataset.ts (oder wo User definiert ist)
export interface Activity {
id: number | string;
type: string;
description: string;
user: string | null;
created_at: string; // ISO-String, relativeTime() erwartet das
}

View file

@ -32,6 +32,7 @@ import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.
import CardBoxDataset from '@/Components/CardBoxDataset.vue';
import type { User } from '@/Dataset';
import { stardust } from '@eidellev/adonis-stardust/client';
import type { Activity } from '@/Dataset';
const mainService = MainService();
@ -65,6 +66,7 @@ onMounted(async () => {
mainService.fetchApi('clients'),
mainService.fetchApi('authors'),
mainService.fetchApi('datasets'),
mainService.fetchApi('activities'),
loadChart(),
]);
} catch (e) {
@ -176,16 +178,16 @@ const quickActions = [
},
];
type Activity = {
id: number | string;
description: string;
user?: string;
created_at: string;
};
// type Activity = {
// id: number | string;
// description: string;
// user?: string;
// created_at: string;
// };
// Reads from the store if your backend provides it; otherwise renders an empty
// state. Populate via e.g. mainService.fetchApi('activities') in onMounted.
const recentActivity = computed<Activity[]>(() => (mainService as any).activities ?? []);
const recentActivity = computed<Activity[]>(() => mainService.activities);
const relativeTime = (iso: string): string => {
const then = new Date(iso).getTime();
@ -235,67 +237,6 @@ const relativeTime = (iso: string): string => {
{{ loadError }}
</div>
<!-- Stats Grid -->
<div class="reveal reveal-1 grid grid-cols-1 gap-6 lg:grid-cols-3 mb-8">
<div
class="rounded-xl border-l-4 border-emerald-500 bg-white dark:bg-slate-900/40 shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300"
>
<CardBoxWidget color="text-emerald-500" :icon="mdiAccountMultiple" :number="authors.length" label="Authors" />
</div>
<div
class="rounded-xl border-l-4 border-blue-500 bg-white dark:bg-slate-900/40 shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300"
>
<CardBoxWidget color="text-blue-500" :icon="mdiDatabaseOutline" :number="datasets.length" label="Publications" />
</div>
<div
class="rounded-xl border-l-4 border-purple-500 bg-white dark:bg-slate-900/40 shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300"
>
<CardBoxWidget color="text-purple-500" :icon="mdiChartTimelineVariant" :number="submitters.length" label="Submitters" />
</div>
</div>
<!-- Recent Datasets Section -->
<div v-if="recentDatasets.length > 0" class="reveal reveal-2 mb-8">
<SectionTitleLineWithButton :icon="mdiTrendingUp" title="Recent Publications">
<span class="text-sm text-gray-500 dark:text-gray-400"> Latest {{ recentDatasets.length }} publications </span>
</SectionTitleLineWithButton>
<div class="grid grid-cols-1 gap-4">
<CardBoxDataset
v-for="dataset in recentDatasets"
:key="dataset.id"
:dataset="dataset"
class="hover:shadow-md transition-all duration-300"
/>
</div>
</div>
<!-- Chart Section -->
<SectionTitleLineWithButton :icon="mdiChartPie" title="Trends Overview" class="reveal reveal-3 mt-8">
<span class="text-sm text-gray-500 dark:text-gray-400">Publications per month</span>
</SectionTitleLineWithButton>
<CardBox
title="Performance"
:icon="mdiFinance"
:header-icon="mdiReload"
class="reveal reveal-3 mb-6 shadow-lg"
@header-icon-click="loadChart"
>
<div v-if="isLoadingChart" role="status" aria-live="polite" class="flex items-center justify-center h-96">
<div class="flex flex-col items-center gap-3">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
<p class="text-sm text-gray-500 dark:text-gray-400">Loading chart data...</p>
</div>
</div>
<div v-else-if="chartData" class="relative">
<line-chart :data="chartData" class="h-96" />
</div>
<div v-else class="flex items-center justify-center h-96 text-gray-500 dark:text-gray-400">
<p>No chart data available</p>
</div>
</CardBox>
<!-- ============================== Admin ============================== -->
<template v-if="isAdmin">
<SectionTitleLineWithButton :icon="mdiShieldCrownOutline" title="Admin Overview" class="mt-10">
@ -330,7 +271,12 @@ const relativeTime = (iso: string): string => {
<!-- Quick actions + Recent activity -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- Quick actions -->
<CardBox :icon="mdiLightningBoltOutline" :show-header-icon="false" title="Quick Actions" class="lg:col-span-1 shadow-lg">
<CardBox
:icon="mdiLightningBoltOutline"
:show-header-icon="false"
title="Quick Actions"
class="lg:col-span-1 shadow-lg"
>
<div class="grid gap-3">
<Link
v-for="action in quickActions"
@ -398,6 +344,67 @@ const relativeTime = (iso: string): string => {
<TableSampleClients />
</CardBox>
</template>
<!-- Stats Grid -->
<!-- <div class="reveal reveal-1 grid grid-cols-1 gap-6 lg:grid-cols-3 mb-8">
<div
class="rounded-xl border-l-4 border-emerald-500 bg-white dark:bg-slate-900/40 shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300"
>
<CardBoxWidget color="text-emerald-500" :icon="mdiAccountMultiple" :number="authors.length" label="Authors" />
</div>
<div
class="rounded-xl border-l-4 border-blue-500 bg-white dark:bg-slate-900/40 shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300"
>
<CardBoxWidget color="text-blue-500" :icon="mdiDatabaseOutline" :number="datasets.length" label="Publications" />
</div>
<div
class="rounded-xl border-l-4 border-purple-500 bg-white dark:bg-slate-900/40 shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300"
>
<CardBoxWidget color="text-purple-500" :icon="mdiChartTimelineVariant" :number="submitters.length" label="Submitters" />
</div>
</div> -->
<!-- Recent Datasets Section -->
<div v-if="recentDatasets.length > 0" class="reveal reveal-2 mb-8">
<SectionTitleLineWithButton :icon="mdiTrendingUp" title="Recent Publications">
<span class="text-sm text-gray-500 dark:text-gray-400"> Latest {{ recentDatasets.length }} publications </span>
</SectionTitleLineWithButton>
<div class="grid grid-cols-1 gap-4">
<CardBoxDataset
v-for="dataset in recentDatasets"
:key="dataset.id"
:dataset="dataset"
class="hover:shadow-md transition-all duration-300"
/>
</div>
</div>
<!-- Chart Section -->
<SectionTitleLineWithButton :icon="mdiChartPie" title="Trends Overview" class="reveal reveal-3 mt-8">
<span class="text-sm text-gray-500 dark:text-gray-400">Publications per month</span>
</SectionTitleLineWithButton>
<CardBox
title="Performance"
:icon="mdiFinance"
:header-icon="mdiReload"
class="reveal reveal-3 mb-6 shadow-lg"
@header-icon-click="loadChart"
>
<div v-if="isLoadingChart" role="status" aria-live="polite" class="flex items-center justify-center h-96">
<div class="flex flex-col items-center gap-3">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
<p class="text-sm text-gray-500 dark:text-gray-400">Loading chart data...</p>
</div>
</div>
<div v-else-if="chartData" class="relative">
<line-chart :data="chartData" class="h-96" />
</div>
<div v-else class="flex items-center justify-center h-96 text-gray-500 dark:text-gray-400">
<p>No chart data available</p>
</div>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>

View file

@ -1,6 +1,6 @@
import { defineStore } from 'pinia';
import axios from 'axios';
import { Dataset } from '@/Dataset';
import { Activity, Dataset } from '@/Dataset';
import menu from '@/menu';
// import type Person from '#models/person';
@ -133,6 +133,8 @@ export const MainService = defineStore('main', {
used: 0,
codes: [],
activities: [] as Array<Activity>, // <-- neu
graphData: {},
}),
actions: {
@ -203,6 +205,17 @@ export const MainService = defineStore('main', {
});
},
// async fetchApi(resource: string) {
// try {
// const { data } = await axios.get(`/api/${resource}`);
// // @ts-ignore dynamischer Key
// this[resource] = data;
// } catch (error) {
// console.error(`Failed to fetch ${resource}`, error);
// }
// },
setState(state: any) {
this.totpState = state;
},