Implement Activity Log management and Person activity integration
- Updated qry__activity_log to support filtering by person_id - Created /core/activity_logs standalone page for monitoring system actions - Enhanced Person detail page with 'Recent Activity' column showing real data - Added 'Activity Logs' card to the Core Management dashboard
This commit is contained in:
@@ -181,52 +181,112 @@ export async function update_ae_obj__activity_log({
|
||||
return ae_promises.update__activity_log_obj;
|
||||
}
|
||||
|
||||
// Updated 2026-01-06
|
||||
// Updated 2026-01-07
|
||||
|
||||
export async function qry__activity_log({
|
||||
|
||||
api_cfg,
|
||||
|
||||
account_id,
|
||||
|
||||
qry_str,
|
||||
|
||||
qry_person_id = null,
|
||||
|
||||
enabled = 'enabled',
|
||||
|
||||
hidden = 'not_hidden',
|
||||
|
||||
view = 'default',
|
||||
|
||||
limit = 50,
|
||||
|
||||
offset = 0,
|
||||
|
||||
order_by_li = { created_on: 'DESC' },
|
||||
|
||||
log_lvl = 0
|
||||
|
||||
}: {
|
||||
|
||||
api_cfg: any;
|
||||
|
||||
account_id: string;
|
||||
|
||||
qry_str?: string;
|
||||
|
||||
qry_person_id?: string | null;
|
||||
|
||||
enabled?: 'enabled' | 'all' | 'not_enabled';
|
||||
|
||||
hidden?: 'hidden' | 'all' | 'not_hidden';
|
||||
|
||||
view?: string;
|
||||
|
||||
limit?: number;
|
||||
|
||||
offset?: number;
|
||||
|
||||
order_by_li?: Record<string, 'ASC' | 'DESC'>;
|
||||
|
||||
log_lvl?: number;
|
||||
|
||||
}) {
|
||||
|
||||
const search_query: any = { and: [] };
|
||||
|
||||
|
||||
|
||||
if (account_id) {
|
||||
|
||||
search_query.and.push({ field: 'account_id_random', op: 'eq', value: account_id });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (qry_str) {
|
||||
|
||||
search_query.q = qry_str;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (qry_person_id) {
|
||||
|
||||
search_query.and.push({ field: 'person_id_random', op: 'eq', value: qry_person_id });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
ae_promises.load__activity_log_obj_li = await api.search_ae_obj_v3({
|
||||
|
||||
api_cfg,
|
||||
|
||||
obj_type: 'activity_log',
|
||||
|
||||
search_query,
|
||||
|
||||
enabled,
|
||||
|
||||
hidden,
|
||||
|
||||
view,
|
||||
|
||||
limit,
|
||||
|
||||
offset,
|
||||
|
||||
order_by_li,
|
||||
|
||||
log_lvl
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
return ae_promises.load__activity_log_obj_li;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Building, Globe, Users, ShieldCheck, List } from 'lucide-svelte';
|
||||
import { Building, Globe, Users, ShieldCheck, List, History } from 'lucide-svelte';
|
||||
import { ae_loc, slct } from '$lib/stores/ae_stores';
|
||||
|
||||
interface Props {
|
||||
@@ -70,6 +70,18 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Activity Log Card -->
|
||||
<div class="card p-4 space-y-4 variant-soft-success">
|
||||
<div class="flex items-center gap-2">
|
||||
<History size={20} />
|
||||
<h3 class="h4">Activity Logs</h3>
|
||||
</div>
|
||||
<p class="text-sm opacity-80 text-balance">Monitor system actions and historical changes for the account.</p>
|
||||
<a class="btn variant-filled-success w-full" href="/core/activity_logs">
|
||||
View Activity Logs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Lookups Card -->
|
||||
<div class="card p-4 space-y-4 variant-soft-surface">
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
179
src/routes/core/activity_logs/+page.svelte
Normal file
179
src/routes/core/activity_logs/+page.svelte
Normal file
@@ -0,0 +1,179 @@
|
||||
<script lang="ts">
|
||||
/** @type {import('./$types').PageData} */
|
||||
import { onMount } from 'svelte';
|
||||
import { qry__activity_log } from '$lib/ae_core/core__activity_log';
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { Search, History, Calendar, User, Tag, Activity } from 'lucide-svelte';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
|
||||
// State
|
||||
let log_li: any[] = $state([]);
|
||||
let loading = $state(true);
|
||||
let qry_str = $state('');
|
||||
let limit = $state(50);
|
||||
|
||||
/**
|
||||
* Fetches activity logs from the API based on current filters.
|
||||
*/
|
||||
async function load_logs() {
|
||||
if (!$ae_loc.account_id) return;
|
||||
loading = true;
|
||||
|
||||
const results = await qry__activity_log({
|
||||
api_cfg: $ae_api,
|
||||
account_id: $ae_loc.account_id,
|
||||
qry_str: qry_str || undefined,
|
||||
limit: limit,
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
log_li = results || [];
|
||||
loading = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
load_logs();
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the search form submission.
|
||||
*/
|
||||
function handle_search(e: Event) {
|
||||
e.preventDefault();
|
||||
load_logs();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-4 space-y-4">
|
||||
<header class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<History size={24} />
|
||||
<h1 class="h2">Activity Logs</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="card p-4 variant-soft">
|
||||
<form onsubmit={handle_search} class="flex flex-wrap gap-4 items-end">
|
||||
<label class="label grow max-w-md">
|
||||
<span>Search (Keyword)</span>
|
||||
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
|
||||
<input type="search" placeholder="Action, name, description..." bind:value={qry_str} />
|
||||
<button type="submit" class="variant-filled-secondary">
|
||||
<Search size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span>Limit</span>
|
||||
<select class="select" bind:value={limit} onchange={load_logs}>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={250}>200</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button type="button" class="btn variant-filled-secondary" onclick={load_logs} disabled={loading}>
|
||||
Refresh
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center p-12">
|
||||
<div class="placeholder animate-pulse w-full h-32"></div>
|
||||
</div>
|
||||
{:else if log_li.length === 0}
|
||||
<div class="card p-8 text-center">
|
||||
<p>No activity logs found for this account matching your criteria.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="table-container">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date/Time</th>
|
||||
<th>User/Person</th>
|
||||
<th>Action</th>
|
||||
<th>Context</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each log_li as log}
|
||||
<tr>
|
||||
<!-- Date/Time -->
|
||||
<td class="whitespace-nowrap">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold">{ae_util.iso_datetime_formatter(log.created_on, 'date_short')}</span>
|
||||
<span class="text-xs opacity-60">{ae_util.iso_datetime_formatter(log.created_on, 'time_12_short')}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- User/Person -->
|
||||
<td>
|
||||
<div class="flex flex-col max-w-[200px] truncate">
|
||||
{#if log.person_full_name || log.person_id_random}
|
||||
<span class="flex items-center gap-1">
|
||||
<User size={12} />
|
||||
{log.person_full_name || 'Person'}
|
||||
</span>
|
||||
<span class="text-[10px] opacity-50 font-mono">{log.person_id_random || '--'}</span>
|
||||
{:else if log.name}
|
||||
<span class="italic text-xs opacity-70">{log.name}</span>
|
||||
{:else}
|
||||
<span class="opacity-30">--</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Action -->
|
||||
<td>
|
||||
<span class="badge variant-soft-primary uppercase text-[10px] tracking-wider">
|
||||
{log.action}
|
||||
</span>
|
||||
{#if log.action_with}
|
||||
<span class="text-xs opacity-60 ml-1">via {log.action_with}</span>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- Context -->
|
||||
<td>
|
||||
{#if log.object_type}
|
||||
<div class="flex flex-col text-xs">
|
||||
<span class="flex items-center gap-1 opacity-70">
|
||||
<Tag size={10} />
|
||||
{log.object_type}
|
||||
</span>
|
||||
{#if log.external_client_id}
|
||||
<span class="text-[10px] opacity-50 font-mono truncate max-w-[100px]">{log.external_client_id}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="opacity-30">--</span>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- Summary/Description -->
|
||||
<td>
|
||||
<div class="flex flex-col gap-1 max-w-md">
|
||||
{#if log.summary}
|
||||
<span class="font-medium text-sm">{log.summary}</span>
|
||||
{/if}
|
||||
{#if log.description}
|
||||
<p class="text-xs opacity-70 line-clamp-2" title={log.description}>{log.description}</p>
|
||||
{/if}
|
||||
{#if !log.summary && !log.description}
|
||||
<span class="italic opacity-30 text-xs">No detail provided</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
5
src/routes/core/activity_logs/+page.ts
Normal file
5
src/routes/core/activity_logs/+page.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ parent }) {
|
||||
const data = await parent();
|
||||
return data;
|
||||
}
|
||||
@@ -31,7 +31,8 @@
|
||||
import { update_ae_obj__person } from '$lib/ae_core/ae_core__person';
|
||||
import { qry_ae_obj_li__event } from '$lib/ae_events/ae_events__event';
|
||||
import { qry__post } from '$lib/ae_posts/ae_posts__post';
|
||||
import { Users, Link, Unlink, UserPlus, ShieldCheck, User, Calendar, MessageSquare, History } from 'lucide-svelte';
|
||||
import { qry__activity_log } from '$lib/ae_core/core__activity_log';
|
||||
import { Users, Link, Unlink, UserPlus, ShieldCheck, User, Calendar, MessageSquare, History, Activity } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
@@ -59,6 +60,7 @@
|
||||
|
||||
let related_events: any[] = $state([]);
|
||||
let related_posts: any[] = $state([]);
|
||||
let related_activity_logs: any[] = $state([]);
|
||||
let loading_activity = $state(false);
|
||||
|
||||
async function load_activity() {
|
||||
@@ -66,7 +68,7 @@
|
||||
loading_activity = true;
|
||||
|
||||
// Load related data using search queries
|
||||
const [events, posts] = await Promise.all([
|
||||
const [events, posts, logs] = await Promise.all([
|
||||
qry_ae_obj_li__event({
|
||||
api_cfg: $ae_api,
|
||||
for_obj_id: $ae_loc.account_id,
|
||||
@@ -78,11 +80,19 @@
|
||||
account_id: $ae_loc.account_id,
|
||||
qry_person_id: $slct.person_id,
|
||||
log_lvl: 1
|
||||
}),
|
||||
qry__activity_log({
|
||||
api_cfg: $ae_api,
|
||||
account_id: $ae_loc.account_id,
|
||||
qry_person_id: $slct.person_id,
|
||||
limit: 10,
|
||||
log_lvl: 1
|
||||
})
|
||||
]);
|
||||
|
||||
related_events = events || [];
|
||||
related_posts = posts || [];
|
||||
related_activity_logs = logs || [];
|
||||
loading_activity = false;
|
||||
}
|
||||
|
||||
@@ -294,7 +304,7 @@
|
||||
<span>Linked Activity & Content</span>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Related Events -->
|
||||
<div class="space-y-3">
|
||||
<h4 class="h4 flex items-center gap-2 text-sm opacity-70 uppercase tracking-wider font-bold">
|
||||
@@ -308,8 +318,8 @@
|
||||
<div class="space-y-2">
|
||||
{#each related_events as ev}
|
||||
<a href="/events/{ev.event_id_random}" class="card p-3 variant-soft flex flex-col gap-1 hover:variant-soft-primary transition-all">
|
||||
<span class="font-bold">{ev.name}</span>
|
||||
<span class="text-xs opacity-60">{new Date(ev.start_datetime).toLocaleDateString()}</span>
|
||||
<span class="font-bold text-sm">{ev.name}</span>
|
||||
<span class="text-[10px] opacity-60">{new Date(ev.start_datetime).toLocaleDateString()}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -328,17 +338,46 @@
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each related_posts as post}
|
||||
<a href="/posts/{post.post_id_random}" class="card p-3 variant-soft flex flex-col gap-1 hover:variant-soft-primary transition-all">
|
||||
<span class="font-bold">{post.title}</span>
|
||||
<div class="flex justify-between items-center text-xs opacity-60">
|
||||
<a href="/idaa/bb/{post.post_id_random}" class="card p-3 variant-soft flex flex-col gap-1 hover:variant-soft-primary transition-all">
|
||||
<span class="font-bold text-sm">{post.title}</span>
|
||||
<div class="flex justify-between items-center text-[10px] opacity-60">
|
||||
<span>{new Date(post.created_on).toLocaleDateString()}</span>
|
||||
<span class="badge variant-soft-surface">{post.post_comment_count || 0} comments</span>
|
||||
<span class="badge variant-soft-surface scale-75">{post.post_comment_count || 0} comments</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="space-y-3">
|
||||
<h4 class="h4 flex items-center gap-2 text-sm opacity-70 uppercase tracking-wider font-bold">
|
||||
<Activity size={16} /> Recent Activity
|
||||
</h4>
|
||||
{#if loading_activity}
|
||||
<div class="placeholder animate-pulse h-20 w-full"></div>
|
||||
{:else if related_activity_logs.length === 0}
|
||||
<p class="text-sm italic opacity-50">No recent activity logs.</p>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each related_activity_logs as log}
|
||||
<div class="card p-3 variant-soft flex flex-col gap-1">
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<span class="badge variant-filled-surface text-[9px] uppercase tracking-tighter">{log.action}</span>
|
||||
<span class="text-[9px] opacity-50">{new Date(log.created_on).toLocaleDateString()}</span>
|
||||
</div>
|
||||
{#if log.summary}
|
||||
<span class="text-xs opacity-80 line-clamp-1">{log.summary}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<a href="/core/activity_logs?person_id={$slct.person_id}" class="btn btn-sm variant-soft-surface w-full text-[10px]">
|
||||
View All Activity
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user