Fix 500 error, add Address/Contact placeholders, and enhance Person activity view

- Fixed broken import path in /core dashboard
- Simplified core layout data requirements to prevent crashes
- Implemented V3 CRUD and Dexie tables for Addresses and Contacts
- Created placeholder management pages for /core/addresses and /core/contacts
- Added 'Linked Activity & Content' section to Person detail page to show related events and posts
This commit is contained in:
Scott Idem
2026-01-06 16:16:31 -05:00
parent 076ac1e662
commit d2084de4d8
8 changed files with 460 additions and 5 deletions

View File

@@ -0,0 +1,79 @@
import type { key_val } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api';
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { db_core } from '$lib/ae_core/db_core';
const ae_promises: key_val = {};
export interface Address {
id: string;
address_id: string;
address_id_random: string;
account_id: string;
account_id_random: string;
for_type?: string;
for_id?: string;
for_id_random?: string;
city: string;
state_province: string;
country: string;
enable: null | boolean;
hide?: null | boolean;
priority?: null | boolean;
sort?: null | number;
group?: null | string;
notes?: null | string;
created_on: Date;
updated_on?: null | Date;
}
export async function load_ae_obj_li__address({
api_cfg,
for_obj_type,
for_obj_id,
enabled = 'enabled',
hidden = 'not_hidden',
log_lvl = 0
}: {
api_cfg: any;
for_obj_type?: string;
for_obj_id?: string;
enabled?: 'all' | 'enabled' | 'not_enabled';
hidden?: 'all' | 'hidden' | 'not_hidden';
log_lvl?: number;
}) {
ae_promises.load__address_obj_li = await api.get_ae_obj_li_v3({
api_cfg,
obj_type: 'address',
for_obj_type,
for_obj_id,
enabled,
hidden,
log_lvl
}).then(async (result) => {
if (result && Array.isArray(result)) {
const processed = await process_ae_obj__address_props({ obj_li: result, log_lvl });
await db_save_ae_obj_li__ae_obj({
db_instance: db_core,
table_name: 'address',
obj_li: processed,
properties_to_save: ['id', 'address_id', 'address_id_random', 'city', 'state_province', 'country', 'enable'],
log_lvl
});
return result;
}
return [];
});
return ae_promises.load__address_obj_li;
}
export async function process_ae_obj__address_props({ obj_li, log_lvl = 0 }: { obj_li: any[], log_lvl?: number }) {
return obj_li.map(obj => {
const new_obj = { ...obj };
new_obj.id = obj.address_id_random;
return new_obj;
});
}

View File

@@ -0,0 +1,79 @@
import type { key_val } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api';
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { db_core } from '$lib/ae_core/db_core';
const ae_promises: key_val = {};
export interface Contact {
id: string;
contact_id: string;
contact_id_random: string;
account_id: string;
account_id_random: string;
for_type?: string;
for_id?: string;
for_id_random?: string;
name: string;
email: string;
phone: string;
enable: null | boolean;
hide?: null | boolean;
priority?: null | boolean;
sort?: null | number;
group?: null | string;
notes?: null | string;
created_on: Date;
updated_on?: null | Date;
}
export async function load_ae_obj_li__contact({
api_cfg,
for_obj_type,
for_obj_id,
enabled = 'enabled',
hidden = 'not_hidden',
log_lvl = 0
}: {
api_cfg: any;
for_obj_type?: string;
for_obj_id?: string;
enabled?: 'all' | 'enabled' | 'not_enabled';
hidden?: 'all' | 'hidden' | 'not_hidden';
log_lvl?: number;
}) {
ae_promises.load__contact_obj_li = await api.get_ae_obj_li_v3({
api_cfg,
obj_type: 'contact',
for_obj_type,
for_obj_id,
enabled,
hidden,
log_lvl
}).then(async (result) => {
if (result && Array.isArray(result)) {
const processed = await process_ae_obj__contact_props({ obj_li: result, log_lvl });
await db_save_ae_obj_li__ae_obj({
db_instance: db_core,
table_name: 'contact',
obj_li: processed,
properties_to_save: ['id', 'contact_id', 'contact_id_random', 'name', 'email', 'phone', 'enable'],
log_lvl
});
return result;
}
return [];
});
return ae_promises.load__contact_obj_li;
}
export async function process_ae_obj__contact_props({ obj_li, log_lvl = 0 }: { obj_li: any[], log_lvl?: number }) {
return obj_li.map(obj => {
const new_obj = { ...obj };
new_obj.id = obj.contact_id_random;
return new_obj;
});
}

View File

