Standardize Core UI forms and unify schemas for V3 API compatibility
- Implement new Svelte 5 Person, Address, and Contact form components with surgical payload logic. - Refactor core routes (People, Addresses, Contacts) to support unified Create/Edit workflows. - Update ae_types.ts, db_core.ts, and db_journals.ts to align with V3 backend object models. - Fix type safety issues in Journal history views and refine metadata display. - Migrate person core functions to the newer ae_core__person module.
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
||||
delete_ae_obj_id__person,
|
||||
update_ae_obj__person
|
||||
// db_save_ae_obj_li__person
|
||||
} from '$lib/ae_core/core__person';
|
||||
} from '$lib/ae_core/ae_core__person';
|
||||
|
||||
import {
|
||||
auth_ae_obj__username_password,
|
||||
|
||||
@@ -1,208 +1,91 @@
|
||||
import Dexie, { type Table } from 'dexie';
|
||||
import type {
|
||||
ae_HostedFile,
|
||||
ae_Person,
|
||||
ae_User,
|
||||
ae_Account,
|
||||
ae_Site,
|
||||
ae_SiteDomain,
|
||||
ae_Address,
|
||||
ae_Contact
|
||||
} from '$lib/types/ae_types';
|
||||
|
||||
// li = list
|
||||
// kv = key value list
|
||||
|
||||
// Updated 2025-01-07
|
||||
export interface File {
|
||||
id: string;
|
||||
id_random: string;
|
||||
hosted_file_id: string;
|
||||
hosted_file_id_random: string;
|
||||
|
||||
hash_sha256: string;
|
||||
|
||||
for_type?: string;
|
||||
for_id?: string;
|
||||
for_id_random?: string;
|
||||
|
||||
account_id: string;
|
||||
|
||||
filename: string;
|
||||
extension: string;
|
||||
content_type: string;
|
||||
size: number; // In bytes
|
||||
|
||||
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;
|
||||
|
||||
// Additional fields for convenience (database views)
|
||||
filename_no_ext: string;
|
||||
filename_w_ext: string;
|
||||
// Updated 2026-01-09 - Unified Types
|
||||
export interface File extends ae_HostedFile {
|
||||
// Legacy mapping support
|
||||
content_type?: string;
|
||||
filename_no_ext?: string;
|
||||
filename_w_ext?: string;
|
||||
}
|
||||
|
||||
// Updated 2024-07-17
|
||||
export interface Person {
|
||||
id: string;
|
||||
// id_random: string;
|
||||
person_id: string;
|
||||
person_id_random: string;
|
||||
|
||||
external_id?: string; // This may be semi-random or unique only withing the account.
|
||||
external_sys_id?: string; // Generated by an external system. Ideally this should be something like a UUID. It may be the same as the external_id if nothing given.
|
||||
code?: string; // Not currently used.
|
||||
|
||||
account_id?: string; // Technically this is not required for global users.
|
||||
account_id_random?: string; // Technically this is not required for global users.
|
||||
|
||||
person_profile_id?: null | string;
|
||||
person_profile_id_random?: null | string; // The new table person_profile will be used soon...
|
||||
|
||||
user_id?: string;
|
||||
user_id_random?: string;
|
||||
|
||||
pronouns?: null | string;
|
||||
informal_name?: null | string;
|
||||
title_names?: null | string;
|
||||
given_name: string;
|
||||
middle_name?: null | string;
|
||||
family_name: null | string;
|
||||
designations?: null | string;
|
||||
|
||||
professional_title?: null | string;
|
||||
|
||||
full_name?: string;
|
||||
full_name_override?: null | string; // was called display_name
|
||||
|
||||
affiliations?: null | string;
|
||||
|
||||
primary_email?: string;
|
||||
|
||||
biography?: null | string;
|
||||
|
||||
agree?: null | boolean;
|
||||
comments?: null | string;
|
||||
|
||||
allow_auth_key?: null | boolean; // For sign in without password
|
||||
auth_key?: null | string; // Should this be saved locally?
|
||||
passcode?: null | string;
|
||||
|
||||
data_json?: null | 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;
|
||||
|
||||
// Generated fields for sorting locally only
|
||||
tmp_sort_1?: null | string;
|
||||
tmp_sort_2?: null | string;
|
||||
tmp_sort_3?: null | string;
|
||||
|
||||
// Additional fields for convenience (database views)
|
||||
username?: string; // Same as user_username
|
||||
// user_username?: null|string; // Same as username
|
||||
user_name?: null | string;
|
||||
user_email?: null | string;
|
||||
user_allow_auth_key?: null | boolean; // For sign in without password
|
||||
export interface Person extends ae_Person {
|
||||
// Legacy mapping support and view fields
|
||||
external_id?: string;
|
||||
external_sys_id?: string;
|
||||
person_profile_id?: string;
|
||||
person_profile_id_random?: string;
|
||||
|
||||
username?: string;
|
||||
user_name?: string;
|
||||
user_email?: string;
|
||||
user_super?: boolean;
|
||||
user_manager?: boolean;
|
||||
user_administrator?: boolean;
|
||||
user_public?: boolean;
|
||||
|
||||
organization_id?: null | string; // The organization this person belongs to, if any.
|
||||
organization_id_random?: null | string; // The random ID of the organization this person belongs to, if any.
|
||||
organization_name?: null | string;
|
||||
organization_id?: string;
|
||||
organization_id_random?: string;
|
||||
organization_name?: string;
|
||||
|
||||
contact_id?: null | string; // The contact ID of the person, if any.
|
||||
contact_id_random?: null | string; // The random ID of the contact, if any.
|
||||
contact_name?: null | string;
|
||||
contact_email?: null | string;
|
||||
contact_cc_email?: null | string;
|
||||
contact_phone_mobile?: null | string;
|
||||
contact_phone_home?: null | string;
|
||||
contact_phone_office?: null | string;
|
||||
contact_phone_landline?: null | string;
|
||||
contact_phone_fax?: null | string;
|
||||
contact_phone_other?: null | string;
|
||||
contact_id?: string;
|
||||
contact_id_random?: string;
|
||||
contact_name?: string;
|
||||
|
||||
address_id?: null | string; // The address ID of the person, if any.
|
||||
address_id_random?: null | string; // The random ID of the address, if any.
|
||||
address_city?: null | string;
|
||||
address_country_alpha_2_code?: null | string; // ISO 3166-1 alpha-2 country code
|
||||
address_id?: string;
|
||||
address_id_random?: string;
|
||||
}
|
||||
|
||||
// Updated 2026-01-06
|
||||
export interface User {
|
||||
id: string;
|
||||
user_id: string;
|
||||
user_id_random: string;
|
||||
|
||||
username: string;
|
||||
name: string;
|
||||
email: string;
|
||||
|
||||
allow_auth_key: boolean;
|
||||
super: boolean;
|
||||
manager: boolean;
|
||||
administrator: boolean;
|
||||
verified: boolean;
|
||||
public: boolean;
|
||||
|
||||
person_id?: string;
|
||||
person_id_random?: 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;
|
||||
|
||||
tmp_sort_1?: string;
|
||||
tmp_sort_2?: string;
|
||||
export interface User extends ae_User {
|
||||
// Additional local fields if needed
|
||||
}
|
||||
|
||||
// Updated 2026-01-06
|
||||
// Updated 2026-01-09
|
||||
export class MySubClassedDexie extends Dexie {
|
||||
file!: Table<File>;
|
||||
person!: Table<Person>;
|
||||
user!: Table<User>;
|
||||
account!: Table<any>;
|
||||
site!: Table<any>;
|
||||
site_domain!: Table<any>;
|
||||
address!: Table<any>;
|
||||
contact!: Table<any>;
|
||||
account!: Table<ae_Account>;
|
||||
site!: Table<ae_Site>;
|
||||
site_domain!: Table<ae_SiteDomain>;
|
||||
address!: Table<ae_Address>;
|
||||
contact!: Table<ae_Contact>;
|
||||
|
||||
constructor() {
|
||||
super('ae_core_db');
|
||||
this.version(1).stores({
|
||||
this.version(2).stores({
|
||||
file: `
|
||||
id, id_random, hosted_file_id, hosted_file_id_random,
|
||||
id, hosted_file_id, hosted_file_id_random,
|
||||
hash_sha256,
|
||||
account_id,
|
||||
account_id, account_id_random,
|
||||
for_type, for_id, for_id_random,
|
||||
filename, extension,
|
||||
content_type, size,
|
||||
enable, hide, priority, sort, group, created_on, updated_on`,
|
||||
|
||||
person: `
|
||||
id, person_id, person_id_random,
|
||||
external_id, code,
|
||||
account_id, user_id,
|
||||
account_id_random, user_id_random,
|
||||
person_profile_id,
|
||||
person_profile_id_random,
|
||||
account_id, account_id_random,
|
||||
user_id, user_id_random,
|
||||
given_name, family_name,
|
||||
full_name, affiliations, email,
|
||||
agree,
|
||||
enable, hide, priority, sort, group, created_on, updated_on`,
|
||||
|
||||
user: `
|
||||
id, user_id, user_id_random,
|
||||
username, name, email,
|
||||
username, email,
|
||||
super, manager, administrator,
|
||||
enable, hide, priority, sort, group, created_on, updated_on`,
|
||||
|
||||
@@ -220,21 +103,21 @@ export class MySubClassedDexie extends Dexie {
|
||||
site_domain: `
|
||||
id, site_domain_id, site_domain_id_random,
|
||||
site_id, site_id_random,
|
||||
domain,
|
||||
fqdn,
|
||||
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,
|
||||
for_type, 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,
|
||||
for_type, for_id_random,
|
||||
title, email, phone_mobile,
|
||||
enable, hide, priority, sort, group, created_on, updated_on`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ export async function update_ae_obj__event({
|
||||
return result;
|
||||
}
|
||||
|
||||
// Updated 2026-01-06
|
||||
// Updated 2026-01-09
|
||||
export async function qry_ae_obj_li__event({
|
||||
api_cfg,
|
||||
for_obj_type = 'account',
|
||||
@@ -347,11 +347,10 @@ export async function qry_ae_obj_li__event({
|
||||
const search_query: any = { and: [] };
|
||||
if (qry_str) search_query.q = qry_str;
|
||||
|
||||
if (qry_person_id) {
|
||||
search_query.and.push({ field: 'external_person_id', op: 'eq', value: qry_person_id });
|
||||
}
|
||||
// Note: V3 does not support searching by person_id directly on the event object yet.
|
||||
// We will filter client-side instead.
|
||||
|
||||
return await api.search_ae_obj_v3({
|
||||
const result_li = await api.search_ae_obj_v3({
|
||||
api_cfg,
|
||||
obj_type: 'event',
|
||||
for_obj_type,
|
||||
@@ -360,11 +359,25 @@ export async function qry_ae_obj_li__event({
|
||||
enabled,
|
||||
hidden,
|
||||
view,
|
||||
limit,
|
||||
limit: qry_person_id ? 500 : limit, // Increase limit if filtering client-side
|
||||
offset,
|
||||
order_by_li,
|
||||
log_lvl
|
||||
});
|
||||
|
||||
if (qry_person_id && result_li) {
|
||||
return result_li.filter((ev: any) => {
|
||||
return (
|
||||
ev.external_person_id === qry_person_id ||
|
||||
ev.poc_person_id === qry_person_id ||
|
||||
ev.poc_person_id_random === qry_person_id ||
|
||||
ev.poc_event_person_id === qry_person_id ||
|
||||
ev.poc_event_person_id_random === qry_person_id
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return result_li;
|
||||
}
|
||||
|
||||
// Updated 2025-05-09
|
||||
|
||||
@@ -1,456 +1,50 @@
|
||||
import Dexie, { type Table } from 'dexie';
|
||||
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import type { ae_Journal, ae_JournalEntry } from '$lib/types/ae_types';
|
||||
|
||||
// li = list
|
||||
// kv = key value list
|
||||
// json = JSON string
|
||||
// ux = user experience (mode)
|
||||
// LLM = Large Language Model (AI)
|
||||
// Updated 2025-03-15
|
||||
// Updated 2026-01-09 - Unified Types
|
||||
|
||||
export interface Journal {
|
||||
id: string; // actually "id_random"
|
||||
journal_id: string;
|
||||
|
||||
// Essentially this is a change log of journals
|
||||
snapshot_id?: string; // This is the original journal ID. If deleted, then delete all children journals.
|
||||
previous_id?: null | string; // This is the old or parent journal ID
|
||||
next_id?: null | string; // This is the new or child journal ID
|
||||
|
||||
external_id?: null | string;
|
||||
import_id?: null | string;
|
||||
code?: null | string;
|
||||
|
||||
for_type?: null | string;
|
||||
for_id?: null | string;
|
||||
|
||||
// template?: null|boolean; // Is this a template journal? If true, it can be used to create new journals.
|
||||
|
||||
type_code?: null | string;
|
||||
|
||||
account_id?: null | string; // Owner account of the journal
|
||||
person_id?: null | string; // Owner person of the journal
|
||||
// event_id?: null|string; // Assign to an event???
|
||||
// location_id?: null|string; // Assign to a location???
|
||||
|
||||
name: string; // or the title
|
||||
short_name?: null | string; // Short name for the journal, if any. Used for display purposes.
|
||||
summary?: null | string; // LLM (AI) generated summary...???
|
||||
outline?: null | string; // LLM (AI) generated outline...???
|
||||
|
||||
description?: null | string;
|
||||
description_md_html?: null | string; // Markdown converted to HTML based on description. Uses marked or similar library for conversion.
|
||||
description_md_html_alt?: null | string; // Markdown converted to HTML based on description. Uses marked or similar library for conversion.
|
||||
description_html?: null | string;
|
||||
description_json?: null | string;
|
||||
|
||||
start_datetime?: null | Date;
|
||||
end_datetime?: null | Date;
|
||||
timezone?: null | string;
|
||||
|
||||
alert?: null | boolean; // LLM (AI) generated summary...???
|
||||
alert_msg?: null | string; // LLM (AI) generated summary...???
|
||||
|
||||
sort_by?: null | string; // This is the sort by field
|
||||
sort_by_desc?: null | string; // This is the sort by field description
|
||||
|
||||
cfg_json?: null | key_val; // This is the configuration JSON for the journal
|
||||
|
||||
data_json?: null | key_val; // We always need to store something extra...
|
||||
|
||||
ux_mode?: null | string; // 'mobile' or 'desktop'
|
||||
|
||||
// This only allows for basic access to the data.
|
||||
passcode_read?: null | string; // For LLM (AI) generated summary...???
|
||||
passcode_read_expire?: null | Date;
|
||||
passcode_write?: null | string;
|
||||
passcode_write_expire?: null | Date;
|
||||
|
||||
passcode?: null | string; // For Journal Entry encryption password
|
||||
passcode_timeout?: null | number; // Timeout in seconds
|
||||
|
||||
private_passcode?: null | string; // Combine with the Journal passcode for Journal Entry encryption password
|
||||
|
||||
auth_key?: null | string; // For Journal authorization without sign in
|
||||
|
||||
enable: null | boolean;
|
||||
hide?: null | boolean;
|
||||
archive?: null | boolean; // Archive the journal
|
||||
archive_on?: null | Date;
|
||||
priority?: null | boolean;
|
||||
sort?: null | number;
|
||||
group?: null | string;
|
||||
notes?: null | string;
|
||||
created_on: Date;
|
||||
updated_on?: null | Date;
|
||||
|
||||
// Generated fields for sorting locally only
|
||||
tmp_sort_1?: null | string;
|
||||
tmp_sort_2?: null | string;
|
||||
tmp_sort_3?: null | string;
|
||||
|
||||
combined_passcode?: null | string; // For Journal Entry encryption password
|
||||
|
||||
// Additional fields for convenience (database views)
|
||||
file_count?: null | number; // Only files directly under a journal
|
||||
journal_file_id_li_json?: null | string;
|
||||
|
||||
// One person
|
||||
person__given_name?: null | string;
|
||||
person__family_name?: null | string;
|
||||
person__full_name?: null | string;
|
||||
person__primary_email?: null | string;
|
||||
person__passcode?: null | string;
|
||||
|
||||
// JSON formatted key value pairs for multiple people: {id: name, email, etc.}
|
||||
person__kv_json?: null | string;
|
||||
|
||||
journal_name?: null | string;
|
||||
|
||||
journal_location_code?: null | string;
|
||||
journal_location_name?: null | string;
|
||||
|
||||
journal_entry_count?: null | number;
|
||||
|
||||
// A key value list of the entries
|
||||
journal_entry_kv?: null | key_val;
|
||||
journal_entry_li?: null | [];
|
||||
// A key value list of the files
|
||||
journal_file_kv?: null | key_val;
|
||||
journal_file_li?: null | [];
|
||||
|
||||
// journal_collection_id?: null|string; // For a collection of journals?
|
||||
|
||||
// Future standard fields!!!
|
||||
obj_id?: null | string;
|
||||
obj_ext_uid?: null | string; // Probably not needed for journals
|
||||
obj_ext_id?: null | string; // Probably not needed for journals
|
||||
obj_import_id?: null | string; // Probably not needed for journals
|
||||
obj_code?: null | string;
|
||||
obj_account_id?: null | string;
|
||||
obj_passcode?: null | string;
|
||||
obj_type?: null | string; // Should always be 'journal' in this case
|
||||
obj_type_ver_id?: null | string; // The ID from the table for the object type
|
||||
obj_name?: null | string;
|
||||
obj_summary?: null | string; // LLM (AI) generated summary...???
|
||||
obj_outline?: null | string; // LLM (AI) generated outline...???
|
||||
obj_description?: null | string; // Probably not needed for journals
|
||||
obj_enable?: null | boolean;
|
||||
obj_enable_on?: null | Date;
|
||||
obj_archive_on?: null | Date;
|
||||
obj_hide?: null | boolean;
|
||||
obj_priority?: null | number;
|
||||
obj_sort?: null | number;
|
||||
obj_group?: null | string;
|
||||
obj_cfg_json?: null | string;
|
||||
obj_notes?: null | string;
|
||||
obj_created_on?: Date;
|
||||
obj_updated_on?: null | Date;
|
||||
export interface Journal extends ae_Journal {
|
||||
// Add any Dexie-specific or legacy local-only fields here if not in ae_Journal
|
||||
// Most fields are now in ae_Journal and ae_BaseObj
|
||||
|
||||
// For backward compatibility with some views that expect these
|
||||
combined_passcode?: string;
|
||||
person__given_name?: string;
|
||||
person__family_name?: string;
|
||||
person__full_name?: string;
|
||||
person__primary_email?: string;
|
||||
person__passcode?: string;
|
||||
person__kv_json?: string;
|
||||
|
||||
journal_entry_kv?: key_val;
|
||||
journal_entry_li?: any[];
|
||||
journal_file_kv?: key_val;
|
||||
journal_file_li?: any[];
|
||||
}
|
||||
|
||||
export const journal_field_li = [
|
||||
'id',
|
||||
'journal_id',
|
||||
'snapshot_id',
|
||||
'previous_id',
|
||||
'next_id',
|
||||
'external_id',
|
||||
'import_id',
|
||||
'code',
|
||||
'for_type',
|
||||
'for_id',
|
||||
'type_code',
|
||||
'journal_id_random',
|
||||
'account_id',
|
||||
'account_id_random',
|
||||
'person_id',
|
||||
'person_id_random',
|
||||
'code',
|
||||
'name',
|
||||
'short_name',
|
||||
'summary',
|
||||
'outline',
|
||||
'description',
|
||||
'description_md_html',
|
||||
'description_md_html_alt',
|
||||
'description_html',
|
||||
'description_json',
|
||||
'start_datetime',
|
||||
'end_datetime',
|
||||
'timezone',
|
||||
'alert',
|
||||
'alert_msg',
|
||||
'sort_by',
|
||||
'sort_by_desc',
|
||||
'cfg_json',
|
||||
'data_json',
|
||||
'ux_mode',
|
||||
'passcode_read',
|
||||
'passcode_read_expire',
|
||||
'passcode_write',
|
||||
'passcode_write_expire',
|
||||
'passcode_timeout',
|
||||
'private_passcode',
|
||||
'auth_key',
|
||||
'enable',
|
||||
'hide',
|
||||
'archive', // Archive the journal
|
||||
'archive_on', // Archive date
|
||||
'priority', // Priority flag
|
||||
'sort', // Sort order
|
||||
'group', // Group name
|
||||
'notes', // Notes about the journal
|
||||
'created_on', // Creation date
|
||||
'updated_on', // Last updated date
|
||||
|
||||
'tmp_sort_1', // Temporary sort field 1
|
||||
'tmp_sort_2', // Temporary sort field 2
|
||||
'tmp_sort_3', // Temporary sort field 3
|
||||
|
||||
'combined_passcode', // For Journal Entry encryption password
|
||||
'file_count', // Only files directly under a journal
|
||||
'journal_file_id_li_json', // JSON string of file IDs
|
||||
'person__given_name', // Person's given name
|
||||
'person__family_name', // Person's family name
|
||||
'person__full_name', // Person's full name
|
||||
'person__primary_email', // Person's primary email
|
||||
'person__passcode', // Person's passcode
|
||||
'person__kv_json', // JSON formatted key value pairs for multiple people
|
||||
'journal_name', // Journal name
|
||||
'journal_location_code', // Journal location code
|
||||
'journal_location_name', // Journal location name
|
||||
'journal_entry_count', // Count of journal entries
|
||||
'journal_entry_kv', // Key value list of the entries
|
||||
'journal_entry_li', // List of journal entries
|
||||
'journal_file_kv', // Key value list of the files
|
||||
'journal_file_li', // List of journal files
|
||||
|
||||
'obj_id', // Object ID
|
||||
'obj_ext_uid', // External UID
|
||||
'obj_ext_id', // External ID
|
||||
'obj_import_id', // Import ID
|
||||
'obj_code', // Object code
|
||||
'obj_account_id', // Object account ID
|
||||
'obj_passcode', // Object passcode
|
||||
'obj_type', // Object type
|
||||
'obj_type_ver_id', // Object type version ID
|
||||
'obj_name', // Object name
|
||||
'obj_summary', // Object summary
|
||||
'obj_outline', // Object outline
|
||||
'obj_description', // Object description
|
||||
'obj_enable', // Object enable flag
|
||||
'obj_enable_on', // Object enable date
|
||||
'obj_archive_on', // Object archive date
|
||||
'obj_hide', // Object hide flag
|
||||
'obj_priority', // Object priority
|
||||
'obj_sort', // Object sort order
|
||||
'obj_group', // Object group name
|
||||
'obj_cfg_json', // Object configuration JSON
|
||||
'obj_notes', // Object notes
|
||||
'obj_created_on', // Object creation date
|
||||
'obj_updated_on' // Object last updated date
|
||||
];
|
||||
|
||||
// Updated 2025-04-02
|
||||
export interface Journal_Entry {
|
||||
id: string; // actually "id_random"
|
||||
journal_entry_id: string;
|
||||
|
||||
journal_id: string; // This is the parent journal ID. If deleted, then delete all children journal entries.
|
||||
|
||||
// Essentially this is a change log of journal entries
|
||||
snapshot_id?: string; // This is the original journal ID. If deleted, then delete all children journal entries.
|
||||
previous_id?: null | string; // This is the old or parent journal ID
|
||||
next_id?: null | string; // This is the new or child journal ID
|
||||
|
||||
external_id?: null | string;
|
||||
import_id?: null | string;
|
||||
code?: null | string;
|
||||
|
||||
for_type?: null | string;
|
||||
for_id?: null | string;
|
||||
|
||||
template?: null | boolean; // Is this a template journal entry? If true, it can be used to create new journal entries.
|
||||
|
||||
activity_code?: null | string;
|
||||
category_code?: null | string;
|
||||
topic_code?: null | string;
|
||||
type_code?: null | string;
|
||||
tags?: null | string; // Comma separated tags
|
||||
|
||||
journal_entry_type?: null | string; // This is the type of journal entry
|
||||
|
||||
account_id?: null | string; // Owner account of the journal
|
||||
person_id?: null | string; // Owner person of the journal
|
||||
// event_id?: null|string; // Assign to an event???
|
||||
// location_id?: null|string; // Assign to a location???
|
||||
|
||||
public?: null | boolean;
|
||||
private?: null | boolean;
|
||||
personal?: null | boolean;
|
||||
professional?: null | boolean;
|
||||
|
||||
name: string; // or the title
|
||||
short_name?: null | string; // Short name for the journal entry, if any. Used for display purposes. Most likely for the templates list.
|
||||
summary?: null | string; // LLM (AI) generated summary...???
|
||||
outline?: null | string; // LLM (AI) generated outline...???
|
||||
// description?: null|string; // This is the description of the journal entry
|
||||
|
||||
content?: null | string;
|
||||
content_md_html?: null | string; // Markdown converted to HTML based on content. Uses marked or similar library for conversion.
|
||||
content_md_html_alt?: null | string; // Markdown converted to HTML based on content. Uses marked or similar library for conversion.
|
||||
content_html?: null | string;
|
||||
content_json?: null | string;
|
||||
content_encrypted?: null | string; // This is the encrypted content of the journal entry
|
||||
|
||||
history?: null | string; // This is the history of the journal entry; a log
|
||||
history_encrypted?: null | string; // This is the encrypted history of the journal entry
|
||||
|
||||
passcode_hash?: null | string; // This is the passcode hash for the journal entry to look up the passcode
|
||||
|
||||
start_datetime?: null | Date;
|
||||
end_datetime?: null | Date;
|
||||
timezone?: null | string;
|
||||
seconds?: null | number; // Duration in seconds
|
||||
|
||||
location?: null | string; // Location of the journal entry
|
||||
latitude?: null | number; // Latitude of the journal entry
|
||||
longitude?: null | number; // Longitude of the journal entry
|
||||
|
||||
billable?: null | boolean; // Is this billable?
|
||||
bill_to?: null | string; // Who to bill for this journal entry
|
||||
bill_rate?: null | number; // Rate to bill for this journal entry
|
||||
billable_minutes?: null | number; // Billable minutes for this journal entry
|
||||
|
||||
alert?: null | boolean; // LLM (AI) generated summary...???
|
||||
alert_msg?: null | string; // LLM (AI) generated summary...???
|
||||
|
||||
parent_id?: null | string; // This is the parent journal entry ID. If deleted, then delete all children journal entries.
|
||||
related_entry_id_li?: null | key_val; // List of related journal entry IDs
|
||||
|
||||
// cfg_json?: null|key_val; // This is the configuration JSON for the journal entry
|
||||
data_json?: null | key_val; // We always need to store something extra...
|
||||
|
||||
// This only allows for basic access to the content.
|
||||
passcode_read?: null | string; // For LLM (AI) generated summary...???
|
||||
passcode_read_expire?: null | Date;
|
||||
passcode_write?: null | string;
|
||||
passcode_write_expire?: null | Date;
|
||||
|
||||
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;
|
||||
|
||||
// Generated fields for sorting locally only
|
||||
tmp_sort_1?: null | string;
|
||||
tmp_sort_2?: null | string;
|
||||
tmp_sort_3?: null | string;
|
||||
|
||||
// Additional fields for convenience (database views)
|
||||
file_count?: null | number; // Only files directly under a journal
|
||||
journal_file_id_li_json?: null | string;
|
||||
|
||||
journal_code?: null | string; // This is the code for the journal entry
|
||||
journal_name?: null | string; // This is the name for the journal entry
|
||||
|
||||
// One person
|
||||
person__given_name?: null | string;
|
||||
person__family_name?: null | string;
|
||||
person__full_name?: null | string;
|
||||
person__primary_email?: null | string;
|
||||
person__passcode?: null | string;
|
||||
|
||||
// JSON formatted key value pairs for multiple people: {id: name, email, etc.}
|
||||
person__kv_json?: null | string;
|
||||
|
||||
// A key value list of the files
|
||||
journal_file_kv?: null | key_val;
|
||||
journal_file_li?: null | [];
|
||||
|
||||
// journal_collection_id?: null|string; // For a collection of journal entries?
|
||||
|
||||
// Future standard fields!!!
|
||||
obj_id?: null | string;
|
||||
obj_ext_uid?: null | string; // Probably not needed for journal entries
|
||||
obj_ext_id?: null | string; // Probably not needed for journal entries
|
||||
obj_import_id?: null | string; // Probably not needed for journal entries
|
||||
obj_code?: null | string;
|
||||
obj_account_id?: null | string;
|
||||
obj_passcode?: null | string;
|
||||
obj_type?: null | string; // Should always be 'journal' in this case
|
||||
obj_type_ver_id?: null | string; // The ID from the table for the object type
|
||||
obj_name?: null | string;
|
||||
obj_summary?: null | string; // LLM (AI) generated summary...???
|
||||
obj_outline?: null | string; // LLM (AI) generated outline...???
|
||||
obj_description?: null | string; // Probably not needed for journal entries
|
||||
obj_enable?: null | boolean;
|
||||
obj_enable_on?: null | Date;
|
||||
obj_archive_on?: null | Date;
|
||||
obj_hide?: null | boolean;
|
||||
obj_priority?: null | number;
|
||||
obj_sort?: null | number;
|
||||
obj_group?: null | string;
|
||||
obj_cfg_json?: null | string;
|
||||
obj_notes?: null | string;
|
||||
obj_created_on?: Date;
|
||||
obj_updated_on?: null | Date;
|
||||
}
|
||||
|
||||
export const journal_entry_field_li = [
|
||||
'id',
|
||||
'journal_entry_id',
|
||||
'journal_id',
|
||||
'code',
|
||||
'for_type',
|
||||
'for_id',
|
||||
'template',
|
||||
'activity_code',
|
||||
'category_code',
|
||||
'topic_code',
|
||||
'type_code',
|
||||
'tags',
|
||||
'journal_entry_type',
|
||||
'account_id',
|
||||
'person_id',
|
||||
'public',
|
||||
'private',
|
||||
'personal',
|
||||
'professional',
|
||||
'name',
|
||||
'short_name',
|
||||
'summary',
|
||||
'outline',
|
||||
'content',
|
||||
'content_md_html',
|
||||
'content_md_html_alt',
|
||||
'content_html',
|
||||
'content_json',
|
||||
'content_encrypted',
|
||||
'history',
|
||||
'history_encrypted',
|
||||
'passcode_hash',
|
||||
'start_datetime',
|
||||
'end_datetime',
|
||||
'timezone',
|
||||
'seconds',
|
||||
'location',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'billable',
|
||||
'bill_to',
|
||||
'bill_rate',
|
||||
'billable_minutes',
|
||||
'alert',
|
||||
'alert_msg',
|
||||
'parent_id',
|
||||
'related_entry_id_li',
|
||||
'data_json',
|
||||
'passcode_read',
|
||||
'passcode_read_expire',
|
||||
'passcode_write',
|
||||
'passcode_write_expire',
|
||||
'enable',
|
||||
'hide',
|
||||
'priority',
|
||||
@@ -459,80 +53,73 @@ export const journal_entry_field_li = [
|
||||
'notes',
|
||||
'created_on',
|
||||
'updated_on',
|
||||
|
||||
'tmp_sort_1',
|
||||
'tmp_sort_2',
|
||||
'tmp_sort_3',
|
||||
|
||||
'file_count',
|
||||
'journal_file_id_li_json',
|
||||
'journal_code',
|
||||
'journal_name',
|
||||
'person__given_name',
|
||||
'person__family_name',
|
||||
'person__full_name',
|
||||
'person__primary_email',
|
||||
'person__passcode',
|
||||
'person__kv_json',
|
||||
'journal_file_kv',
|
||||
'journal_file_li',
|
||||
|
||||
'obj_id',
|
||||
'obj_ext_uid',
|
||||
'obj_ext_id',
|
||||
'obj_import_id',
|
||||
'obj_code',
|
||||
'obj_account_id',
|
||||
'obj_passcode',
|
||||
'obj_type',
|
||||
'obj_type_ver_id',
|
||||
'obj_name',
|
||||
'obj_summary',
|
||||
'obj_outline',
|
||||
'obj_description',
|
||||
'obj_enable',
|
||||
'obj_enable_on',
|
||||
'obj_archive_on',
|
||||
'obj_hide',
|
||||
'obj_priority',
|
||||
'obj_sort',
|
||||
'obj_group',
|
||||
'obj_cfg_json',
|
||||
'obj_notes',
|
||||
'obj_created_on',
|
||||
'obj_updated_on'
|
||||
'tmp_sort_3'
|
||||
];
|
||||
|
||||
export interface Journal_Entry extends ae_JournalEntry {
|
||||
// Add any Dexie-specific or legacy local-only fields here
|
||||
person__given_name?: string;
|
||||
person__family_name?: string;
|
||||
person__full_name?: string;
|
||||
person__primary_email?: string;
|
||||
person__passcode?: string;
|
||||
person__kv_json?: string;
|
||||
|
||||
journal_file_kv?: key_val;
|
||||
journal_file_li?: any[];
|
||||
}
|
||||
|
||||
export const journal_entry_field_li = [
|
||||
'id',
|
||||
'journal_entry_id',
|
||||
'journal_entry_id_random',
|
||||
'journal_id',
|
||||
'journal_id_random',
|
||||
'person_id',
|
||||
'person_id_random',
|
||||
'code',
|
||||
'name',
|
||||
'short_name',
|
||||
'summary',
|
||||
'outline',
|
||||
'content',
|
||||
'content_md_html',
|
||||
'content_encrypted',
|
||||
'enable',
|
||||
'hide',
|
||||
'priority',
|
||||
'sort',
|
||||
'group',
|
||||
'notes',
|
||||
'created_on',
|
||||
'updated_on',
|
||||
'tmp_sort_1',
|
||||
'tmp_sort_2',
|
||||
'tmp_sort_3'
|
||||
];
|
||||
|
||||
// Updated 2024-06-10
|
||||
export class MySubClassedDexie extends Dexie {
|
||||
// We just tell the typing system this is the case
|
||||
journal!: Table<Journal>;
|
||||
journal_entry!: Table<Journal_Entry>;
|
||||
|
||||
constructor() {
|
||||
super('ae_journals_db');
|
||||
this.version(4).stores({
|
||||
this.version(5).stores({
|
||||
journal: `
|
||||
id, journal_id,
|
||||
id, journal_id, journal_id_random,
|
||||
code,
|
||||
account_id,
|
||||
person_id,
|
||||
conference, type,
|
||||
account_id, account_id_random,
|
||||
person_id, person_id_random,
|
||||
name,
|
||||
start_datetime, end_datetime,
|
||||
timezone,
|
||||
tmp_sort_1, tmp_sort_2, tmp_sort_3,
|
||||
enable, hide, priority, sort, group, created_on, updated_on`,
|
||||
journal_entry: `
|
||||
id, journal_entry_id,
|
||||
journal_id,
|
||||
id, journal_entry_id, journal_entry_id_random,
|
||||
journal_id, journal_id_random,
|
||||
code,
|
||||
account_id,
|
||||
template,
|
||||
name,
|
||||
start_datetime, end_datetime,
|
||||
timezone,
|
||||
tmp_sort_1, tmp_sort_2, tmp_sort_3,
|
||||
enable, hide, priority, sort, group, created_on, updated_on`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,12 +106,40 @@ export interface ae_SiteDomain extends ae_BaseObj {
|
||||
/**
|
||||
* Journal - A collection of entries
|
||||
*/
|
||||
/**
|
||||
* Journal - A container for journal entries
|
||||
*/
|
||||
export interface ae_Journal extends ae_BaseObj {
|
||||
journal_id: string;
|
||||
journal_id_random: string;
|
||||
account_id: string;
|
||||
account_id_random: string;
|
||||
person_id?: string;
|
||||
person_id_random?: string;
|
||||
|
||||
for_type?: string;
|
||||
for_id?: string;
|
||||
for_id_random?: string;
|
||||
|
||||
type_code?: string;
|
||||
tags?: string;
|
||||
|
||||
summary?: string;
|
||||
outline?: string;
|
||||
|
||||
start_datetime?: string | Date;
|
||||
end_datetime?: string | Date;
|
||||
timezone?: string;
|
||||
|
||||
passcode?: string;
|
||||
passcode_timeout?: number;
|
||||
private_passcode?: string;
|
||||
|
||||
cfg_json?: any;
|
||||
data_json?: any;
|
||||
meta_json?: any;
|
||||
|
||||
// SQL View fields
|
||||
journal_entry_count?: number;
|
||||
file_count?: number;
|
||||
}
|
||||
@@ -128,6 +156,8 @@ export interface ae_JournalEntry extends ae_BaseObj {
|
||||
person_id?: string;
|
||||
person_id_random?: string;
|
||||
|
||||
template?: boolean;
|
||||
|
||||
journal_entry_type?: string;
|
||||
activity_code?: string;
|
||||
category_code?: string;
|
||||
@@ -155,14 +185,30 @@ export interface ae_JournalEntry extends ae_BaseObj {
|
||||
|
||||
passcode_hash?: string;
|
||||
|
||||
start_datetime?: string | Date;
|
||||
end_datetime?: string | Date;
|
||||
timezone?: string;
|
||||
seconds?: number;
|
||||
|
||||
location?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
|
||||
billable?: boolean;
|
||||
bill_to?: string;
|
||||
bill_rate?: number;
|
||||
billable_minutes?: number;
|
||||
|
||||
alert?: boolean;
|
||||
alert_msg?: string;
|
||||
|
||||
data_json?: any;
|
||||
meta_json?: any;
|
||||
|
||||
// SQL View fields
|
||||
journal_code?: string;
|
||||
journal_name?: string;
|
||||
file_count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,10 +224,12 @@ export interface ae_Person extends ae_BaseObj {
|
||||
user_id_random?: string;
|
||||
|
||||
prefix?: string;
|
||||
title_names?: string;
|
||||
given_name?: string;
|
||||
middle_name?: string;
|
||||
family_name?: string;
|
||||
suffix?: string;
|
||||
designations?: string;
|
||||
|
||||
full_name?: string;
|
||||
informal_name?: string;
|
||||
@@ -199,6 +247,9 @@ export interface ae_Person extends ae_BaseObj {
|
||||
|
||||
tagline?: string;
|
||||
|
||||
allow_auth_key?: boolean;
|
||||
passcode?: string;
|
||||
|
||||
data_json?: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { MapPin, Plus, Search, ExternalLink } from 'lucide-svelte';
|
||||
import { MapPin, Plus, Search, ExternalLink, X } from 'lucide-svelte';
|
||||
import { load_ae_obj_li__address, create_ae_obj__address } from '$lib/ae_core/ae_core__address';
|
||||
import Address_form from './ae_comp__address_form.svelte';
|
||||
|
||||
let address_li: any[] = $state([]);
|
||||
let loading = $state(true);
|
||||
let show_add_form = $state(false);
|
||||
|
||||
async function load_addresses() {
|
||||
if (!$ae_loc.account_id) return;
|
||||
@@ -20,27 +22,6 @@
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function handle_add() {
|
||||
const city = prompt('Enter city:');
|
||||
if (!city) return;
|
||||
const state_province = prompt('Enter state/province:');
|
||||
const country = prompt('Enter country:', 'USA');
|
||||
|
||||
const result = await create_ae_obj__address({
|
||||
api_cfg: $ae_api,
|
||||
account_id: $ae_loc.account_id,
|
||||
data_kv: { city, state_province, country, enable: true },
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
if (result) {
|
||||
load_addresses();
|
||||
if (result.address_id_random) {
|
||||
goto(`/core/addresses/${result.address_id_random}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!$ae_loc.manager_access) {
|
||||
goto('/core');
|
||||
@@ -56,11 +37,30 @@
|
||||
<MapPin size={24} />
|
||||
<h1 class="h2">Address Management</h1>
|
||||
</div>
|
||||
<button class="btn variant-filled-primary" onclick={handle_add}>
|
||||
<Plus size={16} class="mr-2" /> Add Address
|
||||
<button class="btn variant-filled-primary" onclick={() => show_add_form = !show_add_form}>
|
||||
{#if show_add_form}
|
||||
<X size={16} class="mr-2" /> Cancel
|
||||
{:else}
|
||||
<Plus size={16} class="mr-2" /> Add Address
|
||||
{/if}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#if show_add_form}
|
||||
<div class="mb-8">
|
||||
<Address_form
|
||||
onSave={(new_addr) => {
|
||||
show_add_form = false;
|
||||
load_addresses();
|
||||
if (new_addr.address_id_random) {
|
||||
goto(`/core/addresses/${new_addr.address_id_random}`);
|
||||
}
|
||||
}}
|
||||
onCancel={() => show_add_form = false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="placeholder animate-pulse h-64 w-full"></div>
|
||||
{:else if address_li.length === 0}
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
import { editable_fields__address } from '$lib/ae_core/ae_core__address.editable_fields';
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Save, Trash2, ArrowLeft, MapPin } from 'lucide-svelte';
|
||||
import { Save, Trash2, ArrowLeft, MapPin, Edit, Eye } from 'lucide-svelte';
|
||||
import Address_form from '../ae_comp__address_form.svelte';
|
||||
|
||||
let address_id = $page.params.address_id;
|
||||
let address: any = $state(null);
|
||||
let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
let is_editing = $state(false);
|
||||
|
||||
async function load_data() {
|
||||
loading = true;
|
||||
@@ -34,24 +35,6 @@
|
||||
load_data();
|
||||
});
|
||||
|
||||
async function handle_save() {
|
||||
saving = true;
|
||||
const data_kv: any = {};
|
||||
editable_fields__address.forEach(field => {
|
||||
if (address[field] !== undefined) {
|
||||
data_kv[field] = address[field];
|
||||
}
|
||||
});
|
||||
|
||||
await update_ae_obj__address({
|
||||
api_cfg: $ae_api,
|
||||
address_id,
|
||||
data_kv,
|
||||
log_lvl: 1
|
||||
});
|
||||
saving = false;
|
||||
}
|
||||
|
||||
async function handle_delete() {
|
||||
if (!confirm('Permanently delete this address?')) return;
|
||||
await delete_ae_obj_id__address({
|
||||
@@ -76,11 +59,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn variant-soft-error" onclick={handle_delete} disabled={loading || saving}>
|
||||
<Trash2 size={16} class="mr-2" /> Delete
|
||||
<button class="btn btn-sm variant-soft-secondary" 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 variant-filled-primary" onclick={handle_save} disabled={loading || saving}>
|
||||
<Save size={16} class="mr-2" /> Save Changes
|
||||
<button class="btn btn-sm variant-soft-error" onclick={handle_delete} disabled={loading}>
|
||||
<Trash2 size={16} class="mr-2" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -88,72 +75,99 @@
|
||||
{#if loading}
|
||||
<div class="placeholder animate-pulse w-full h-64"></div>
|
||||
{:else if address}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<div class="card p-4 space-y-4">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Address Details</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="label">
|
||||
<span>City</span>
|
||||
<input class="input" type="text" bind:value={address.city} />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>State / Province</span>
|
||||
<input class="input" type="text" bind:value={address.state_province} />
|
||||
</label>
|
||||
<label class="label md:col-span-2">
|
||||
<span>Country</span>
|
||||
<input class="input" type="text" bind:value={address.country} />
|
||||
</label>
|
||||
{#if is_editing}
|
||||
<Address_form
|
||||
{address}
|
||||
onSave={(updated) => {
|
||||
address = updated;
|
||||
is_editing = false;
|
||||
}}
|
||||
onCancel={() => is_editing = false}
|
||||
/>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<div class="card p-6 space-y-4 variant-soft">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Address Details</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Attention To</p>
|
||||
<p>{address.attention_to || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Organization</p>
|
||||
<p>{address.organization_name || '--'}</p>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Address Lines</p>
|
||||
<p>{address.line_1}</p>
|
||||
{#if address.line_2}<p>{address.line_2}</p>{/if}
|
||||
{#if address.line_3}<p>{address.line_3}</p>{/if}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">City, State/Province</p>
|
||||
<p>{address.city}, {address.state_province || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Postal Code / Country</p>
|
||||
<p>{address.postal_code || '--'} / {address.country_name || address.country || '--'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6 space-y-4 variant-soft">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Technical Details</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Timezone</p>
|
||||
<p>{address.timezone || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Coordinates</p>
|
||||
<p>{address.latitude || '--'}, {address.longitude || '--'}</p>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Internal Notes</p>
|
||||
<p class="whitespace-pre-wrap">{address.notes || '--'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 space-y-4">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Internal Metadata</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="label">
|
||||
<span>Group</span>
|
||||
<input class="input" type="text" bind:value={address.group} />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Sort Priority</span>
|
||||
<input class="input" type="number" bind:value={address.sort} />
|
||||
</label>
|
||||
<label class="label md:col-span-2">
|
||||
<span>Internal Notes</span>
|
||||
<textarea class="textarea" rows="3" bind:value={address.notes}></textarea>
|
||||
</label>
|
||||
<div class="space-y-6">
|
||||
<div class="card p-6 space-y-4 variant-soft">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Status</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Enabled</span>
|
||||
<span class="badge {address.enable ? 'variant-filled-success' : 'variant-filled-error'}">
|
||||
{address.enable ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Hidden</span>
|
||||
<span class="badge {address.hide ? 'variant-filled-warning' : 'variant-filled-surface'}">
|
||||
{address.hide ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Priority</span>
|
||||
<span class="badge {address.priority ? 'variant-filled-secondary' : 'variant-filled-surface'}">
|
||||
{address.priority ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 opacity-60 text-xs font-mono variant-soft">
|
||||
<p>ID: {address.address_id_random}</p>
|
||||
<p>Created: {new Date(address.created_on).toLocaleString()}</p>
|
||||
{#if address.updated_on}
|
||||
<p>Updated: {new Date(address.updated_on).toLocaleString()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="card p-4 space-y-4">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Status & Visibility</h3>
|
||||
<div class="space-y-4">
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={address.enable} />
|
||||
<p>Enabled</p>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={address.hide} />
|
||||
<p>Hidden from Public</p>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={address.priority} />
|
||||
<p>High Priority</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 space-y-2 opacity-60 text-sm font-mono">
|
||||
<p>ID: {address.address_id_random}</p>
|
||||
<p>Created: {new Date(address.created_on).toLocaleString()}</p>
|
||||
{#if address.updated_on}
|
||||
<p>Updated: {new Date(address.updated_on).toLocaleString()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
258
src/routes/core/addresses/ae_comp__address_form.svelte
Normal file
258
src/routes/core/addresses/ae_comp__address_form.svelte
Normal file
@@ -0,0 +1,258 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Address Form Component
|
||||
* Standardized 2026-01-09 for Core UI Polish.
|
||||
* Uses unified ae_Address type and Svelte 5 Runes.
|
||||
*/
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { update_ae_obj__address, create_ae_obj__address } from '$lib/ae_core/ae_core__address';
|
||||
import type { ae_Address } from '$lib/types/ae_types';
|
||||
import { Save, X, MapPin, Globe, Clock, Navigation } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
address?: ae_Address | null;
|
||||
onSave?: (address: ae_Address) => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
let { address = null, onSave, onCancel }: Props = $props();
|
||||
|
||||
// Form State (Runes)
|
||||
let formData = $state({
|
||||
attention_to: address?.attention_to ?? '',
|
||||
organization_name: address?.organization_name ?? '',
|
||||
line_1: address?.line_1 ?? '',
|
||||
line_2: address?.line_2 ?? '',
|
||||
line_3: address?.line_3 ?? '',
|
||||
city: address?.city ?? '',
|
||||
state_province: address?.state_province ?? '',
|
||||
postal_code: address?.postal_code ?? '',
|
||||
country: address?.country ?? '',
|
||||
// country_name: address?.country_name ?? '', // DO NOT USE - Scott 2026-01-09
|
||||
timezone: address?.timezone ?? '',
|
||||
latitude: address?.latitude ?? '',
|
||||
longitude: address?.longitude ?? '',
|
||||
notes: address?.notes ?? '',
|
||||
enable: address?.enable ?? true,
|
||||
hide: address?.hide ?? false,
|
||||
priority: address?.priority ?? false
|
||||
});
|
||||
|
||||
let is_loading = $state(false);
|
||||
let error_msg = $state('');
|
||||
|
||||
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() === '') {
|
||||
// line_1 and city are likely required, but we'll trim them
|
||||
if (key === 'line_1' || key === 'city') {
|
||||
payload[key] = payload[key].trim();
|
||||
} else {
|
||||
payload[key] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (address?.address_id_random) {
|
||||
// Update existing
|
||||
result = await update_ae_obj__address({
|
||||
api_cfg: $ae_api,
|
||||
address_id: address.address_id_random,
|
||||
data_kv: payload,
|
||||
log_lvl: 1
|
||||
});
|
||||
} else {
|
||||
// Create new
|
||||
result = await create_ae_obj__address({
|
||||
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 address 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 variant-glass-surface">
|
||||
<header class="flex justify-between items-center border-b border-surface-500/30 pb-4">
|
||||
<h3 class="h3 flex items-center gap-2">
|
||||
<MapPin size={24} />
|
||||
{address ? 'Edit Address' : 'Create New Address'}
|
||||
</h3>
|
||||
<div class="flex gap-2">
|
||||
{#if onCancel}
|
||||
<button type="button" class="btn btn-sm variant-soft" onclick={onCancel}>
|
||||
<X size={16} class="mr-1" /> Cancel
|
||||
</button>
|
||||
{/if}
|
||||
<button type="submit" class="btn btn-sm variant-filled-primary" disabled={is_loading}>
|
||||
{#if is_loading}
|
||||
<span class="animate-spin mr-2">⏳</span>
|
||||
{:else}
|
||||
<Save size={16} class="mr-1" />
|
||||
{/if}
|
||||
Save Address
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if error_msg}
|
||||
<aside class="alert variant-filled-error">
|
||||
<div class="alert-message">
|
||||
<p>{error_msg}</p>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Location Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Location Details</legend>
|
||||
|
||||
<label class="label">
|
||||
<span>Attention To / Name</span>
|
||||
<input class="input" type="text" bind:value={formData.attention_to} placeholder="John Doe" />
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span>Organization Name</span>
|
||||
<input class="input" type="text" bind:value={formData.organization_name} placeholder="Acme Corp" />
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span>Address Line 1</span>
|
||||
<input class="input" type="text" bind:value={formData.line_1} required placeholder="123 Main St" />
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span>Line 2</span>
|
||||
<input class="input" type="text" bind:value={formData.line_2} placeholder="Suite 100" />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Line 3</span>
|
||||
<input class="input" type="text" bind:value={formData.line_3} placeholder="Floor 2" />
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Region Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Region & Code</legend>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span>City</span>
|
||||
<input class="input" type="text" bind:value={formData.city} required placeholder="Metropolis" />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>State / Province</span>
|
||||
<input class="input" type="text" bind:value={formData.state_province} placeholder="NY" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span>Postal Code</span>
|
||||
<input class="input" type="text" bind:value={formData.postal_code} placeholder="12345" />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Country (Code)</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Globe size={16} /></div>
|
||||
<input type="text" bind:value={formData.country} placeholder="USA" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- DO NOT USE - Scott 2026-01-09 -->
|
||||
<!-- <label class="label">
|
||||
<span>Country Name</span>
|
||||
<input class="input" type="text" bind:value={formData.country_name} placeholder="United States" />
|
||||
</label> -->
|
||||
</fieldset>
|
||||
|
||||
<!-- Technical Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Technical & GIS</legend>
|
||||
|
||||
<label class="label">
|
||||
<span>Timezone</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Clock size={16} /></div>
|
||||
<input type="text" bind:value={formData.timezone} placeholder="America/New_York" />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span>Latitude</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Navigation size={16} /></div>
|
||||
<input type="text" bind:value={formData.latitude} placeholder="40.7128" />
|
||||
</div>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Longitude</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Navigation size={16} /></div>
|
||||
<input type="text" bind:value={formData.longitude} placeholder="-74.0060" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Status Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<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">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.enable} />
|
||||
<span>Enabled</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.hide} />
|
||||
<span>Hidden</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.priority} />
|
||||
<span>Priority</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="label">
|
||||
<span>Internal Notes</span>
|
||||
<textarea class="textarea" rows="2" bind:value={formData.notes} placeholder="Notes about this address..."></textarea>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<footer class="flex justify-end gap-2 border-t border-surface-500/30 pt-4">
|
||||
<button type="submit" class="btn variant-filled-primary w-full md:w-auto" disabled={is_loading}>
|
||||
{#if is_loading}
|
||||
<span class="animate-spin mr-2">⏳</span>
|
||||
{/if}
|
||||
{address ? 'Update Address' : 'Create Address'}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
@@ -2,11 +2,13 @@
|
||||
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, ExternalLink } from 'lucide-svelte';
|
||||
import { Phone, Plus, Search, Mail, User, ExternalLink, 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 loading = $state(true);
|
||||
let show_add_form = $state(false);
|
||||
|
||||
async function load_contacts() {
|
||||
if (!$ae_loc.account_id) return;
|
||||
@@ -20,27 +22,6 @@
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function handle_add() {
|
||||
const name = prompt('Enter contact name:');
|
||||
if (!name) return;
|
||||
const email = prompt('Enter email address:');
|
||||
const phone = prompt('Enter phone number:');
|
||||
|
||||
const result = await create_ae_obj__contact({
|
||||
api_cfg: $ae_api,
|
||||
account_id: $ae_loc.account_id,
|
||||
data_kv: { name, email, phone, enable: true },
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
if (result) {
|
||||
load_contacts();
|
||||
if (result.contact_id_random) {
|
||||
goto(`/core/contacts/${result.contact_id_random}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!$ae_loc.manager_access) {
|
||||
goto('/core');
|
||||
@@ -56,11 +37,30 @@
|
||||
<Phone size={24} />
|
||||
<h1 class="h2">Contact Management</h1>
|
||||
</div>
|
||||
<button class="btn variant-filled-primary" onclick={handle_add}>
|
||||
<Plus size={16} class="mr-2" /> Add Contact
|
||||
<button class="btn variant-filled-primary" onclick={() => show_add_form = !show_add_form}>
|
||||
{#if show_add_form}
|
||||
<X size={16} class="mr-2" /> Cancel
|
||||
{:else}
|
||||
<Plus size={16} class="mr-2" /> Add Contact
|
||||
{/if}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#if show_add_form}
|
||||
<div class="mb-8">
|
||||
<Contact_form
|
||||
onSave={(new_con) => {
|
||||
show_add_form = false;
|
||||
load_contacts();
|
||||
if (new_con.contact_id_random) {
|
||||
goto(`/core/contacts/${new_con.contact_id_random}`);
|
||||
}
|
||||
}}
|
||||
onCancel={() => show_add_form = false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="placeholder animate-pulse h-64 w-full"></div>
|
||||
{:else if contact_li.length === 0}
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
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 { Save, Trash2, ArrowLeft, UserRound } from 'lucide-svelte';
|
||||
import { Save, Trash2, ArrowLeft, UserRound, Edit, Eye } from 'lucide-svelte';
|
||||
import Contact_form from '../ae_comp__contact_form.svelte';
|
||||
|
||||
let contact_id = $page.params.contact_id;
|
||||
let contact: any = $state(null);
|
||||
let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
let is_editing = $state(false);
|
||||
|
||||
async function load_data() {
|
||||
loading = true;
|
||||
@@ -34,24 +35,6 @@
|
||||
load_data();
|
||||
});
|
||||
|
||||
async function handle_save() {
|
||||
saving = true;
|
||||
const data_kv: any = {};
|
||||
editable_fields__contact.forEach(field => {
|
||||
if (contact[field] !== undefined) {
|
||||
data_kv[field] = contact[field];
|
||||
}
|
||||
});
|
||||
|
||||
await update_ae_obj__contact({
|
||||
api_cfg: $ae_api,
|
||||
contact_id,
|
||||
data_kv,
|
||||
log_lvl: 1
|
||||
});
|
||||
saving = false;
|
||||
}
|
||||
|
||||
async function handle_delete() {
|
||||
if (!confirm('Permanently delete this contact?')) return;
|
||||
await delete_ae_obj_id__contact({
|
||||
@@ -72,15 +55,19 @@
|
||||
</a>
|
||||
<div class="flex items-center gap-2">
|
||||
<UserRound size={24} />
|
||||
<h1 class="h2">{contact?.name ?? 'Loading Contact...'}</h1>
|
||||
<h1 class="h2">{contact?.name || contact?.title || 'Loading Contact...'}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn variant-soft-error" onclick={handle_delete} disabled={loading || saving}>
|
||||
<Trash2 size={16} class="mr-2" /> Delete
|
||||
<button class="btn btn-sm variant-soft-secondary" 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 variant-filled-primary" onclick={handle_save} disabled={loading || saving}>
|
||||
<Save size={16} class="mr-2" /> Save Changes
|
||||
<button class="btn btn-sm variant-soft-error" onclick={handle_delete} disabled={loading}>
|
||||
<Trash2 size={16} class="mr-2" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -88,72 +75,110 @@
|
||||
{#if loading}
|
||||
<div class="placeholder animate-pulse w-full h-64"></div>
|
||||
{:else if contact}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<div class="card p-4 space-y-4">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Contact Information</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="label md:col-span-2">
|
||||
<span>Display Name</span>
|
||||
<input class="input" type="text" bind:value={contact.name} />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Email Address</span>
|
||||
<input class="input" type="email" bind:value={contact.email} />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Phone Number</span>
|
||||
<input class="input" type="tel" bind:value={contact.phone} />
|
||||
</label>
|
||||
{#if is_editing}
|
||||
<Contact_form
|
||||
{contact}
|
||||
onSave={(updated) => {
|
||||
contact = updated;
|
||||
is_editing = false;
|
||||
}}
|
||||
onCancel={() => is_editing = false}
|
||||
/>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<div class="card p-6 space-y-4 variant-soft">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Contact Details</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Title / Name</p>
|
||||
<p>{contact.title || contact.name || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Tagline</p>
|
||||
<p>{contact.tagline || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Email</p>
|
||||
<p>{contact.email || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Website</p>
|
||||
<p>
|
||||
{#if contact.website_url}
|
||||
<a href={contact.website_url} target="_blank" class="text-blue-500 underline">{contact.website_url}</a>
|
||||
{:else}
|
||||
--
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6 space-y-4 variant-soft">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Communication & Social</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Mobile Phone</p>
|
||||
<p>{contact.phone_mobile || contact.phone || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Office Phone</p>
|
||||
<p>{contact.phone_office || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">LinkedIn</p>
|
||||
<p>{contact.linkedin_url || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Socials</p>
|
||||
<div class="flex gap-2">
|
||||
{#if contact.facebook_url}<span>FB</span>{/if}
|
||||
{#if contact.instagram_url}<span>IG</span>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Internal Notes</p>
|
||||
<p class="whitespace-pre-wrap">{contact.notes || '--'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 space-y-4">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Internal Metadata</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="label">
|
||||
<span>Group</span>
|
||||
<input class="input" type="text" bind:value={contact.group} />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Sort Priority</span>
|
||||
<input class="input" type="number" bind:value={contact.sort} />
|
||||
</label>
|
||||
<label class="label md:col-span-2">
|
||||
<span>Internal Notes</span>
|
||||
<textarea class="textarea" rows="3" bind:value={contact.notes}></textarea>
|
||||
</label>
|
||||
<div class="space-y-6">
|
||||
<div class="card p-6 space-y-4 variant-soft">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Status</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Enabled</span>
|
||||
<span class="badge {contact.enable ? 'variant-filled-success' : 'variant-filled-error'}">
|
||||
{contact.enable ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Hidden</span>
|
||||
<span class="badge {contact.hide ? 'variant-filled-warning' : 'variant-filled-surface'}">
|
||||
{contact.hide ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Priority</span>
|
||||
<span class="badge {contact.priority ? 'variant-filled-secondary' : 'variant-filled-surface'}">
|
||||
{contact.priority ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 opacity-60 text-xs font-mono variant-soft">
|
||||
<p>ID: {contact.contact_id_random}</p>
|
||||
<p>Created: {new Date(contact.created_on).toLocaleString()}</p>
|
||||
{#if contact.updated_on}
|
||||
<p>Updated: {new Date(contact.updated_on).toLocaleString()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="card p-4 space-y-4">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Status & Visibility</h3>
|
||||
<div class="space-y-4">
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={contact.enable} />
|
||||
<p>Enabled</p>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={contact.hide} />
|
||||
<p>Hidden from Public</p>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={contact.priority} />
|
||||
<p>High Priority</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 space-y-2 opacity-60 text-sm font-mono">
|
||||
<p>ID: {contact.contact_id_random}</p>
|
||||
<p>Created: {new Date(contact.created_on).toLocaleString()}</p>
|
||||
{#if contact.updated_on}
|
||||
<p>Updated: {new Date(contact.updated_on).toLocaleString()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
240
src/routes/core/contacts/ae_comp__contact_form.svelte
Normal file
240
src/routes/core/contacts/ae_comp__contact_form.svelte
Normal file
@@ -0,0 +1,240 @@
|
||||
<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 { Save, X, Phone, Mail, Globe, Facebook, Instagram, Linkedin, UserPlus } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
contact?: ae_Contact | null;
|
||||
onSave?: (contact: ae_Contact) => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
let { contact = null, onSave, onCancel }: Props = $props();
|
||||
|
||||
// Form State (Runes)
|
||||
let formData = $state({
|
||||
title: contact?.title ?? '',
|
||||
tagline: contact?.tagline ?? '',
|
||||
email: contact?.email ?? '',
|
||||
phone_mobile: contact?.phone_mobile ?? '',
|
||||
phone_office: contact?.phone_office ?? '',
|
||||
website_url: contact?.website_url ?? '',
|
||||
facebook_url: contact?.facebook_url ?? '',
|
||||
instagram_url: contact?.instagram_url ?? '',
|
||||
linkedin_url: contact?.linkedin_url ?? '',
|
||||
notes: contact?.notes ?? '',
|
||||
enable: contact?.enable ?? true,
|
||||
hide: contact?.hide ?? false,
|
||||
priority: contact?.priority ?? false
|
||||
});
|
||||
|
||||
let is_loading = $state(false);
|
||||
let error_msg = $state('');
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 variant-glass-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 variant-soft" onclick={onCancel}>
|
||||
<X size={16} class="mr-1" /> Cancel
|
||||
</button>
|
||||
{/if}
|
||||
<button type="submit" class="btn btn-sm variant-filled-primary" disabled={is_loading}>
|
||||
{#if is_loading}
|
||||
<span class="animate-spin mr-2">⏳</span>
|
||||
{:else}
|
||||
<Save size={16} class="mr-1" />
|
||||
{/if}
|
||||
Save Contact
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if error_msg}
|
||||
<aside class="alert variant-filled-error">
|
||||
<div class="alert-message">
|
||||
<p>{error_msg}</p>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<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>
|
||||
|
||||
<label class="label">
|
||||
<span>Title / Name</span>
|
||||
<input class="input" type="text" bind:value={formData.title} required placeholder="Business Office" />
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span>Tagline</span>
|
||||
<input class="input" type="text" bind:value={formData.tagline} placeholder="Primary contact for business inquiries" />
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span>Email Address</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Mail size={16} /></div>
|
||||
<input type="email" bind:value={formData.email} placeholder="contact@example.com" />
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<!-- Communication Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Phone & Web</legend>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span>Mobile Phone</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Phone size={16} /></div>
|
||||
<input type="tel" bind:value={formData.phone_mobile} placeholder="+1..." />
|
||||
</div>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Office Phone</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Phone size={16} /></div>
|
||||
<input type="tel" bind:value={formData.phone_office} placeholder="+1..." />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="label">
|
||||
<span>Website URL</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Globe size={16} /></div>
|
||||
<input type="url" bind:value={formData.website_url} placeholder="https://..." />
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<!-- Social Media Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Social Media</legend>
|
||||
|
||||
<label class="label">
|
||||
<span>LinkedIn URL</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Linkedin size={16} /></div>
|
||||
<input type="url" bind:value={formData.linkedin_url} placeholder="https://linkedin.com/in/..." />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span>Facebook URL</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Facebook size={16} /></div>
|
||||
<input type="url" bind:value={formData.facebook_url} placeholder="https://facebook.com/..." />
|
||||
</div>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Instagram URL</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Instagram size={16} /></div>
|
||||
<input type="url" bind:value={formData.instagram_url} placeholder="https://instagram.com/..." />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Status Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<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">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.enable} />
|
||||
<span>Enabled</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.hide} />
|
||||
<span>Hidden</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.priority} />
|
||||
<span>Priority</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="label">
|
||||
<span>Internal Notes</span>
|
||||
<textarea class="textarea" rows="2" bind:value={formData.notes} placeholder="Additional details..."></textarea>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<footer class="flex justify-end gap-2 border-t border-surface-500/30 pt-4">
|
||||
<button type="submit" class="btn variant-filled-primary w-full md:w-auto" disabled={is_loading}>
|
||||
{#if is_loading}
|
||||
<span class="animate-spin mr-2">⏳</span>
|
||||
{/if}
|
||||
{contact ? 'Update Contact' : 'Create Contact'}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
@@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { X } from 'lucide-svelte';
|
||||
import { ae_loc, ae_sess, slct, ae_api } from '$lib/stores/ae_stores';
|
||||
import { core_func } from '$lib/ae_core/ae_core_functions';
|
||||
import { Users, Plus } from 'lucide-svelte';
|
||||
import Comp_person_search from './ae_comp__person_search.svelte';
|
||||
import Comp_person_obj_tbl from '../ae_comp__person_obj_tbl.svelte';
|
||||
import Person_form from './ae_comp__person_form.svelte';
|
||||
|
||||
let person_id_random_li: string[] = $state([]);
|
||||
let show_add_form = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
if (!$ae_loc.manager_access) {
|
||||
@@ -25,31 +28,30 @@
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={async () => {
|
||||
if (!confirm(`Add a new person to ${$ae_loc.account_name}?`)) return;
|
||||
let person_data = {
|
||||
account_id_random: $slct.account_id,
|
||||
source_code: 'manual:SK-core',
|
||||
given_name: 'New',
|
||||
family_name: 'Person',
|
||||
enable: true
|
||||
};
|
||||
let new_person_obj = await core_func.create_ae_obj__person({
|
||||
api_cfg: $ae_api,
|
||||
data_kv: person_data,
|
||||
log_lvl: 1
|
||||
});
|
||||
if (new_person_obj && confirm(`Person created. View details?`)) {
|
||||
goto(`/core/people/${new_person_obj.person_id_random}`);
|
||||
}
|
||||
}}
|
||||
onclick={() => show_add_form = !show_add_form}
|
||||
class="btn variant-filled-primary"
|
||||
class:hidden={!$ae_loc.edit_mode}
|
||||
>
|
||||
<Plus size={16} class="mr-2" /> Add Person
|
||||
{#if show_add_form}
|
||||
<X size={16} class="mr-2" /> Cancel
|
||||
{:else}
|
||||
<Plus size={16} class="mr-2" /> Add Person
|
||||
{/if}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#if show_add_form}
|
||||
<div class="mb-8">
|
||||
<Person_form
|
||||
onSave={(new_person) => {
|
||||
show_add_form = false;
|
||||
goto(`/core/people/${new_person.person_id_random}`);
|
||||
}}
|
||||
onCancel={() => show_add_form = false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Comp_person_search on_results={(results) => {
|
||||
person_id_random_li = results.map(p => p.person_id_random);
|
||||
$ae_sess.person.show_report__person_li = true;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
// Imports
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
// import { api } from '$lib/api';
|
||||
@@ -27,12 +28,13 @@
|
||||
// import { events_func } from '$lib/ae_events_functions';
|
||||
|
||||
import Person_view from './../../person_view.svelte';
|
||||
import Person_form from './../ae_comp__person_form.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 { qry_ae_obj_li__event } from '$lib/ae_events/ae_events__event';
|
||||
import { qry__post } from '$lib/ae_posts/ae_posts__post';
|
||||
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';
|
||||
import { Users, Link, Unlink, UserPlus, ShieldCheck, User, Calendar, MessageSquare, History, Activity, Edit, Eye } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
@@ -49,7 +51,7 @@
|
||||
$slct.person_id = ae_acct.slct.person_id;
|
||||
$slct.person_obj = ae_acct.slct.person_obj;
|
||||
|
||||
$ae_sess.person.show_edit__person = false;
|
||||
let is_editing = $state(false);
|
||||
|
||||
let lq__person_obj = liveQuery(() => db_core.person.get($slct.person_id));
|
||||
$slct.lq__person_obj = lq__person_obj;
|
||||
@@ -171,11 +173,25 @@
|
||||
class:border-gray-100={!$ae_loc.person.show_content__person_page_help}
|
||||
>
|
||||
<div>
|
||||
<a href="/core" class={ae_snip.classes__core_menu__button}>
|
||||
<a href="/core/people" class={ae_snip.classes__core_menu__button}>
|
||||
<span class="fas fa-arrow-left mx-1"></span>
|
||||
Back to Core
|
||||
Back to People
|
||||
</a>
|
||||
|
||||
{#if $ae_loc.edit_mode}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => is_editing = !is_editing}
|
||||
class="btn btn-sm variant-soft-secondary mx-1"
|
||||
>
|
||||
{#if is_editing}
|
||||
<Eye size={16} class="mr-2" /> View Mode
|
||||
{:else}
|
||||
<Edit size={16} class="mr-2" /> Edit Mode
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
@@ -183,7 +199,7 @@
|
||||
!$ae_loc.person.show_content__person_page_help;
|
||||
}}
|
||||
class={ae_snip.classes__core_menu__button}
|
||||
title="Help and information about the session search"
|
||||
title="Help and information about the person page"
|
||||
>
|
||||
<span class="fas fa-question-circle mx-1"></span>
|
||||
{#if $ae_loc.person.show_content__person_page_help}
|
||||
@@ -383,20 +399,25 @@
|
||||
{/if}
|
||||
|
||||
{#if !$lq__person_obj}
|
||||
<div class="flex flex-row items-center justify-center">
|
||||
<div class="flex flex-row items-center justify-center p-8">
|
||||
<span class="fas fa-spinner fa-spin mx-1"></span>
|
||||
<span>Loading...</span>
|
||||
<span>Loading Person...</span>
|
||||
</div>
|
||||
{:else if is_editing}
|
||||
<div class="px-4">
|
||||
<Person_form
|
||||
person={$lq__person_obj}
|
||||
onSave={(updated) => {
|
||||
is_editing = false;
|
||||
// The liveQuery should pick up the changes after they are saved to IndexedDB
|
||||
// inside update_ae_obj__person -> db_save_ae_obj_li__ae_obj
|
||||
}}
|
||||
onCancel={() => is_editing = false}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- {$lq__person_obj?.full_name} -->
|
||||
<Person_view person_id={$slct.person_id} />
|
||||
{/if}
|
||||
|
||||
<!-- <hr class="w-full border border-gray-200" /> -->
|
||||
|
||||
<!-- {#await $slct.person_obj}
|
||||
<span class="fas fa-spinner fa-spin text-xl text-blue-500"></span>
|
||||
{:then result} -->
|
||||
<Person_view person_id={$slct.person_id} />
|
||||
<!-- {:catch error}
|
||||
<div class="text-red-800">
|
||||
<span class="fas fa-exclamation-triangle text-xl"></span>
|
||||
|
||||
262
src/routes/core/people/ae_comp__person_form.svelte
Normal file
262
src/routes/core/people/ae_comp__person_form.svelte
Normal file
@@ -0,0 +1,262 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Person Form Component
|
||||
* Standardized 2026-01-09 for Core UI Polish.
|
||||
* Uses unified ae_Person type and Svelte 5 Runes.
|
||||
*/
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { core_func } from '$lib/ae_core/ae_core_functions';
|
||||
import type { ae_Person } from '$lib/types/ae_types';
|
||||
import { Save, X, User, Mail, Phone, Building, Briefcase, Tag } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
person?: ae_Person | null;
|
||||
onSave?: (person: ae_Person) => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
let { person = null, onSave, onCancel }: Props = $props();
|
||||
|
||||
// Form State (Runes)
|
||||
let formData = $state({
|
||||
given_name: person?.given_name ?? '',
|
||||
family_name: person?.family_name ?? '',
|
||||
middle_name: person?.middle_name ?? '',
|
||||
prefix: person?.prefix ?? person?.title_names ?? '',
|
||||
suffix: person?.suffix ?? person?.designations ?? '',
|
||||
nickname: person?.informal_name ?? '',
|
||||
professional_title: person?.professional_title ?? '',
|
||||
affiliations: person?.affiliations ?? '',
|
||||
primary_email: person?.primary_email ?? '',
|
||||
phone: person?.phone ?? '',
|
||||
tagline: person?.tagline ?? '',
|
||||
notes: person?.notes ?? '',
|
||||
enable: person?.enable ?? true,
|
||||
hide: person?.hide ?? false,
|
||||
priority: person?.priority ?? false
|
||||
});
|
||||
|
||||
let is_loading = $state(false);
|
||||
let error_msg = $state('');
|
||||
|
||||
async function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
is_loading = true;
|
||||
error_msg = '';
|
||||
|
||||
// Clean payload: Map fields and handle optional values
|
||||
const payload: any = {
|
||||
given_name: formData.given_name.trim(),
|
||||
family_name: formData.family_name.trim() || null,
|
||||
middle_name: formData.middle_name.trim() || null,
|
||||
informal_name: formData.nickname.trim() || null,
|
||||
// title_names: formData.prefix.trim() || null, // DO NOT USE - Scott 2026-01-09
|
||||
designations: formData.suffix.trim() || null,
|
||||
// NOTE:DO NOT USE Do note send the full_name field at this time - Scott 2026-01-09
|
||||
// full_name: `${formData.prefix ? formData.prefix + ' ' : ''}${formData.given_name} ${formData.family_name}${formData.suffix ? ', ' + formData.suffix : ''}`.trim(), // DO NOT USE - Scott 2026-01-09
|
||||
professional_title: formData.professional_title.trim() || null,
|
||||
affiliations: formData.affiliations.trim() || null,
|
||||
primary_email: formData.primary_email.trim() || null,
|
||||
tagline: formData.tagline.trim() || null,
|
||||
notes: formData.notes.trim() || null,
|
||||
allow_auth_key: true,
|
||||
enable: formData.enable,
|
||||
hide: formData.hide,
|
||||
priority: formData.priority
|
||||
};
|
||||
|
||||
// Ensure strings are truly null if empty after trim
|
||||
for (const key in payload) {
|
||||
if (payload[key] === '') {
|
||||
if (key !== 'given_name' && key !== 'full_name') {
|
||||
payload[key] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (person?.person_id_random) {
|
||||
// Update existing
|
||||
result = await core_func.update_ae_obj__person({
|
||||
api_cfg: $ae_api,
|
||||
person_id: person.person_id_random,
|
||||
data_kv: payload,
|
||||
log_lvl: 1
|
||||
});
|
||||
} else {
|
||||
// Create new
|
||||
result = await core_func.create_ae_obj__person({
|
||||
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 person record. The server rejected the request (400).';
|
||||
}
|
||||
} 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 variant-glass-surface">
|
||||
<header class="flex justify-between items-center border-b border-surface-500/30 pb-4">
|
||||
<h3 class="h3 flex items-center gap-2">
|
||||
<User size={24} />
|
||||
{person ? 'Edit Person' : 'Create New Person'}
|
||||
</h3>
|
||||
<div class="flex gap-2">
|
||||
{#if onCancel}
|
||||
<button type="button" class="btn btn-sm variant-soft" onclick={onCancel}>
|
||||
<X size={16} class="mr-1" /> Cancel
|
||||
</button>
|
||||
{/if}
|
||||
<button type="submit" class="btn btn-sm variant-filled-primary" disabled={is_loading}>
|
||||
{#if is_loading}
|
||||
<span class="animate-spin mr-2">⏳</span>
|
||||
{:else}
|
||||
<Save size={16} class="mr-1" />
|
||||
{/if}
|
||||
Save Person
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if error_msg}
|
||||
<aside class="alert variant-filled-error">
|
||||
<div class="alert-message">
|
||||
<p>{error_msg}</p>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Name Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Identity</legend>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<label class="label col-span-1">
|
||||
<span>Prefix</span>
|
||||
<input class="input placeholder-surface-400" type="text" bind:value={formData.prefix} placeholder="Mr." />
|
||||
</label>
|
||||
<label class="label col-span-3">
|
||||
<span>Given Name</span>
|
||||
<input class="input placeholder-surface-400" type="text" bind:value={formData.given_name} required placeholder="Jane" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<label class="label col-span-3">
|
||||
<span>Family Name</span>
|
||||
<input class="input placeholder-surface-400" type="text" bind:value={formData.family_name} required placeholder="Doe" />
|
||||
</label>
|
||||
<label class="label col-span-1">
|
||||
<span>Suffix</span>
|
||||
<input class="input placeholder-surface-400" type="text" bind:value={formData.suffix} placeholder="PhD" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="label">
|
||||
<span>Middle Name / Informal Name (Nickname)</span>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<input class="input placeholder-surface-400" type="text" bind:value={formData.middle_name} placeholder="Middle" />
|
||||
<input class="input placeholder-surface-400" type="text" bind:value={formData.nickname} placeholder="Nickname" />
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Contact Information</legend>
|
||||
|
||||
<label class="label">
|
||||
<span>Primary Email</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Mail size={16} /></div>
|
||||
<input class="input placeholder-surface-400" type="email" bind:value={formData.primary_email} placeholder="jane.doe@example.com" />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span>Phone Number</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Phone size={16} /></div>
|
||||
<input class="input placeholder-surface-400" type="tel" bind:value={formData.phone} placeholder="+1 (555) 000-0000" />
|
||||
</div>
|
||||
<small class="opacity-60 text-xs">(Saved only locally until Contact created)</small>
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span>Tagline</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Tag size={16} /></div>
|
||||
<input class="input placeholder-surface-400" type="text" bind:value={formData.tagline} placeholder="Software Architect & Visionary" />
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<!-- Professional Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Professional</legend>
|
||||
|
||||
<label class="label">
|
||||
<span>Professional Title</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Briefcase size={16} /></div>
|
||||
<input class="input placeholder-surface-400" type="text" bind:value={formData.professional_title} placeholder="Senior Engineer" />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span>Affiliations</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Building size={16} /></div>
|
||||
<input class="input placeholder-surface-400" type="text" bind:value={formData.affiliations} placeholder="One Sky IT, LLC" />
|
||||
</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<!-- Metadata Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Status & Flags</legend>
|
||||
|
||||
<div class="flex flex-wrap gap-4 pt-2">
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.enable} />
|
||||
<span>Enabled</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.hide} />
|
||||
<span>Hidden</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.priority} />
|
||||
<span>Priority</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="label">
|
||||
<span>Notes (Internal)</span>
|
||||
<textarea class="textarea placeholder-surface-400" rows="3" bind:value={formData.notes} placeholder="Additional details..."></textarea>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<footer class="flex justify-end gap-2 border-t border-surface-500/30 pt-4">
|
||||
<button type="submit" class="btn variant-filled-primary w-full md:w-auto" disabled={is_loading}>
|
||||
{#if is_loading}
|
||||
<span class="animate-spin mr-2">⏳</span>
|
||||
{/if}
|
||||
{person ? 'Update Person Record' : 'Create Person Record'}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
@@ -379,7 +379,7 @@
|
||||
});
|
||||
console.log(country_data);
|
||||
address['country'] = country_data[0].english_short_name; // Assume there is only one match
|
||||
// address['country_name'] = country_data[0].english_short_name;
|
||||
// address['country_name'] = country_data[0].english_short_name; // DO NOT USE - Scott 2026-01-09
|
||||
}
|
||||
event_do['location_address_json'] = address;
|
||||
|
||||
|
||||
@@ -16,24 +16,6 @@
|
||||
</script>
|
||||
|
||||
<section class="journal-metadata w-full space-y-4">
|
||||
<!-- Original Date/Time Info -->
|
||||
{#if entry.original_datetime || entry.original_timezone}
|
||||
<div class="flex flex-col sm:flex-row gap-4 p-3 bg-surface-500/5 rounded-lg border border-surface-500/20">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs font-semibold uppercase tracking-wider text-surface-500">Original Date/Time</span>
|
||||
<span class="text-sm font-medium">
|
||||
{entry.original_datetime ? ae_util.iso_datetime_formatter(entry.original_datetime, 'datetime_12_long') : '--'}
|
||||
</span>
|
||||
</div>
|
||||
{#if entry.original_timezone}
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs font-semibold uppercase tracking-wider text-surface-500">Timezone</span>
|
||||
<span class="text-sm font-medium">{entry.original_timezone}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- System Timestamps -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center gap-2 px-1 text-xs text-surface-500">
|
||||
<div class="flex gap-4">
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
journals_slct
|
||||
} from '$lib/ae_journals/ae_journals_stores';
|
||||
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
|
||||
import type { ae_JournalEntry } from '$lib/types/ae_types';
|
||||
|
||||
import Journal_entry_obj_qry from './../ae_comp__journal_entry_obj_qry.svelte';
|
||||
|
||||
@@ -292,7 +293,7 @@
|
||||
Entries...
|
||||
</option>
|
||||
<!-- loop through each key value -->
|
||||
{#each Object.entries($journals_loc.entry_view_history_kv).reverse() as [journal_entry_id, journal_entry_obj]}
|
||||
{#each Object.entries($journals_loc.entry_view_history_kv as Record<string, ae_JournalEntry>).reverse() as [journal_entry_id, journal_entry_obj]}
|
||||
<option value={journal_entry_obj.id}>
|
||||
{(journal_entry_obj?.name || journal_entry_obj?.id) ?? 'NONE'}
|
||||
</option>
|
||||
@@ -428,11 +429,3 @@
|
||||
{@render children?.()}
|
||||
<!-- </div> -->
|
||||
</section>
|
||||
|
||||
<!-- {#if $journals_sess.show__modal_edit__journal_obj}
|
||||
<Journal_obj_id_edit
|
||||
log_lvl={log_lvl}
|
||||
lq__journal_obj={lq__journal_obj}
|
||||
show={$journals_sess.show__modal_edit__journal_obj}
|
||||
/>
|
||||
{/if} -->
|
||||
|
||||
Reference in New Issue
Block a user