Making the code easier to read and more consistent.

This commit is contained in:
Scott Idem
2026-03-24 12:05:22 -04:00
parent 94849137f0
commit 9a75243d9c
29 changed files with 4096 additions and 2281 deletions

View File

@@ -1,50 +1,67 @@
<script lang="ts">
import { onMount } from 'svelte';
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
import { goto } from '$app/navigation';
import { Activity, Contact, ExternalLink, ListFilter, Mail, Phone, Plus, Search, ShieldCheck, User, X } from '@lucide/svelte';
import { load_ae_obj_li__contact, create_ae_obj__contact } from '$lib/ae_core/ae_core__contact';
import Contact_form from './ae_comp__contact_form.svelte';
import { onMount } from 'svelte';
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
import { goto } from '$app/navigation';
import {
Activity,
Contact,
ExternalLink,
ListFilter,
Mail,
Phone,
Plus,
Search,
ShieldCheck,
User,
X
} from '@lucide/svelte';
import {
load_ae_obj_li__contact,
create_ae_obj__contact
} from '$lib/ae_core/ae_core__contact';
import Contact_form from './ae_comp__contact_form.svelte';
let contact_li: any[] = $state([]);
let qry_str = $state('');
let filtered_li: any[] = $derived(
qry_str
? contact_li.filter(c =>
c.name?.toLowerCase().includes(qry_str.toLowerCase()) ||
c.title?.toLowerCase().includes(qry_str.toLowerCase()) ||
c.email?.toLowerCase().includes(qry_str.toLowerCase()) ||
c.phone_office?.toLowerCase().includes(qry_str.toLowerCase())
)
: contact_li
);
let loading = $state(true);
let show_add_form = $state(false);
let contact_li: any[] = $state([]);
let qry_str = $state('');
let filtered_li: any[] = $derived(
qry_str
? contact_li.filter(
(c) =>
c.name?.toLowerCase().includes(qry_str.toLowerCase()) ||
c.title?.toLowerCase().includes(qry_str.toLowerCase()) ||
c.email?.toLowerCase().includes(qry_str.toLowerCase()) ||
c.phone_office?.toLowerCase().includes(qry_str.toLowerCase())
)
: contact_li
);
let loading = $state(true);
let show_add_form = $state(false);
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',
hidden: 'all',
log_lvl: 1
});
loading = false;
}
onMount(() => {
if (!$ae_loc.manager_access) {
goto('/core');
return;
}
load_contacts();
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',
hidden: '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 bg-surface-50-900-token p-4 rounded-xl shadow-lg border border-surface-500/10">
<header
class="flex justify-between items-center bg-surface-50-900-token p-4 rounded-xl shadow-lg border border-surface-500/10">
<div class="flex items-center gap-2">
<div class="p-2 bg-primary-500/10 rounded-lg">
<Phone size={24} class="text-primary-500" />
@@ -53,8 +70,7 @@
</div>
<button
class="btn btn-sm preset-filled-primary font-bold shadow-lg"
onclick={() => show_add_form = !show_add_form}
>
onclick={() => (show_add_form = !show_add_form)}>
{#if show_add_form}
<X size={16} class="mr-2" /> Cancel
{:else}
@@ -73,29 +89,36 @@
goto(`/core/contacts/${new_con.contact_id_random}`);
}
}}
onCancel={() => show_add_form = false}
/>
onCancel={() => (show_add_form = false)} />
</div>
{/if}
<div class="card p-6 shadow-xl preset-tonal-surface border border-surface-500/10">
<div
class="card p-6 shadow-xl preset-tonal-surface border border-surface-500/10">
<div class="max-w-2xl space-y-1">
<span class="label block text-xs font-bold opacity-75 uppercase tracking-wider ml-1">Search Directory</span>
<div class="flex bg-surface-200-700-token rounded-lg overflow-hidden border border-surface-500/20 shadow-inner group focus-within:ring-2 focus-within:ring-primary-500/50 transition-all">
<div class="flex items-center justify-center px-4 bg-surface-300-600-token border-r border-surface-500/20">
<span
class="label block text-xs font-bold opacity-75 uppercase tracking-wider ml-1"
>Search Directory</span>
<div
class="flex bg-surface-200-700-token rounded-lg overflow-hidden border border-surface-500/20 shadow-inner group focus-within:ring-2 focus-within:ring-primary-500/50 transition-all">
<div
class="flex items-center justify-center px-4 bg-surface-300-600-token border-r border-surface-500/20">
<Search size={18} class="opacity-50" />
</div>
<input
class="bg-transparent border-0 ring-0 focus:ring-0 p-3 grow placeholder:opacity-50"
type="search"
bind:value={qry_str}
placeholder="Search by name, title, email, or phone..."
/>
<button class="preset-filled-primary font-bold px-10 py-3 hover:brightness-110 transition-all border-l border-surface-500/20 flex items-center justify-center min-w-[100px]" onclick={load_contacts} disabled={loading}>
placeholder="Search by name, title, email, or phone..." />
<button
class="preset-filled-primary font-bold px-10 py-3 hover:brightness-110 transition-all border-l border-surface-500/20 flex items-center justify-center min-w-[100px]"
onclick={load_contacts}
disabled={loading}>
{#if loading}
<span class="animate-spin text-xl"></span>
{:else}
<span class="whitespace-nowrap tracking-wide">Refresh</span>
<span class="whitespace-nowrap tracking-wide"
>Refresh</span>
{/if}
</button>
</div>
@@ -104,53 +127,82 @@
{#if loading}
<div class="card p-8 flex justify-center items-center h-64">
<div class="placeholder animate-pulse w-full h-full rounded-2xl"></div>
<div class="placeholder animate-pulse w-full h-full rounded-2xl">
</div>
</div>
{:else if filtered_li.length === 0}
<div class="card p-12 text-center preset-tonal-surface border-2 border-dashed border-surface-500/20 rounded-2xl">
<div
class="card p-12 text-center preset-tonal-surface border-2 border-dashed border-surface-500/20 rounded-2xl">
<Contact size={48} class="mx-auto mb-4 opacity-20" />
<h3 class="h3 font-bold opacity-50">No Contacts Found</h3>
<p class="opacity-60 max-w-xs mx-auto mt-2">Business and support contacts will appear here.</p>
<p class="opacity-60 max-w-xs mx-auto mt-2">
Business and support contacts will appear here.
</p>
</div>
{:else}
<div class="card p-6 preset-tonal-surface shadow-xl border border-surface-500/10">
<h3 class="h4 font-bold border-b border-surface-500/30 pb-2 mb-6 flex items-center gap-2">
<div
class="card p-6 preset-tonal-surface shadow-xl border border-surface-500/10">
<h3
class="h4 font-bold border-b border-surface-500/30 pb-2 mb-6 flex items-center gap-2">
<ListFilter size={18} class="text-secondary-500" />
Directory Results
<span class="badge preset-tonal-secondary ml-auto">{filtered_li.length} entries</span>
<span class="badge preset-tonal-secondary ml-auto"
>{filtered_li.length} entries</span>
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each filtered_li as con (con.contact_id_random)}
<div class="card p-5 space-y-4 preset-tonal-surface shadow-md border border-surface-500/10 hover:border-primary-500/30 transition-all group relative">
<div
class="card p-5 space-y-4 preset-tonal-surface shadow-md border border-surface-500/10 hover:border-primary-500/30 transition-all group relative">
<div class="absolute top-4 right-4">
<span class="badge {con.enable ? 'preset-filled-success' : 'preset-filled-error'} text-[8px] uppercase font-bold shadow-sm">
<span
class="badge {con.enable
? 'preset-filled-success'
: 'preset-filled-error'} text-[8px] uppercase font-bold shadow-sm">
{con.enable ? 'Active' : 'Disabled'}
</span>
</div>
<header class="flex items-center gap-3">
<div class="avatar preset-filled-primary w-12 h-12 flex items-center justify-center rounded-full shadow-inner group-hover:scale-110 transition-transform">
<div
class="avatar preset-filled-primary w-12 h-12 flex items-center justify-center rounded-full shadow-inner group-hover:scale-110 transition-transform">
<User size={24} />
</div>
<div class="pr-12">
<p class="font-black tracking-tight truncate">{con.name || con.title || '--'}</p>
<p class="text-[10px] uppercase font-bold opacity-50 truncate">{con.title || 'Support Contact'}</p>
<p class="font-black tracking-tight truncate">
{con.name || con.title || '--'}
</p>
<p
class="text-[10px] uppercase font-bold opacity-50 truncate">
{con.title || 'Support Contact'}
</p>
</div>
</header>
<div class="space-y-2 text-xs opacity-70">
<div class="flex items-center gap-2 bg-black/5 p-2 rounded-lg">
<Mail size={14} class="text-primary-500 shrink-0" />
<span class="truncate">{con.email || 'No Email'}</span>
<div
class="flex items-center gap-2 bg-black/5 p-2 rounded-lg">
<Mail
size={14}
class="text-primary-500 shrink-0" />
<span class="truncate"
>{con.email || 'No Email'}</span>
</div>
<div class="flex items-center gap-2 bg-black/5 p-2 rounded-lg">
<Phone size={14} class="text-secondary-500 shrink-0" />
<span class="truncate">{con.phone_office || con.phone_mobile || '--'}</span>
<div
class="flex items-center gap-2 bg-black/5 p-2 rounded-lg">
<Phone
size={14}
class="text-secondary-500 shrink-0" />
<span class="truncate"
>{con.phone_office ||
con.phone_mobile ||
'--'}</span>
</div>
</div>
<a class="btn btn-sm preset-filled-primary font-bold w-full shadow-lg group-hover:brightness-110 transition-all" href="/core/contacts/{con.contact_id_random}">
<a
class="btn btn-sm preset-filled-primary font-bold w-full shadow-lg group-hover:brightness-110 transition-all"
href="/core/contacts/{con.contact_id_random}">
Manage Contact
</a>
</div>