@@ -173,6 +173,8 @@ export class MySubClassedDexie extends Dexie {
account!: Table<any>;
site!: Table<any>;
site_domain!: Table<any>;
address!: Table<any>;
contact!: Table<any>;
constructor() {
super('ae_core_db');
@@ -219,6 +221,20 @@ export class MySubClassedDexie extends Dexie {
id, site_domain_id, site_domain_id_random,
site_id, site_id_random,
domain,
enable, hide, priority, sort, group, created_on, updated_on`,
address: `
id, address_id, address_id_random,
account_id, account_id_random,
for_type, for_id, for_id_random,
city, state_province, country,
enable, hide, priority, sort, group, created_on, updated_on`,
contact: `
id, contact_id, contact_id_random,
account_id, account_id_random,
for_type, for_id, for_id_random,
name, email, phone,
enable, hide, priority, sort, group, created_on, updated_on`
});
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { ae_loc, ae_sess, ae_api, slct } from '$lib/stores/ae_stores';
import { Building, Globe, Users, ShieldCheck, List, LayoutDashboard } from 'lucide-svelte';
import { Building, Globe, Users, ShieldCheck, List, LayoutDashboard, MapPin, Phone } from 'lucide-svelte';
interface Props {
data: any;
@@ -31,6 +31,12 @@
<a href="/core/people" class="btn btn-sm variant-soft-warning">
<Users size={14} class="mr-1" /> People
</a>
<a href="/core/addresses" class="btn btn-sm variant-soft-surface">
<MapPin size={14} class="mr-1" /> Addresses
</a>
<a href="/core/contacts" class="btn btn-sm variant-soft-surface">
<Phone size={14} class="mr-1" /> Contacts
</a>
<a href="/core/lookups" class="btn btn-sm variant-soft-surface">
<List size={14} class="mr-1" /> Lookups
</a>

View File

@@ -1,8 +1,16 @@
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageLoad = async ({ parent }) => {
const parentData = await parent();
const data = await parent();
if (!data.account_id) {
console.error('Core Dashboard: No account_id found in parent data');
// We could throw an error here, but for now let's just log it and return
// to avoid a hard crash if the user is just navigating.
}
return {
account_id: parentData.account_id
account_id: data.account_id
};
};
};

View File

@@ -0,0 +1,81 @@
<script lang="ts">
import { onMount } from 'svelte';
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
import { goto } from '$app/navigation';
import { MapPin, Plus, Search } from 'lucide-svelte';
import { load_ae_obj_li__address } from '$lib/ae_core/ae_core__address';
let address_li: any[] = $state([]);
let loading = $state(true);
async function load_addresses() {
if (!$ae_loc.account_id) return;
loading = true;
address_li = await load_ae_obj_li__address({
api_cfg: $ae_api,
for_obj_id: $ae_loc.account_id,
enabled: 'all',
log_lvl: 1
});
loading = false;
}
onMount(() => {
if (!$ae_loc.manager_access) {
goto('/core');
return;
}
load_addresses();
});
</script>
<div class="container mx-auto p-4 space-y-6">
<header class="flex justify-between items-center">
<div class="flex items-center gap-2">
<MapPin size={24} />
<h1 class="h2">Address Management</h1>
</div>
<button class="btn variant-filled-primary" disabled>
<Plus size={16} class="mr-2" /> Add Address
</button>
</header>
{#if loading}
<div class="placeholder animate-pulse h-64 w-full"></div>
{:else if address_li.length === 0}
<div class="card p-8 text-center variant-soft">
<p class="opacity-60">No addresses found for this account.</p>
</div>
{:else}
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th>City</th>
<th>State/Province</th>
<th>Country</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
{#each address_li as addr}
<tr>
<td>{addr.city || '--'}</td>
<td>{addr.state_province || '--'}</td>
<td>{addr.country || '--'}</td>
<td>
<span class="badge {addr.enable ? 'variant-filled-success' : 'variant-filled-error'}">
{addr.enable ? 'Enabled' : 'Disabled'}
</span>
</td>
<td class="text-right">
<button class="btn btn-sm variant-soft-primary" disabled>Manage</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>

View File

@@ -0,0 +1,91 @@
<script lang="ts">
import { onMount } from 'svelte';
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
import { goto } from '$app/navigation';
import { Phone, Plus, Search, Mail, User } from 'lucide-svelte';
import { load_ae_obj_li__contact } from '$lib/ae_core/ae_core__contact';
let contact_li: any[] = $state([]);
let loading = $state(true);
async function load_contacts() {
if (!$ae_loc.account_id) return;
loading = true;
contact_li = await load_ae_obj_li__contact({
api_cfg: $ae_api,
for_obj_id: $ae_loc.account_id,
enabled: 'all',
log_lvl: 1
});
loading = false;
}
onMount(() => {
if (!$ae_loc.manager_access) {
goto('/core');
return;
}
load_contacts();
});
</script>
<div class="container mx-auto p-4 space-y-6">
<header class="flex justify-between items-center">
<div class="flex items-center gap-2">
<Phone size={24} />
<h1 class="h2">Contact Management</h1>
</div>
<button class="btn variant-filled-primary" disabled>
<Plus size={16} class="mr-2" /> Add Contact
</button>
</header>
{#if loading}
<div class="placeholder animate-pulse h-64 w-full"></div>
{:else if contact_li.length === 0}
<div class="card p-8 text-center variant-soft">
<p class="opacity-60">No contacts found for this account.</p>
</div>
{:else}
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
{#each contact_li as con}
<tr>
<td>
<div class="flex items-center gap-2">
<User size={14} class="opacity-60" />
{con.name || '--'}
</div>
</td>
<td>
<div class="flex items-center gap-2">
<Mail size={14} class="opacity-60" />
{con.email || '--'}
</div>
</td>
<td>{con.phone || '--'}</td>
<td>
<span class="badge {con.enable ? 'variant-filled-success' : 'variant-filled-error'}">
{con.enable ? 'Enabled' : 'Disabled'}
</span>
</td>
<td class="text-right">
<button class="btn btn-sm variant-soft-primary" disabled>Manage</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>

View File

@@ -28,7 +28,9 @@
import Person_view from './../../person_view.svelte';
import { load_ae_obj_li__user } from '$lib/ae_core/ae_core__user';
import { update_ae_obj__person } from '$lib/ae_core/ae_core__person';
import { Users, Link, Unlink, UserPlus, ShieldCheck, User } from 'lucide-svelte';
import { qry_ae_obj_li__event } from '$lib/ae_events/ae_events__event';
import { load_ae_obj_li__post } from '$lib/ae_posts/ae_posts__post';
import { Users, Link, Unlink, UserPlus, ShieldCheck, User, Calendar, MessageSquare, History } from 'lucide-svelte';
interface Props {
data: any;
@@ -54,6 +56,36 @@
let loading_users = $state(false);
let show_link_ui = $state(false);
let related_events: any[] = $state([]);
let related_posts: any[] = $state([]);
let loading_activity = $state(false);
async function load_activity() {
if (!$slct.person_id) return;
loading_activity = true;
// Load related data using search queries
// Assuming person_id_random is the field name in these objects
const [events, posts] = await Promise.all([
qry_ae_obj_li__event({
api_cfg: $ae_api,
for_obj_id: $ae_loc.account_id,
params: { person_id_random: $slct.person_id },
log_lvl: 1
}),
load_ae_obj_li__post({
api_cfg: $ae_api,
for_obj_id: $ae_loc.account_id,
params: { person_id_random: $slct.person_id },
log_lvl: 1
})
]);
related_events = events || [];
related_posts = posts || [];
loading_activity = false;
}
async function load_unlinked_users() {
if (!$ae_loc.manager_access) return;
loading_users = true;
@@ -95,6 +127,14 @@
});
}
onMount(() => {
if (!$ae_loc.manager_access) {
goto('/core');
return;
}
load_activity();
});
if (!$ae_loc.person) {
$ae_loc.person = {};
}
@@ -246,6 +286,61 @@
</div>
{/if}
</div>
<!-- Activity & Content Section -->
<div class="card p-4 variant-soft-surface space-y-4 mx-4">
<header class="flex items-center gap-2 font-bold border-b border-surface-500/30 pb-2">
<History size={18} />
<span>Linked Activity & Content</span>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 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">
<Calendar size={16} /> Related Events
</h4>
{#if loading_activity}
<div class="placeholder animate-pulse h-20 w-full"></div>
{:else if related_events.length === 0}
<p class="text-sm italic opacity-50">No related events found.</p>
{:else}
<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>
</a>
{/each}
</div>
{/if}
</div>
<!-- Related Posts -->
<div class="space-y-3">
<h4 class="h4 flex items-center gap-2 text-sm opacity-70 uppercase tracking-wider font-bold">
<MessageSquare size={16} /> Related Posts
</h4>
{#if loading_activity}
<div class="placeholder animate-pulse h-20 w-full"></div>
{:else if related_posts.length === 0}
<p class="text-sm italic opacity-50">No related posts found.</p>
{: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">
<span>{new Date(post.created_on).toLocaleDateString()}</span>
<span class="badge variant-soft-surface">{post.post_comment_count || 0} comments</span>
</div>
</a>
{/each}
</div>
{/if}
</div>
</div>
</div>
{/if}
{#if !$lq__person_obj}