Files
OSIT-AE-App-Svelte/src/routes/journals/ae_comp__journal_obj_id_edit.svelte
2026-05-15 13:04:50 -04:00

817 lines
38 KiB
Svelte

<script lang="ts">
import { untrack } from 'svelte';
/**
* ae_comp__journal_obj_id_edit.svelte
* Standardized Journal-level configuration.
* Restored missing visibility and button toggles.
*/
import {
BetweenVerticalEnd,
BetweenVerticalStart,
BookHeart,
BookOpenText,
BriefcaseBusiness,
CalendarClock,
Check,
CodeXml,
Copy,
Database,
Expand,
Eye,
EyeOff,
FileDown,
FilePlus,
FileUp,
FingerprintPattern,
Globe,
Layout,
LockKeyhole,
MessageSquareWarning,
Minus,
MonitorPlay,
MousePointerClick,
Palette,
Plus,
Settings,
ShieldCheck,
Siren,
Target,
Trash2,
X,
Zap
} from '@lucide/svelte';
import { Modal } from 'flowbite-svelte';
import { goto } from '$app/navigation';
// *** Import Aether specific variables and functions
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import {
journals_loc,
journals_sess,
journals_slct
} from '$lib/ae_journals/ae_journals_stores';
import { journals_func } from '$lib/ae_journals/ae_journals_functions';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
interface Props {
log_lvl?: number;
lq__journal_obj: any;
show?: boolean;
on_new_entry?: () => void;
on_show_export?: () => void;
on_show_import?: () => void;
}
let {
log_lvl = $bindable(0),
lq__journal_obj,
show = $bindable(false),
on_new_entry,
on_show_export,
on_show_import
}: Props = $props();
// *** Internal State
let tab: 'actions' | 'general' | 'security' | 'ui' | 'json' = $state('actions');
let tmp__journal_obj: any = $state({});
// Deep copy on mount or when lq changes to ensure we have a working copy
$effect(() => {
if (show && $lq__journal_obj) {
const source_id = $lq__journal_obj.journal_id;
untrack(() => {
if (
!tmp__journal_obj.journal_id ||
tmp__journal_obj.journal_id !== source_id
) {
tmp__journal_obj = JSON.parse(JSON.stringify($lq__journal_obj));
// Ensure cfg_json exists
if (!tmp__journal_obj.cfg_json) tmp__journal_obj.cfg_json = {};
}
});
}
});
async function handle_update_journal(close_modal: boolean = true) {
if (!tmp__journal_obj.name || !tmp__journal_obj.type_code) {
alert('Please provide both name and type for the journal.');
return;
}
try {
// DEFINITIVE BASE TABLE COLUMNS ONLY
const data_kv = {
name: tmp__journal_obj.name,
short_name: tmp__journal_obj.short_name,
type_code: tmp__journal_obj.type_code,
description: tmp__journal_obj.description,
summary: tmp__journal_obj.summary,
outline: tmp__journal_obj.outline,
passcode: tmp__journal_obj.passcode,
private_passcode: tmp__journal_obj.private_passcode,
public_passcode: tmp__journal_obj.public_passcode,
passcode_timeout: tmp__journal_obj.passcode_timeout,
auth_key: tmp__journal_obj.auth_key,
allow_auth: tmp__journal_obj.allow_auth,
cfg_json: tmp__journal_obj.cfg_json,
data_json: tmp__journal_obj.data_json,
enable: tmp__journal_obj.enable,
hide: tmp__journal_obj.hide,
priority: tmp__journal_obj.priority,
sort: tmp__journal_obj.sort,
group: tmp__journal_obj.group,
notes: tmp__journal_obj.notes,
alert: tmp__journal_obj.alert,
alert_msg: tmp__journal_obj.alert_msg,
archive_on: tmp__journal_obj.archive_on,
default_private: tmp__journal_obj.default_private,
default_public: tmp__journal_obj.default_public,
default_personal: tmp__journal_obj.default_personal,
default_professional: tmp__journal_obj.default_professional
};
await journals_func.update_ae_obj__journal({
api_cfg: $ae_api,
journal_id: $lq__journal_obj?.journal_id,
data_kv: data_kv,
log_lvl: log_lvl
});
if (close_modal) show = false;
} catch (error) {
console.error('Error updating journal:', error);
if (close_modal) alert('Failed to update journal.');
}
}
async function delete_journal() {
if (
confirm(
`CRITICAL WARNING: Are you sure you want to delete the journal "${$lq__journal_obj.name}"? This will delete all entries associated with it.`
)
) {
try {
await journals_func.delete_ae_obj_id__journal({
api_cfg: $ae_api,
journal_id: $lq__journal_obj?.journal_id,
log_lvl: log_lvl
});
alert('Journal deleted successfully!');
goto('/journals');
} catch (error) {
console.error('Error deleting journal:', error);
alert('Failed to delete journal.');
}
}
}
</script>
<Modal
bind:open={show}
autoclose={false}
dismissable={false}
placement="top-center"
size="xl"
class="relative mx-auto flex w-full flex-col rounded-lg border border-orange-300 bg-white text-gray-800 shadow-xl dark:border-orange-700 dark:bg-gray-800 dark:text-gray-200"
headerClass="flex flex-row gap-2 items-center justify-between w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-t-lg border-b border-orange-200 dark:border-orange-800"
footerClass="flex flex-row gap-2 items-center justify-center w-full bg-orange-100 dark:bg-orange-900 p-4 rounded-b-lg border-t border-orange-200 dark:border-orange-800">
{#snippet header()}
<h3 class="flex flex-1 items-center gap-2 text-lg font-bold">
<Settings class="text-primary-500" />
<span>Journal Config: {$lq__journal_obj?.name ?? '--'}</span>
</h3>
<button
type="button"
class="btn-icon btn-icon-sm preset-tonal-surface ml-2"
onclick={() => (show = false)}>
<X size="1.1em" />
</button>
{/snippet}
<div class="h-[75vh] space-y-6 overflow-y-auto px-4 py-2">
<!-- Navigation Tabs -->
<div
class="bg-surface-500/10 sticky top-0 z-10 mx-auto mb-4 flex max-w-fit justify-center gap-1 rounded-lg p-1 backdrop-blur-sm">
<button
type="button"
class="btn btn-sm transition-all {tab === 'actions'
? 'preset-filled-primary'
: 'preset-tonal-surface'}"
onclick={() => (tab = 'actions')}>
<Zap size="1.1em" class="mr-1" /> Quick Actions
</button>
<button
type="button"
class="btn btn-sm transition-all {tab === 'general'
? 'preset-filled-primary'
: 'preset-tonal-surface'}"
onclick={() => (tab = 'general')}>
<Layout size="1.1em" class="mr-1" /> General
</button>
<button
type="button"
class="btn btn-sm transition-all {tab === 'security'
? 'preset-filled-primary'
: 'preset-tonal-surface'}"
onclick={() => (tab = 'security')}>
<ShieldCheck size="1.1em" class="mr-1" /> Status & Security
</button>
<button
type="button"
class="btn btn-sm transition-all {tab === 'ui'
? 'preset-filled-primary'
: 'preset-tonal-surface'}"
onclick={() => (tab = 'ui')}>
<Palette size="1.1em" class="mr-1" /> UI/Visuals
</button>
<button
type="button"
class="btn btn-sm transition-all {tab === 'json'
? 'preset-filled-primary'
: 'preset-tonal-surface'}"
onclick={() => (tab = 'json')}>
<CodeXml size="1.1em" class="mr-1" /> JSON
</button>
</div>
{#if tab === 'actions'}
<div class="animate-in fade-in space-y-6 duration-300">
<section class="grid grid-cols-1 gap-4 md:grid-cols-2">
<button
type="button"
class="btn preset-tonal-secondary w-full py-4 text-lg font-bold"
onclick={() => {
show = false;
on_new_entry?.();
}}>
<FilePlus size="1.5em" class="mr-2" /> New Journal Entry
</button>
<button
type="button"
class="btn preset-tonal-surface w-full py-4"
onclick={() => {
show = false;
on_show_export?.();
}}>
<FileDown size="1.5em" class="mr-2" /> Export Entries
</button>
<button
type="button"
class="btn preset-tonal-surface w-full py-4"
onclick={() => {
show = false;
on_show_import?.();
}}>
<FileUp size="1.5em" class="mr-2" /> Import Entries
</button>
</section>
</div>
{:else if tab === 'general'}
<div class="animate-in fade-in space-y-6 duration-300">
<!-- Core Meta -->
<section class="grid grid-cols-1 gap-4 p-2">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Journal Name</span>
<input
type="text"
bind:value={tmp__journal_obj.name}
class="input"
placeholder="e.g. Personal Log" />
</label>
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Description (Markdown)</span>
<div class="w-full">
{#if tmp__journal_obj?.cfg_json?.pref_editor === 'codemirror'}
<AE_Comp_Editor_CodeMirror
bind:content={tmp__journal_obj.description}
language="markdown"
theme_mode={$ae_loc.theme_mode}
placeholder="Describe the purpose of this journal..."
class_li="rounded-lg border border-surface-500/20" />
{:else}
<textarea
bind:value={tmp__journal_obj.description}
class="textarea h-32"
placeholder="Describe the purpose of this journal..."
></textarea>
{/if}
</div>
</label>
<div class="grid grid-cols-2 gap-4">
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Type</span>
<select
bind:value={tmp__journal_obj.type_code}
class="select">
{#each $journals_loc.journal.type_code_li as type (type.code)}
<option value={type.code}
>{type.name}</option>
{/each}
</select>
</label>
<label class="label flex flex-col items-start gap-1">
<span class="text-sm font-bold opacity-70"
>Journal Group (text)</span>
<input
type="text"
bind:value={tmp__journal_obj.group}
class="input"
placeholder="Standard" />
</label>
</div>
</section>
<!-- Categories -->
<section class="space-y-4 p-2">
<h2
class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
<MonitorPlay size="1em" class="text-primary-500" />
Journal Categories
</h2>
<div
class="bg-surface-500/5 border-surface-500/10 space-y-2 rounded-lg border p-4">
{#each tmp__journal_obj?.cfg_json?.category_li ?? [] as category, i (category.code ?? i)}
<div class="flex items-center gap-2">
<input
type="text"
bind:value={category.code}
class="input input-sm w-32"
placeholder="Code" />
<input
type="text"
bind:value={category.name}
class="input input-sm grow"
placeholder="Display Name" />
<button
type="button"
class="btn-icon btn-icon-sm preset-tonal-error"
onclick={() => {
tmp__journal_obj.cfg_json.category_li.splice(
i,
1
);
}}>
<Minus size="1em" />
</button>
</div>
{/each}
<button
type="button"
class="btn btn-sm preset-tonal-primary mt-2 w-full"
onclick={() => {
if (!tmp__journal_obj.cfg_json.category_li)
tmp__journal_obj.cfg_json.category_li = [];
tmp__journal_obj.cfg_json.category_li.push({
code: '',
name: ''
});
}}>
<Plus size="1em" class="mr-1" /> Add Category
</button>
</div>
</section>
</div>
{:else if tab === 'security'}
<div class="animate-in fade-in space-y-6 duration-300">
<!-- Status & Lifecycle -->
<section class="space-y-4 p-2">
<h2
class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
<FingerprintPattern size="1.2em" class="text-primary-500" />
Status & Lifecycle
</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label
class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
<input
type="checkbox"
bind:checked={tmp__journal_obj.enable}
onchange={() => handle_update_journal(false)}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Enabled</span>
<span class="text-xs opacity-60"
>Allow access to this journal</span>
</div>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
<input
type="checkbox"
bind:checked={tmp__journal_obj.hide}
onchange={() => handle_update_journal(false)}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Hidden</span>
<span class="text-xs opacity-60"
>Hide from standard lists</span>
</div>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 hover:bg-surface-500/10 flex cursor-pointer items-center space-x-3 rounded-lg border p-3 transition-colors">
<input
type="checkbox"
bind:checked={tmp__journal_obj.priority}
onchange={() => handle_update_journal(false)}
class="checkbox checkbox-primary" />
<div class="flex flex-col">
<span class="font-bold">Priority Journal</span>
<span class="text-xs opacity-60"
>Star or pin to top</span>
</div>
</label>
<div
class="bg-surface-500/5 border-surface-500/10 flex items-center justify-between rounded-lg border p-3">
<div class="flex flex-col">
<span class="text-sm font-bold"
>Sort Order</span>
<span class="text-xs opacity-60"
>Manual list position</span>
</div>
<div class="flex items-center gap-2">
<button
type="button"
class="btn-icon btn-icon-sm preset-tonal-surface"
onclick={() => {
tmp__journal_obj.sort =
(tmp__journal_obj.sort ?? 0) - 1;
handle_update_journal(false);
}}><Minus size="1em" /></button>
<span
class="w-8 text-center font-mono text-lg font-bold"
>{tmp__journal_obj.sort ?? 0}</span>
<button
type="button"
class="btn-icon btn-icon-sm preset-tonal-surface"
onclick={() => {
tmp__journal_obj.sort =
(tmp__journal_obj.sort ?? 0) + 1;
handle_update_journal(false);
}}><Plus size="1em" /></button>
</div>
</div>
</div>
</section>
<!-- Encryption Passcodes -->
<section class="space-y-4 p-2">
<h2
class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
<LockKeyhole size="1.2em" class="text-primary-500" />
Encryption Passcodes
</h2>
<div
class="bg-warning-500/10 border-warning-500/30 space-y-4 rounded-lg border p-4 shadow-inner">
<label class="label">
<span
class="text-xs font-bold tracking-wider uppercase opacity-70"
>Primary Passcode (Stored)</span>
<div class="flex gap-2">
<input
type="password"
bind:value={tmp__journal_obj.passcode}
class="input grow"
placeholder="Module-level passcode" />
<FingerprintPattern class="opacity-30" />
</div>
</label>
<label class="label">
<span
class="text-xs font-bold tracking-wider uppercase opacity-70"
>Private Passcode (Double Encryption)</span>
<div class="flex gap-2">
<input
type="password"
bind:value={
tmp__journal_obj.private_passcode
}
class="input grow"
placeholder="User-level secret" />
<ShieldCheck class="opacity-30" />
</div>
</label>
<div
class="text-warning-700 dark:text-warning-300 text-[10px] italic">
* Note: Passcodes are used locally for E2EE. Primary
is often stored in the DB, while Private should
remain known only to you.
</div>
</div>
</section>
<section class="pt-8">
<button
type="button"
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 w-full shadow-lg"
onclick={delete_journal}>
<Trash2 size="1.1em" class="mr-2" /> Delete Entire Journal
</button>
</section>
</div>
{:else if tab === 'ui'}
<div class="animate-in fade-in space-y-8 duration-300">
<section class="space-y-4 p-2">
<h2
class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
<MonitorPlay size="1.2em" class="text-primary-500" />
Default View Modes
</h2>
<div
class="bg-surface-500/5 border-surface-500/10 grid grid-cols-1 gap-6 rounded-lg border p-4 md:grid-cols-2">
<label class="label">
<span class="text-sm font-bold opacity-70"
>Preferred Viewer</span>
<select
bind:value={
tmp__journal_obj.cfg_json.pref_viewer
}
class="select">
<option value="rendered"
>Rendered HTML (Default)</option>
<option value="plain">Plain Text</option>
<option value="codemirror"
>CodeMirror (Syntax)</option>
</select>
</label>
<label class="label">
<span class="text-sm font-bold opacity-70"
>Preferred Editor</span>
<select
bind:value={
tmp__journal_obj.cfg_json.pref_editor
}
class="select">
<option value="textarea"
>Standard Textarea</option>
<option value="codemirror"
>CodeMirror (Advanced)</option>
</select>
</label>
<label class="label">
<span class="text-sm font-bold opacity-70"
>Color Scheme</span>
<select
bind:value={
tmp__journal_obj.cfg_json.color_scheme
}
class="select">
<option value="">Default (Slate)</option>
<option value="blue">Deep Blue</option>
<option value="green">Nature Green</option>
<option value="orange">Aether Orange</option>
<option value="red">Warning Red</option>
<option value="yellow">Caution Yellow</option>
</select>
</label>
<label class="label">
<span class="text-sm font-bold opacity-70"
>Quick Add Placement</span>
<select
bind:value={
tmp__journal_obj.cfg_json.entry_add_text
}
class="select">
<option value="append"
>Append to End (Default)</option>
<option value="prepend"
>Prepend to Start</option>
</select>
</label>
</div>
</section>
<!-- Visibility Toggles (Restored) -->
<section class="space-y-4 p-2">
<h2
class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
<Eye size="1.2em" class="text-primary-500" />
Entry Visibility (Global)
</h2>
<div
class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
<label
class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json.hide_private
}
class="checkbox checkbox-sm" />
<span class="text-xs font-bold">Hide Private</span>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json.hide_personal
}
class="checkbox checkbox-sm" />
<span class="text-xs font-bold">Hide Personal</span>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex cursor-pointer items-center space-x-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json.hide_professional
}
class="checkbox checkbox-sm" />
<span class="text-xs font-bold"
>Hide Professional</span>
</label>
</div>
</section>
<!-- Button Toggles (Restored) -->
<section class="space-y-4 p-2">
<h2
class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
<Settings size="1.2em" class="text-primary-500" />
Entry Feature Buttons
</h2>
<div
class="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4">
<label
class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json.hide_btn_alert
}
class="checkbox checkbox-sm" />
<span class="text-[10px] font-bold uppercase"
>Hide Alert</span>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json.hide_btn_private
}
class="checkbox checkbox-sm" />
<span class="text-[10px] font-bold uppercase"
>Hide Private</span>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json.hide_btn_public
}
class="checkbox checkbox-sm" />
<span class="text-[10px] font-bold uppercase"
>Hide Public</span>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json.hide_btn_personal
}
class="checkbox checkbox-sm" />
<span class="text-[10px] font-bold uppercase"
>Hide Pers.</span>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json
.hide_btn_professional
}
class="checkbox checkbox-sm" />
<span class="text-[10px] font-bold uppercase"
>Hide Prof.</span>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json.hide_btn_template
}
class="checkbox checkbox-sm" />
<span class="text-[10px] font-bold uppercase"
>Hide Templ.</span>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json.hide_copy_plain_md
}
class="checkbox checkbox-sm" />
<span class="text-[10px] font-bold uppercase"
>Hide Copy MD</span>
</label>
<label
class="bg-surface-500/5 border-surface-500/10 flex items-center gap-2 rounded border p-2">
<input
type="checkbox"
bind:checked={
tmp__journal_obj.cfg_json.hide_clone
}
class="checkbox checkbox-sm" />
<span class="text-[10px] font-bold uppercase"
>Hide Clone</span>
</label>
</div>
</section>
<section class="space-y-4 p-2">
<h2
class="border-surface-500/30 flex items-center gap-2 border-b pb-2 text-lg font-bold">
<Layout size="1.2em" class="text-primary-500" />
List Interactions
</h2>
<div class="bg-surface-500/5 space-y-4 rounded-lg p-4">
<label class="label">
<span class="text-xs font-bold opacity-70"
>Expansion Trigger</span>
<select
bind:value={
tmp__journal_obj.cfg_json.expand_li_content
}
class="select">
<option value="click">Click to Expand</option>
<option value="hover">Hover to Expand</option>
</select>
</label>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<label class="label">
<span class="text-xs font-bold opacity-70"
>List Max Height</span>
<select
bind:value={
tmp__journal_obj.cfg_json
.entry_li_max_height
}
class="select select-sm">
<option value="">Default</option>
<option value="max-h-16">Small (16)</option>
<option value="max-h-32"
>Medium (32)</option>
<option value="max-h-64">Large (64)</option>
<option value="max-h-full">Full</option>
</select>
</label>
<label class="label">
<span class="text-xs font-bold opacity-70"
>Active Max Height</span>
<select
bind:value={
tmp__journal_obj.cfg_json
.entry_li_click_max_height
}
class="select select-sm">
<option value="">Default</option>
<option value="active:max-h-64"
>Large (64)</option>
<option value="active:max-h-96"
>X-Large (96)</option>
<option value="active:max-h-full"
>Full</option>
</select>
</label>
</div>
</div>
</section>
</div>
{:else if tab === 'json'}
<div class="h-full min-h-[400px]">
<AE_Comp_Editor_CodeMirror
readonly={true}
content={JSON.stringify(tmp__journal_obj, null, 2)}
theme_mode={$ae_loc.theme_mode}
class_li="rounded-lg border border-surface-500/30" />
</div>
{/if}
</div>
{#snippet footer()}
<div class="flex gap-4">
<button
type="button"
class="btn preset-tonal-surface min-w-[100px] font-bold"
onclick={() => (show = false)}>
<X size="1.2em" class="mr-2" /> Cancel
</button>
<button
type="button"
class="btn preset-filled-primary min-w-[120px] font-bold"
onclick={() => handle_update_journal(true)}>
<Check size="1.2em" class="mr-2" /> Save Changes
</button>
</div>
{/snippet}
</Modal>