View File

@@ -1,56 +1,75 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import {
load_ae_obj_id__contact,
update_ae_obj__contact,
delete_ae_obj_id__contact
} from '$lib/ae_core/ae_core__contact';
import { editable_fields__contact } from '$lib/ae_core/ae_core__contact.editable_fields';
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { goto } from '$app/navigation';
import { Activity, ArrowLeft, Contact, Edit, Eye, Globe, Info, Link2, Linkedin, Mail, Phone, Save, ShieldCheck, Trash2, UserRound } from '@lucide/svelte';
import Contact_form from '../ae_comp__contact_form.svelte';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import {
load_ae_obj_id__contact,
update_ae_obj__contact,
delete_ae_obj_id__contact
} from '$lib/ae_core/ae_core__contact';
import { editable_fields__contact } from '$lib/ae_core/ae_core__contact.editable_fields';
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { goto } from '$app/navigation';
import {
Activity,
ArrowLeft,
Contact,
Edit,
Eye,
Globe,
Info,
Link2,
Linkedin,
Mail,
Phone,
Save,
ShieldCheck,
Trash2,
UserRound
} from '@lucide/svelte';
import Contact_form from '../ae_comp__contact_form.svelte';
let contact_id = $derived($page.params.contact_id ?? '');
let contact: any = $state(null);
let loading = $state(true);
let is_editing = $state(false);
let contact_id = $derived($page.params.contact_id ?? '');
let contact: any = $state(null);
let loading = $state(true);
let is_editing = $state(false);
async function load_data() {
loading = true;
contact = await load_ae_obj_id__contact({
api_cfg: $ae_api,
contact_id,
log_lvl: 1
});
loading = false;
}
onMount(() => {
if (!$ae_loc.manager_access) {
goto('/core');
return;
}
load_data();
async function load_data() {
loading = true;
contact = await load_ae_obj_id__contact({
api_cfg: $ae_api,
contact_id,
log_lvl: 1
});
loading = false;
}
async function handle_delete() {
if (!confirm('Permanently delete this contact?')) return;
await delete_ae_obj_id__contact({
api_cfg: $ae_api,
contact_id,
method: 'delete',
log_lvl: 1
});
goto('/core/contacts');
onMount(() => {
if (!$ae_loc.manager_access) {
goto('/core');
return;
}
load_data();
});
async function handle_delete() {
if (!confirm('Permanently delete this contact?')) return;
await delete_ae_obj_id__contact({
api_cfg: $ae_api,
contact_id,
method: 'delete',
log_lvl: 1
});
goto('/core/contacts');
}
</script>
<div class="container mx-auto p-4 space-y-6">
<header class="flex flex-wrap justify-between items-center bg-surface-50-900-token p-4 rounded-xl shadow-lg border border-surface-500/10 gap-4">
<header
class="flex flex-wrap justify-between items-center bg-surface-50-900-token p-4 rounded-xl shadow-lg border border-surface-500/10 gap-4">
<div class="flex items-center gap-4">
<a class="btn btn-sm preset-tonal-surface shadow-sm" href="/core/contacts">
<a
class="btn btn-sm preset-tonal-surface shadow-sm"
href="/core/contacts">
<ArrowLeft size={16} />
</a>
<div class="flex items-center gap-3">
@@ -58,20 +77,31 @@
<UserRound size={24} class="text-primary-500" />
</div>
<div>
<h1 class="h2 font-black tracking-tight">{contact?.name || contact?.title || 'Loading...'}</h1>
<p class="text-xs font-bold opacity-50 uppercase tracking-widest">Contact Detail</p>
<h1 class="h2 font-black tracking-tight">
{contact?.name || contact?.title || 'Loading...'}
</h1>
<p
class="text-xs font-bold opacity-50 uppercase tracking-widest">
Contact Detail
</p>
</div>
</div>
</div>
<div class="flex gap-2">
<button class="btn btn-sm preset-tonal-secondary font-bold shadow-sm" onclick={() => is_editing = !is_editing} disabled={loading}>
<button
class="btn btn-sm preset-tonal-secondary font-bold shadow-sm"
onclick={() => (is_editing = !is_editing)}
disabled={loading}>
{#if is_editing}
<Eye size={16} class="mr-2" /> View Mode
{:else}
<Edit size={16} class="mr-2" /> Edit Mode
{/if}
</button>
<button class="btn btn-sm preset-tonal-error font-bold shadow-sm" onclick={handle_delete} disabled={loading}>
<button
class="btn btn-sm preset-tonal-error font-bold shadow-sm"
onclick={handle_delete}
disabled={loading}>
<Trash2 size={16} class="mr-2" /> Delete
</button>
</div>
@@ -79,54 +109,74 @@
{#if loading}
<div class="card p-8 flex justify-center items-center h-64">
<div class="placeholder animate-pulse w-full h-full rounded-2xl"></div>
<div class="placeholder animate-pulse w-full h-full rounded-2xl">
</div>
</div>
{:else if contact}
{#if is_editing}
<div class="animate-fade-in">
<Contact_form
{contact}
<Contact_form
{contact}
onSave={(updated) => {
contact = updated;
is_editing = false;
}}
onCancel={() => is_editing = false}
/>
onCancel={() => (is_editing = false)} />
</div>
{:else}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 animate-fade-in">
<div class="lg:col-span-2 space-y-6">
<div class="card p-6 shadow-xl preset-tonal-surface border border-surface-500/10 space-y-6">
<h3 class="h4 font-bold border-b border-surface-500/30 pb-2 flex items-center gap-2">
<div
class="card p-6 shadow-xl preset-tonal-surface border border-surface-500/10 space-y-6">
<h3
class="h4 font-bold border-b border-surface-500/30 pb-2 flex items-center gap-2">
<Contact size={20} class="text-primary-500" />
Core Information
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-1">
<p class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<p
class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<UserRound size={10} /> Full Name / Title
</p>
<p class="text-lg font-black tracking-tight leading-tight">{contact.name || contact.title || '--'}</p>
<p
class="text-lg font-black tracking-tight leading-tight">
{contact.name || contact.title || '--'}
</p>
</div>
<div class="space-y-1">
<p class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<p
class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<Activity size={10} /> Tagline / Role
</p>
<p class="font-bold">{contact.tagline || '--'}</p>
<p class="font-bold">
{contact.tagline || '--'}
</p>
</div>
<div class="space-y-1 bg-black/5 p-4 rounded-xl border border-surface-500/10">
<p class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<div
class="space-y-1 bg-black/5 p-4 rounded-xl border border-surface-500/10">
<p
class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<Mail size={10} /> Email Address
</p>
<p class="font-bold text-primary-500 break-all">{contact.email || '--'}</p>
<p class="font-bold text-primary-500 break-all">
{contact.email || '--'}
</p>
</div>
<div class="space-y-1 bg-black/5 p-4 rounded-xl border border-surface-500/10">
<p class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<div
class="space-y-1 bg-black/5 p-4 rounded-xl border border-surface-500/10">
<p
class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<Globe size={10} /> Website
</p>
{#if contact.website_url}
<a href={contact.website_url} target="_blank" rel="noopener noreferrer" class="font-bold text-secondary-500 hover:underline flex items-center gap-2 truncate">
{contact.website_url} <Link2 size={12} />
<a
href={contact.website_url}
target="_blank"
rel="noopener noreferrer"
class="font-bold text-secondary-500 hover:underline flex items-center gap-2 truncate">
{contact.website_url}
<Link2 size={12} />
</a>
{:else}
<p class="font-bold">--</p>
@@ -135,36 +185,52 @@
</div>
</div>
<div class="card p-6 shadow-xl preset-tonal-surface border border-surface-500/10 space-y-6">
<h3 class="h4 font-bold border-b border-surface-500/30 pb-2 flex items-center gap-2">
<div
class="card p-6 shadow-xl preset-tonal-surface border border-surface-500/10 space-y-6">
<h3
class="h4 font-bold border-b border-surface-500/30 pb-2 flex items-center gap-2">
<Phone size={20} class="text-secondary-500" />
Communication & Social
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-1">
<p class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<p
class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<Phone size={10} /> Mobile Phone
</p>
<p class="font-mono font-bold">{contact.phone_mobile || contact.phone || '--'}</p>
<p class="font-mono font-bold">
{contact.phone_mobile ||
contact.phone ||
'--'}
</p>
</div>
<div class="space-y-1">
<p class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<p
class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<Phone size={10} /> Office Phone
</p>
<p class="font-mono font-bold">{contact.phone_office || '--'}</p>
<p class="font-mono font-bold">
{contact.phone_office || '--'}
</p>
</div>
<div class="space-y-1">
<p class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<p
class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<Linkedin size={10} /> LinkedIn
</p>
<p class="font-bold truncate">{contact.linkedin_url || '--'}</p>
<p class="font-bold truncate">
{contact.linkedin_url || '--'}
</p>
</div>
<div class="md:col-span-2 space-y-2">
<p class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<p
class="text-[10px] opacity-60 uppercase font-black tracking-widest flex items-center gap-1">
<Info size={10} /> Internal Notes
</p>
<div class="p-4 bg-black/5 rounded-xl border border-dashed border-surface-500/20 italic opacity-80 min-h-[80px]">
{contact.notes || 'No internal notes provided for this contact.'}
<div
class="p-4 bg-black/5 rounded-xl border border-dashed border-surface-500/20 italic opacity-80 min-h-[80px]">
{contact.notes ||
'No internal notes provided for this contact.'}
</div>
</div>
</div>
@@ -172,40 +238,77 @@
</div>
<div class="space-y-6">
<div class="card p-6 shadow-xl preset-tonal-surface border border-surface-500/10 space-y-6">
<h3 class="h4 font-bold border-b border-surface-500/30 pb-2 flex items-center gap-2">
<div
class="card p-6 shadow-xl preset-tonal-surface border border-surface-500/10 space-y-6">
<h3
class="h4 font-bold border-b border-surface-500/30 pb-2 flex items-center gap-2">
<ShieldCheck size={20} class="text-warning-500" />
Status & Flags
</h3>
<div class="space-y-4">
<div class="flex justify-between items-center p-3 bg-black/5 rounded-lg">
<span class="text-sm font-bold opacity-75">Enabled</span>
<span class="badge {contact.enable ? 'preset-filled-success' : 'preset-filled-error'} px-4 py-1 shadow-sm">
<div
class="flex justify-between items-center p-3 bg-black/5 rounded-lg">
<span class="text-sm font-bold opacity-75"
>Enabled</span>
<span
class="badge {contact.enable
? 'preset-filled-success'
: 'preset-filled-error'} px-4 py-1 shadow-sm">
{contact.enable ? 'ACTIVE' : 'DISABLED'}
</span>
</div>
<div class="flex justify-between items-center p-3 bg-black/5 rounded-lg">
<span class="text-sm font-bold opacity-75">Hidden</span>
<span class="badge {contact.hide ? 'preset-filled-warning' : 'preset-filled-surface'} px-4 py-1 shadow-sm">
<div
class="flex justify-between items-center p-3 bg-black/5 rounded-lg">
<span class="text-sm font-bold opacity-75"
>Hidden</span>
<span
class="badge {contact.hide
? 'preset-filled-warning'
: 'preset-filled-surface'} px-4 py-1 shadow-sm">
{contact.hide ? 'YES' : 'NO'}
</span>
</div>
<div class="flex justify-between items-center p-3 bg-black/5 rounded-lg">
<span class="text-sm font-bold opacity-75">Priority</span>
<span class="badge {contact.priority ? 'preset-filled-secondary' : 'preset-filled-surface'} px-4 py-1 shadow-sm">
<div
class="flex justify-between items-center p-3 bg-black/5 rounded-lg">
<span class="text-sm font-bold opacity-75"
>Priority</span>
<span
class="badge {contact.priority
? 'preset-filled-secondary'
: 'preset-filled-surface'} px-4 py-1 shadow-sm">
{contact.priority ? 'YES' : 'NO'}
</span>
</div>
</div>
</div>
<div class="card p-5 preset-tonal-surface shadow-inner border border-surface-500/10 space-y-3">
<p class="text-[10px] uppercase font-black opacity-40 tracking-widest border-b border-surface-500/20 pb-1">System Audit</p>
<div
class="card p-5 preset-tonal-surface shadow-inner border border-surface-500/10 space-y-3">
<p
class="text-[10px] uppercase font-black opacity-40 tracking-widest border-b border-surface-500/20 pb-1">
System Audit
</p>
<div class="space-y-2 text-[10px] font-mono opacity-60">
<p class="flex justify-between"><span>ID:</span> <span class="text-primary-500 font-bold">{contact.contact_id_random}</span></p>
<p class="flex justify-between"><span>Created:</span> <span>{new Date(contact.created_on).toLocaleString()}</span></p>
<p class="flex justify-between">
<span>ID:</span>
<span class="text-primary-500 font-bold"
>{contact.contact_id_random}</span>
</p>
<p class="flex justify-between">
<span>Created:</span>
<span
>{new Date(
contact.created_on
).toLocaleString()}</span>
</p>
{#if contact.updated_on}
<p class="flex justify-between"><span>Updated:</span> <span>{new Date(contact.updated_on).toLocaleString()}</span></p>
<p class="flex justify-between">
<span>Updated:</span>
<span
>{new Date(
contact.updated_on
).toLocaleString()}</span>
</p>
{/if}
</div>
</div>

View File

@@ -1,122 +1,144 @@
<script lang="ts">
/**
* Contact Form Component
* Standardized 2026-01-09 for Core UI Polish.
* Uses unified ae_Contact type and Svelte 5 Runes.
*/
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { update_ae_obj__contact, create_ae_obj__contact } from '$lib/ae_core/ae_core__contact';
import type { ae_Contact } from '$lib/types/ae_types';
import { Facebook, Globe, Instagram, Linkedin, Mail, Phone, Save, UserPlus, X } from '@lucide/svelte';
interface Props {
contact?: ae_Contact | null;
onSave?: (contact: ae_Contact) => void;
onCancel?: () => void;
}
/**
* Contact Form Component
* Standardized 2026-01-09 for Core UI Polish.
* Uses unified ae_Contact type and Svelte 5 Runes.
*/
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import {
update_ae_obj__contact,
create_ae_obj__contact
} from '$lib/ae_core/ae_core__contact';
import type { ae_Contact } from '$lib/types/ae_types';
import {
Facebook,
Globe,
Instagram,
Linkedin,
Mail,
Phone,
Save,
UserPlus,
X
} from '@lucide/svelte';
interface Props {
contact?: ae_Contact | null;
onSave?: (contact: ae_Contact) => void;
onCancel?: () => void;
}
let { contact = null, onSave, onCancel }: Props = $props();
let { contact = null, onSave, onCancel }: Props = $props();
// Form State (Runes)
let formData = $state({
title: '',
tagline: '',
email: '',
phone_mobile: '',
phone_office: '',
website_url: '',
facebook_url: '',
instagram_url: '',
linkedin_url: '',
notes: '',
enable: true,
hide: false,
priority: false
});
// Form State (Runes)
let formData = $state({
title: '',
tagline: '',
email: '',
phone_mobile: '',
phone_office: '',
website_url: '',
facebook_url: '',
instagram_url: '',
linkedin_url: '',
notes: '',
enable: true,
hide: false,
priority: false
});
// Reset form when contact prop changes
$effect(() => {
formData.title = contact?.title ?? '';
formData.tagline = contact?.tagline ?? '';
formData.email = contact?.email ?? '';
formData.phone_mobile = contact?.phone_mobile ?? '';
formData.phone_office = contact?.phone_office ?? '';
formData.website_url = contact?.website_url ?? '';
formData.facebook_url = contact?.facebook_url ?? '';
formData.instagram_url = contact?.instagram_url ?? '';
formData.linkedin_url = contact?.linkedin_url ?? '';
formData.notes = contact?.notes ?? '';
formData.enable = contact?.enable ?? true;
formData.hide = contact?.hide ?? false;
formData.priority = contact?.priority ?? false;
});
// Reset form when contact prop changes
$effect(() => {
formData.title = contact?.title ?? '';
formData.tagline = contact?.tagline ?? '';
formData.email = contact?.email ?? '';
formData.phone_mobile = contact?.phone_mobile ?? '';
formData.phone_office = contact?.phone_office ?? '';
formData.website_url = contact?.website_url ?? '';
formData.facebook_url = contact?.facebook_url ?? '';
formData.instagram_url = contact?.instagram_url ?? '';
formData.linkedin_url = contact?.linkedin_url ?? '';
formData.notes = contact?.notes ?? '';
formData.enable = contact?.enable ?? true;
formData.hide = contact?.hide ?? false;
formData.priority = contact?.priority ?? false;
});
let is_loading = $state(false);
let error_msg = $state('');
let is_loading = $state(false);
let error_msg = $state('');
async function handleSubmit(event: Event) {
event.preventDefault();
is_loading = true;
error_msg = '';
async function handleSubmit(event: Event) {
event.preventDefault();
is_loading = true;
error_msg = '';
// Surgical Payload
const payload: any = { ...formData };
for (const key in payload) {
if (typeof payload[key] === 'string' && payload[key].trim() === '') {
// title is likely required, but we'll trim it
if (key === 'title') {
payload[key] = payload[key].trim();
} else {
payload[key] = null;
}
// Surgical Payload
const payload: any = { ...formData };
for (const key in payload) {
if (typeof payload[key] === 'string' && payload[key].trim() === '') {
// title is likely required, but we'll trim it
if (key === 'title') {
payload[key] = payload[key].trim();
} else {
payload[key] = null;
}
}
try {
let result;
if (contact?.contact_id_random) {
// Update existing
result = await update_ae_obj__contact({
api_cfg: $ae_api,
contact_id: contact.contact_id_random,
data_kv: payload,
log_lvl: 1
});
} else {
// Create new
result = await create_ae_obj__contact({
api_cfg: $ae_api,
account_id: $ae_loc.account_id,
data_kv: payload,
log_lvl: 1
});
}
if (result) {
if (onSave) onSave(result);
} else {
error_msg = 'Failed to save contact record.';
}
} catch (err: any) {
error_msg = err.message || 'An error occurred while saving.';
} finally {
is_loading = false;
}
}
try {
let result;
if (contact?.contact_id_random) {
// Update existing
result = await update_ae_obj__contact({
api_cfg: $ae_api,
contact_id: contact.contact_id_random,
data_kv: payload,
log_lvl: 1
});
} else {
// Create new
result = await create_ae_obj__contact({
api_cfg: $ae_api,
account_id: $ae_loc.account_id,
data_kv: payload,
log_lvl: 1
});
}
if (result) {
if (onSave) onSave(result);
} else {
error_msg = 'Failed to save contact record.';
}
} catch (err: any) {
error_msg = err.message || 'An error occurred while saving.';
} finally {
is_loading = false;
}
}
</script>
<form onsubmit={handleSubmit} class="card p-6 space-y-6 shadow-xl preset-tonal-surface">
<header class="flex justify-between items-center border-b border-surface-500/30 pb-4">
<form
onsubmit={handleSubmit}
class="card p-6 space-y-6 shadow-xl preset-tonal-surface">
<header
class="flex justify-between items-center border-b border-surface-500/30 pb-4">
<h3 class="h3 flex items-center gap-2">
<UserPlus size={24} />
{contact ? 'Edit Contact' : 'Create New Contact'}
</h3>
<div class="flex gap-2">
{#if onCancel}
<button type="button" class="btn btn-sm preset-tonal-surface" onclick={onCancel}>
<button
type="button"
class="btn btn-sm preset-tonal-surface"
onclick={onCancel}>
<X size={16} class="mr-1" /> Cancel
</button>
{/if}
<button type="submit" class="btn btn-sm preset-filled-primary font-bold shadow-lg" disabled={is_loading}>
<button
type="submit"
class="btn btn-sm preset-filled-primary font-bold shadow-lg"
disabled={is_loading}>
{#if is_loading}
<span class="animate-spin mr-2"></span>
{:else}
@@ -138,82 +160,163 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Identity Section -->
<fieldset class="space-y-4">
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Identity & Branding</legend>
<legend
class="text-sm font-bold uppercase tracking-widest opacity-60"
>Identity & Branding</legend>
<div class="space-y-1">
<label class="label text-xs font-bold opacity-75" for="contact-title">Title / Name</label>
<input class="input preset-filled-surface rounded-lg placeholder-surface-400 p-2" id="contact-title" type="text" bind:value={formData.title} required placeholder="Business Office" />
<label
class="label text-xs font-bold opacity-75"
for="contact-title">Title / Name</label>
<input
class="input preset-filled-surface rounded-lg placeholder-surface-400 p-2"
id="contact-title"
type="text"
bind:value={formData.title}
required
placeholder="Business Office" />
</div>
<div class="space-y-1">
<label class="label text-xs font-bold opacity-75" for="contact-tagline">Tagline</label>
<input class="input preset-filled-surface rounded-lg placeholder-surface-400 p-2" id="contact-tagline" type="text" bind:value={formData.tagline} placeholder="Primary contact for business inquiries" />
<label
class="label text-xs font-bold opacity-75"
for="contact-tagline">Tagline</label>
<input
class="input preset-filled-surface rounded-lg placeholder-surface-400 p-2"
id="contact-tagline"
type="text"
bind:value={formData.tagline}
placeholder="Primary contact for business inquiries" />
</div>
<div class="space-y-1">
<label class="label text-xs font-bold opacity-75" for="contact-email">Email Address</label>
<div class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<label
class="label text-xs font-bold opacity-75"
for="contact-email">Email Address</label>
<div
class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<div class="input-group-shim"><Mail size={16} /></div>
<input class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2" id="contact-email" type="email" bind:value={formData.email} placeholder="contact@example.com" />
<input
class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2"
id="contact-email"
type="email"
bind:value={formData.email}
placeholder="contact@example.com" />
</div>
</div>
</fieldset>
<!-- Communication Section -->
<fieldset class="space-y-4">
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Phone & Web</legend>
<legend
class="text-sm font-bold uppercase tracking-widest opacity-60"
>Phone & Web</legend>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<label class="label text-xs font-bold opacity-75" for="contact-phone-mobile">Mobile Phone</label>
<div class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<label
class="label text-xs font-bold opacity-75"
for="contact-phone-mobile">Mobile Phone</label>
<div
class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<div class="input-group-shim"><Phone size={16} /></div>
<input class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2" id="contact-phone-mobile" type="tel" bind:value={formData.phone_mobile} placeholder="+1..." />
<input
class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2"
id="contact-phone-mobile"
type="tel"
bind:value={formData.phone_mobile}
placeholder="+1..." />
</div>
</div>
<div class="space-y-1">
<label class="label text-xs font-bold opacity-75" for="contact-phone-office">Office Phone</label>
<div class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<label
class="label text-xs font-bold opacity-75"
for="contact-phone-office">Office Phone</label>
<div
class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<div class="input-group-shim"><Phone size={16} /></div>
<input class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2" id="contact-phone-office" type="tel" bind:value={formData.phone_office} placeholder="+1..." />
<input
class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2"
id="contact-phone-office"
type="tel"
bind:value={formData.phone_office}
placeholder="+1..." />
</div>
</div>
</div>
<div class="space-y-1">
<label class="label text-xs font-bold opacity-75" for="contact-website">Website URL</label>
<div class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<label
class="label text-xs font-bold opacity-75"
for="contact-website">Website URL</label>
<div
class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<div class="input-group-shim"><Globe size={16} /></div>
<input class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2" id="contact-website" type="url" bind:value={formData.website_url} placeholder="https://..." />
<input
class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2"
id="contact-website"
type="url"
bind:value={formData.website_url}
placeholder="https://..." />
</div>
</div>
</fieldset>
<!-- Social Media Section -->
<fieldset class="space-y-4">
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Social Media</legend>
<legend
class="text-sm font-bold uppercase tracking-widest opacity-60"
>Social Media</legend>
<div class="space-y-1">
<label class="label text-xs font-bold opacity-75" for="contact-linkedin">LinkedIn URL</label>
<div class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<label
class="label text-xs font-bold opacity-75"
for="contact-linkedin">LinkedIn URL</label>
<div
class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<div class="input-group-shim"><Linkedin size={16} /></div>
<input class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2" id="contact-linkedin" type="url" bind:value={formData.linkedin_url} placeholder="https://linkedin.com/in/..." />
<input
class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2"
id="contact-linkedin"
type="url"
bind:value={formData.linkedin_url}
placeholder="https://linkedin.com/in/..." />
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<label class="label text-xs font-bold opacity-75" for="contact-facebook">Facebook URL</label>
<div class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<div class="input-group-shim"><Facebook size={16} /></div>
<input class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2" id="contact-facebook" type="url" bind:value={formData.facebook_url} placeholder="https://facebook.com/..." />
<label
class="label text-xs font-bold opacity-75"
for="contact-facebook">Facebook URL</label>
<div
class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<div class="input-group-shim">
<Facebook size={16} />
</div>
<input
class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2"
id="contact-facebook"
type="url"
bind:value={formData.facebook_url}
placeholder="https://facebook.com/..." />
</div>
</div>
<div class="space-y-1">
<label class="label text-xs font-bold opacity-75" for="contact-instagram">Instagram URL</label>
<div class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<div class="input-group-shim"><Instagram size={16} /></div>
<input class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2" id="contact-instagram" type="url" bind:value={formData.instagram_url} placeholder="https://instagram.com/..." />
<label
class="label text-xs font-bold opacity-75"
for="contact-instagram">Instagram URL</label>
<div
class="input-group input-group-divider grid-cols-[auto_1fr] preset-filled-surface rounded-lg overflow-hidden">
<div class="input-group-shim">
<Instagram size={16} />
</div>
<input
class="bg-transparent border-0 ring-0 focus:ring-0 placeholder-surface-400 p-2"
id="contact-instagram"
type="url"
bind:value={formData.instagram_url}
placeholder="https://instagram.com/..." />
</div>
</div>
</div>
@@ -221,32 +324,53 @@
<!-- Status Section -->
<fieldset class="space-y-4">
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Status</legend>
<legend
class="text-sm font-bold uppercase tracking-widest opacity-60"
>Status</legend>
<div class="flex flex-wrap gap-4 pt-2">
<label class="flex items-center space-x-2 cursor-pointer">
<input class="checkbox" type="checkbox" bind:checked={formData.enable} />
<input
class="checkbox"
type="checkbox"
bind:checked={formData.enable} />
<span class="text-sm font-medium">Enabled</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input class="checkbox" type="checkbox" bind:checked={formData.hide} />
<input
class="checkbox"
type="checkbox"
bind:checked={formData.hide} />
<span class="text-sm font-medium">Hidden</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input class="checkbox" type="checkbox" bind:checked={formData.priority} />
<input
class="checkbox"
type="checkbox"
bind:checked={formData.priority} />
<span class="text-sm font-medium">Priority</span>
</label>
</div>
<div class="space-y-1">
<label class="label text-xs font-bold opacity-75" for="contact-notes">Internal Notes</label>
<textarea class="textarea preset-filled-surface rounded-lg placeholder-surface-400 p-2" id="contact-notes" rows="2" bind:value={formData.notes} placeholder="Additional details..."></textarea>
<label
class="label text-xs font-bold opacity-75"
for="contact-notes">Internal Notes</label>
<textarea
class="textarea preset-filled-surface rounded-lg placeholder-surface-400 p-2"
id="contact-notes"
rows="2"
bind:value={formData.notes}
placeholder="Additional details..."></textarea>
</div>
</fieldset>
</div>
<footer class="flex justify-end gap-2 border-t border-surface-500/30 pt-4">
<button type="submit" class="btn preset-filled-primary font-bold shadow-lg w-full md:w-auto" disabled={is_loading}>
<button
type="submit"
class="btn preset-filled-primary font-bold shadow-lg w-full md:w-auto"
disabled={is_loading}>
{#if is_loading}
<span class="animate-spin mr-2"></span>
{/if}