377 lines
16 KiB
Svelte
377 lines
16 KiB
Svelte
<script lang="ts">
|
|
import type { ConfigurationData } from "../types";
|
|
import { formatDate, configStatusClass } from "../types";
|
|
|
|
export let configurations: ConfigurationData[];
|
|
export let isMobile: boolean;
|
|
|
|
// Configurations split-view state
|
|
let selectedConfig: ConfigurationData | null = null;
|
|
let configFadeKey = 0;
|
|
|
|
// Track which password fields are revealed (by question id)
|
|
let revealedPasswords: Record<number, boolean> = {};
|
|
|
|
function togglePassword(questionId: number) {
|
|
revealedPasswords[questionId] = !revealedPasswords[questionId];
|
|
revealedPasswords = revealedPasswords;
|
|
}
|
|
|
|
function selectConfig(config: ConfigurationData) {
|
|
if (selectedConfig?.id === config.id) {
|
|
selectedConfig = null;
|
|
} else {
|
|
selectedConfig = config;
|
|
configFadeKey++;
|
|
revealedPasswords = {};
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if configurations.length === 0}
|
|
<div class="tab-empty">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
class="tab-empty-icon"
|
|
>
|
|
<path
|
|
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
|
/>
|
|
</svg>
|
|
<p>No configurations found</p>
|
|
</div>
|
|
{:else}
|
|
<div
|
|
class="config-split"
|
|
class:expanded={selectedConfig !== null && !isMobile}
|
|
>
|
|
<!-- Left side: config buttons -->
|
|
<div
|
|
class="config-list"
|
|
class:collapsed={selectedConfig !== null && !isMobile}
|
|
>
|
|
{#each configurations as config (config.id)}
|
|
<button
|
|
class="config-item"
|
|
class:selected={selectedConfig?.id === config.id}
|
|
class:config-inactive={config.status?.name === "Inactive" ||
|
|
config.status?.name === "Automate Inactive"}
|
|
on:click={() => selectConfig(config)}
|
|
type="button"
|
|
>
|
|
<div class="config-item-header">
|
|
<div class="config-name-group">
|
|
<span
|
|
class="config-status-dot dot-{configStatusClass(
|
|
config.status?.name,
|
|
)}"
|
|
title={config.status?.name ?? "Unknown"}
|
|
></span>
|
|
<span class="config-name">{config.name}</span>
|
|
</div>
|
|
<div class="config-header-badges">
|
|
{#if config.status?.name && (!selectedConfig || isMobile)}
|
|
<span
|
|
class="config-status-badge status-{configStatusClass(
|
|
config.status.name,
|
|
)}">{config.status.name}</span
|
|
>
|
|
{/if}
|
|
{#if config.type?.name && (!selectedConfig || isMobile)}
|
|
<span class="config-type-badge">{config.type.name}</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{#if !selectedConfig || isMobile}
|
|
{#if config.description}
|
|
<p class="config-description">{config.description}</p>
|
|
{/if}
|
|
{#if config.key}
|
|
<div class="config-kv">
|
|
<span class="config-key">{config.key}</span>
|
|
{#if config.value}
|
|
<span class="config-value">{config.value}</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{#if formatDate(config.updatedAt) || formatDate(config.createdAt) || formatDate(config.info?.lastUpdated) || formatDate(config.info?.dateEntered)}
|
|
<span class="config-date">
|
|
{#if formatDate(config.updatedAt)}
|
|
Updated {formatDate(config.updatedAt)}
|
|
{:else if formatDate(config.info?.lastUpdated)}
|
|
Updated {formatDate(config.info?.lastUpdated)}
|
|
{:else if formatDate(config.createdAt)}
|
|
Created {formatDate(config.createdAt)}
|
|
{:else if formatDate(config.info?.dateEntered)}
|
|
Created {formatDate(config.info?.dateEntered)}
|
|
{/if}
|
|
</span>
|
|
{/if}
|
|
{/if}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- Right side: config detail panel -->
|
|
{#if selectedConfig}
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<div
|
|
class="bottom-sheet-overlay"
|
|
class:active={selectedConfig !== null}
|
|
on:click={() => {
|
|
selectedConfig = null;
|
|
}}
|
|
>
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<div class="bottom-sheet-panel" on:click|stopPropagation>
|
|
<div class="bottom-sheet-handle"></div>
|
|
<div class="bottom-sheet-body">
|
|
<div class="config-detail-panel">
|
|
{#key configFadeKey}
|
|
<div class="config-detail-content">
|
|
<div class="config-detail-header">
|
|
<div class="config-detail-header-left">
|
|
<h3 class="config-detail-title">
|
|
{selectedConfig.name}
|
|
</h3>
|
|
<div class="config-detail-meta-badges">
|
|
{#if selectedConfig.type?.name}
|
|
<span class="config-badge type"
|
|
>{selectedConfig.type.name}</span
|
|
>
|
|
{/if}
|
|
{#if selectedConfig.status?.name}
|
|
<span
|
|
class="config-badge status-{configStatusClass(
|
|
selectedConfig.status.name,
|
|
)}"
|
|
>
|
|
{selectedConfig.status.name}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="config-detail-close"
|
|
on:click={() => (selectedConfig = null)}
|
|
aria-label="Close detail view"
|
|
type="button"
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="16"
|
|
height="16"
|
|
>
|
|
<path d="M18 6L6 18M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{#if selectedConfig.serialNumber}
|
|
<div class="config-serial">
|
|
<span class="config-serial-label">Serial #</span>
|
|
<span class="config-serial-value"
|
|
>{selectedConfig.serialNumber}</span
|
|
>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Questions / Fields -->
|
|
{#if (selectedConfig.questions && selectedConfig.questions.length > 0) || selectedConfig.notes}
|
|
<div class="config-questions">
|
|
{#if selectedConfig.notes}
|
|
<div class="config-notes">
|
|
<h4 class="config-section-title">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="16"
|
|
height="16"
|
|
>
|
|
<path
|
|
d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"
|
|
/>
|
|
<polyline points="14 2 14 8 20 8" />
|
|
<line x1="16" y1="13" x2="8" y2="13" />
|
|
<line x1="16" y1="17" x2="8" y2="17" />
|
|
<polyline points="10 9 9 9 8 9" />
|
|
</svg>
|
|
Notes
|
|
</h4>
|
|
<p class="config-notes-text">
|
|
{selectedConfig.notes}
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
<h4 class="config-section-title">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="16"
|
|
height="16"
|
|
>
|
|
<path
|
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"
|
|
/>
|
|
<rect x="9" y="3" width="6" height="4" rx="1" />
|
|
</svg>
|
|
Configuration Details
|
|
</h4>
|
|
<div class="questions-grid">
|
|
{#each selectedConfig.questions as q (q.id)}
|
|
<div
|
|
class="question-row"
|
|
class:has-answer={!!q.answer}
|
|
>
|
|
<span class="question-label">{q.question}</span>
|
|
<div class="question-value-wrap">
|
|
{#if q.fieldType === "Password"}
|
|
<span class="question-value password-value">
|
|
{#if revealedPasswords[q.id]}
|
|
{q.answer || "—"}
|
|
{:else}
|
|
{q.answer ? "••••••••" : "—"}
|
|
{/if}
|
|
</span>
|
|
{#if q.answer}
|
|
<button
|
|
class="password-toggle"
|
|
on:click={() => togglePassword(q.id)}
|
|
type="button"
|
|
aria-label={revealedPasswords[q.id]
|
|
? "Hide password"
|
|
: "Show password"}
|
|
>
|
|
{#if revealedPasswords[q.id]}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path
|
|
d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94"
|
|
/>
|
|
<path
|
|
d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19"
|
|
/>
|
|
<path
|
|
d="M14.12 14.12a3 3 0 11-4.24-4.24"
|
|
/>
|
|
<line x1="1" y1="1" x2="23" y2="23" />
|
|
</svg>
|
|
{:else}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path
|
|
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
|
/>
|
|
<circle cx="12" cy="12" r="3" />
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
{/if}
|
|
{:else if q.fieldType === "TextArea"}
|
|
<span class="question-value textarea-value"
|
|
>{q.answer || "—"}</span
|
|
>
|
|
{:else}
|
|
<span class="question-value"
|
|
>{q.answer || "—"}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{:else if !selectedConfig.notes}
|
|
<div class="config-no-questions">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
width="32"
|
|
height="32"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3" />
|
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
</svg>
|
|
<p>No configuration fields available</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Footer metadata -->
|
|
{#if selectedConfig.info}
|
|
<div class="config-info-footer">
|
|
{#if selectedConfig.info.enteredBy || selectedConfig.info.dateEntered}
|
|
<div class="config-info-item">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="13"
|
|
height="13"
|
|
>
|
|
<path d="M12 5v14M5 12h14" />
|
|
</svg>
|
|
Created{#if selectedConfig.info.enteredBy} by
|
|
<strong>{selectedConfig.info.enteredBy}</strong
|
|
>{/if}{#if selectedConfig.info.dateEntered} on
|
|
{formatDate(selectedConfig.info.dateEntered)}{/if}
|
|
</div>
|
|
{/if}
|
|
{#if selectedConfig.info.updatedBy || selectedConfig.info.lastUpdated}
|
|
<div class="config-info-item">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="13"
|
|
height="13"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<polyline points="12 6 12 12 16 14" />
|
|
</svg>
|
|
Updated{#if selectedConfig.info.updatedBy} by
|
|
<strong>{selectedConfig.info.updatedBy}</strong
|
|
>{/if}{#if selectedConfig.info.lastUpdated} on
|
|
{formatDate(selectedConfig.info.lastUpdated)}{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/key}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|