Deployment Script, Postgres migration, UX improvements
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import MixCalculatorWorkspace from '$lib/components/MixCalculatorWorkspace.svelte';
|
||||
import MixCalculatorEditor from '$lib/components/mix-calculator/MixCalculatorEditor.svelte';
|
||||
import { featureFlags } from '$lib/features';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
{#if data.session}
|
||||
<MixCalculatorWorkspace initialSession={data.session} options={data.options} />
|
||||
<MixCalculatorEditor initialSession={data.session} options={data.options} />
|
||||
{:else}
|
||||
<section class="locked-card">
|
||||
<p class="eyebrow">Mix Calculator</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import MixCalculatorWorkspace from '$lib/components/MixCalculatorWorkspace.svelte';
|
||||
import MixCalculatorEditor from '$lib/components/mix-calculator/MixCalculatorEditor.svelte';
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<MixCalculatorWorkspace options={data.options} />
|
||||
<MixCalculatorEditor options={data.options} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MixWorkspace from '$lib/components/MixWorkspace.svelte';
|
||||
import MixEditor from '$lib/components/mixes/MixEditor.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<MixWorkspace rawMaterials={data.rawMaterials} initialMix={data.mix} />
|
||||
<MixEditor rawMaterials={data.rawMaterials} initialMix={data.mix} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MixWorkspace from '$lib/components/MixWorkspace.svelte';
|
||||
import MixEditor from '$lib/components/mixes/MixEditor.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<MixWorkspace rawMaterials={data.rawMaterials} />
|
||||
<MixEditor rawMaterials={data.rawMaterials} />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { api } from '$lib/api';
|
||||
import AppSecondaryRail from '$lib/components/navigation/AppSecondaryRail.svelte';
|
||||
import AppSecondaryRailLayout from '$lib/components/navigation/AppSecondaryRailLayout.svelte';
|
||||
import { clientSession } from '$lib/session';
|
||||
import { toast } from '$lib/toast';
|
||||
import { BarChart3, CirclePlus, Wheat } from 'lucide-svelte';
|
||||
@@ -47,7 +49,10 @@
|
||||
}
|
||||
];
|
||||
|
||||
const railGroups = [...new Set(railItems.map((item) => item.group))];
|
||||
const railGroups = [...new Set(railItems.map((item) => item.group))].map((group) => ({
|
||||
label: group,
|
||||
items: railItems.filter((item) => item.group === group)
|
||||
}));
|
||||
let activeView = $state<RawMaterialsView>('overview');
|
||||
const pageSize = 20;
|
||||
let overviewMixesPage = $state(1);
|
||||
@@ -229,38 +234,18 @@
|
||||
<p class="feedback error">{errorMessage}</p>
|
||||
{/if}
|
||||
|
||||
<div class="workspace-layout">
|
||||
<nav class="workspace-nav" aria-label="Raw materials navigation">
|
||||
<p class="nav-section-label">Raw Materials</p>
|
||||
|
||||
<div class="nav-identity">
|
||||
<div class="nav-avatar" aria-hidden="true">
|
||||
<Wheat size={16} strokeWidth={1.75} />
|
||||
</div>
|
||||
<div class="nav-identity-text">
|
||||
<p class="identity-name">{activeMaterials.length} active inputs</p>
|
||||
<p class="identity-role">{data.rawMaterials.length} tracked materials</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each railGroups as group}
|
||||
<div class="nav-group">
|
||||
<p class="nav-group-label">{group}</p>
|
||||
{#each railItems.filter((item) => item.group === group) as item}
|
||||
{@const Icon = item.icon}
|
||||
<button
|
||||
type="button"
|
||||
class="nav-item"
|
||||
class:active={activeView === item.id}
|
||||
onclick={() => (activeView = item.id)}
|
||||
>
|
||||
<span class="nav-icon"><Icon size={18} strokeWidth={1.75} /></span>
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
<AppSecondaryRailLayout>
|
||||
{#snippet rail()}
|
||||
<AppSecondaryRail
|
||||
sectionLabel="Raw Materials"
|
||||
identityTitle={`${activeMaterials.length} active inputs`}
|
||||
identitySubtitle={`${data.rawMaterials.length} tracked materials`}
|
||||
identityIcon={Wheat}
|
||||
groups={railGroups}
|
||||
activeId={activeView}
|
||||
onSelect={(id) => (activeView = id as RawMaterialsView)}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<div class="workspace-panel">
|
||||
{#if activeRailItem}
|
||||
@@ -620,7 +605,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppSecondaryRailLayout>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@@ -651,7 +636,7 @@
|
||||
|
||||
.locked-card,
|
||||
.feedback,
|
||||
.workspace-layout {
|
||||
:global(.secondary-rail-layout) {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
@@ -688,82 +673,6 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workspace-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 15rem minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
min-height: calc(100vh - 8.5rem);
|
||||
max-height: calc(100vh - 8.5rem);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 1.15rem;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workspace-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 8.5rem);
|
||||
padding: 1.1rem 0.85rem 0.85rem;
|
||||
background: var(--panel);
|
||||
border-right: 1px solid var(--line);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-section-label {
|
||||
margin: 0 0.55rem 0.3rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0 0.25rem 0.9rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.nav-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--green-deep);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-identity-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.identity-name {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.identity-role {
|
||||
margin: 0;
|
||||
font-size: 0.74rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.feedback {
|
||||
padding: 0.95rem 1rem;
|
||||
font-weight: 600;
|
||||
@@ -784,96 +693,22 @@
|
||||
.metric-row,
|
||||
.top-grid,
|
||||
.material-grid,
|
||||
.impact-grid,
|
||||
.nav-group {
|
||||
.impact-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
gap: 0.12rem;
|
||||
padding-top: 0.15rem;
|
||||
}
|
||||
|
||||
.nav-group-label {
|
||||
margin: 0.15rem 0.55rem 0.3rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.6rem;
|
||||
border: none;
|
||||
border-radius: 0.7rem;
|
||||
background: transparent;
|
||||
color: #3a4a41;
|
||||
font-size: 0.93rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 140ms ease, color 140ms ease;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
color: #6d7d74;
|
||||
border-radius: 0.55rem;
|
||||
transition: color 140ms ease;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--panel-soft);
|
||||
color: #304038;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: var(--color-brand);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-item:hover .nav-icon {
|
||||
color: #304038;
|
||||
}
|
||||
|
||||
.nav-item.active .nav-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -0.85rem;
|
||||
top: 0.45rem;
|
||||
bottom: 0.45rem;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-brand);
|
||||
}
|
||||
|
||||
.workspace-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--panel);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
@@ -913,10 +748,7 @@
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
@@ -1194,7 +1026,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.workspace-layout,
|
||||
:global(.secondary-rail-layout),
|
||||
.metric-row,
|
||||
.top-grid,
|
||||
.material-grid,
|
||||
@@ -1202,19 +1034,6 @@
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.workspace-nav {
|
||||
position: static;
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.workspace-layout {
|
||||
min-height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
@@ -1239,21 +1058,5 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.nav-group-label {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import AppSecondaryRail from '$lib/components/navigation/AppSecondaryRail.svelte';
|
||||
import AppSecondaryRailLayout from '$lib/components/navigation/AppSecondaryRailLayout.svelte';
|
||||
import {
|
||||
BarChart3,
|
||||
TrendingUp,
|
||||
@@ -12,6 +14,8 @@
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
type ReportId =
|
||||
| 'sales-target-report'
|
||||
| 'finished-product-kanban'
|
||||
| 'summary'
|
||||
| 'raw-material-costs'
|
||||
| 'mix-cost-summary'
|
||||
@@ -28,6 +32,20 @@
|
||||
};
|
||||
|
||||
const reports: ReportItem[] = [
|
||||
{
|
||||
id: 'sales-target-report',
|
||||
label: 'Sales Target Report',
|
||||
description: 'Embedded Power BI sales target view for current sales tracking and review.',
|
||||
icon: FileText,
|
||||
group: 'Power BI',
|
||||
},
|
||||
{
|
||||
id: 'finished-product-kanban',
|
||||
label: 'Finished Product - Kanban',
|
||||
description: 'Embedded Power BI board for finished product review and kanban-style planning.',
|
||||
icon: FileText,
|
||||
group: 'Power BI',
|
||||
},
|
||||
{
|
||||
id: 'summary',
|
||||
label: 'Overview',
|
||||
@@ -72,61 +90,40 @@
|
||||
},
|
||||
];
|
||||
|
||||
const groups = [...new Set(reports.map((r) => r.group))];
|
||||
const SALES_TARGET_REPORT_URL =
|
||||
'https://app.powerbi.com/view?r=eyJrIjoiZjc1NjljNmEtMmJkMi00ZDlmLThjN2MtNjgyMzcxZDUwMzIyIiwidCI6IjEwNjk4Y2EyLTVmMmUtNGUwYS04ZTQ2LWE5ZGI0Nzk3ZjQ3MCJ9';
|
||||
|
||||
let activeId = $state<ReportId>('summary');
|
||||
const FINISHED_PRODUCT_KANBAN_URL =
|
||||
'https://app.powerbi.com/view?r=eyJrIjoiOTBjYTQ2MjMtZjMwNi00MjAzLTgxNDYtMmEzM2QwNjhlNmFlIiwidCI6IjEwNjk4Y2EyLTVmMmUtNGUwYS04ZTQ2LWE5ZGI0Nzk3ZjQ3MCJ9';
|
||||
|
||||
const orderedGroups = ['Power BI', 'Overview', 'Costing', 'Quality'];
|
||||
|
||||
const railGroups = orderedGroups
|
||||
.filter((group) => reports.some((report) => report.group === group))
|
||||
.map((group) => ({
|
||||
label: group,
|
||||
items: reports.filter((report) => report.group === group)
|
||||
}));
|
||||
|
||||
let activeId = $state<ReportId>('sales-target-report');
|
||||
|
||||
const activeReport = $derived(reports.find((r) => r.id === activeId) ?? reports[0]);
|
||||
</script>
|
||||
|
||||
<div class="reporting-layout">
|
||||
<nav class="report-nav" aria-label="Report navigation">
|
||||
<p class="nav-section-label">Reporting</p>
|
||||
|
||||
<div class="nav-identity">
|
||||
<div class="nav-avatar" aria-hidden="true">
|
||||
<TrendingUp size={16} strokeWidth={1.75} />
|
||||
</div>
|
||||
<div class="nav-identity-text">
|
||||
<p class="identity-name">Workspace reports</p>
|
||||
<p class="identity-role">Costing and quality views</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each groups as group}
|
||||
<div class="nav-group">
|
||||
<p class="nav-group-label">{group}</p>
|
||||
{#each reports.filter((r) => r.group === group) as report}
|
||||
{@const Icon = report.icon}
|
||||
<button
|
||||
type="button"
|
||||
class="nav-item"
|
||||
class:active={activeId === report.id}
|
||||
onclick={() => (activeId = report.id)}
|
||||
>
|
||||
<span class="nav-icon"><Icon size={18} strokeWidth={1.75} /></span>
|
||||
<span>{report.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
<AppSecondaryRailLayout>
|
||||
{#snippet rail()}
|
||||
<AppSecondaryRail
|
||||
sectionLabel="Reporting"
|
||||
identityTitle="Workspace reports"
|
||||
identitySubtitle="Costing and quality views"
|
||||
identityIcon={TrendingUp}
|
||||
groups={railGroups}
|
||||
activeId={activeId}
|
||||
onSelect={(id) => (activeId = id as ReportId)}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<div class="report-panel">
|
||||
{#if activeReport}
|
||||
{@const PanelIcon = activeReport.icon}
|
||||
<header class="panel-header">
|
||||
<div class="panel-header-icon" aria-hidden="true">
|
||||
<PanelIcon size={16} strokeWidth={1.75} />
|
||||
</div>
|
||||
<div>
|
||||
<p class="panel-eyebrow">{activeReport.group}</p>
|
||||
<h2>{activeReport.label}</h2>
|
||||
<p class="panel-description">{activeReport.description}</p>
|
||||
</div>
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<div class="panel-body">
|
||||
{#if activeId === 'summary'}
|
||||
<div class="report-placeholder">
|
||||
@@ -244,227 +241,127 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else if activeId === 'sales-target-report'}
|
||||
<section class="powerbi-embed">
|
||||
<header class="embed-header">
|
||||
<div>
|
||||
<h2>Sales Target Report</h2>
|
||||
<p>Live embedded Power BI view for sales target tracking and review.</p>
|
||||
</div>
|
||||
<a class="powerbi-link" href={SALES_TARGET_REPORT_URL} target="_blank" rel="noreferrer">
|
||||
Open in Power BI
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<div class="powerbi-frame-shell">
|
||||
<iframe
|
||||
title="Sales Target Report"
|
||||
src={SALES_TARGET_REPORT_URL}
|
||||
class="powerbi-frame"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{:else if activeId === 'finished-product-kanban'}
|
||||
<section class="powerbi-embed">
|
||||
<header class="embed-header">
|
||||
<div>
|
||||
<h2>Finished Product - Kanban</h2>
|
||||
<p>Live embedded Power BI view for finished product review and kanban-style planning.</p>
|
||||
</div>
|
||||
<a class="powerbi-link" href={FINISHED_PRODUCT_KANBAN_URL} target="_blank" rel="noreferrer">
|
||||
Open in Power BI
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<div class="powerbi-frame-shell">
|
||||
<iframe
|
||||
title="Finished Product - Kanban"
|
||||
src={FINISHED_PRODUCT_KANBAN_URL}
|
||||
class="powerbi-frame"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppSecondaryRailLayout>
|
||||
|
||||
<style>
|
||||
.reporting-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 15rem minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
min-height: calc(100vh - 8.5rem);
|
||||
max-height: calc(100vh - 8.5rem);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 1.15rem;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.report-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 8.5rem);
|
||||
padding: 1.1rem 0.85rem 0.85rem;
|
||||
background: var(--panel);
|
||||
border-right: 1px solid var(--line);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-section-label {
|
||||
margin: 0 0.55rem 0.3rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0 0.25rem 0.9rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.nav-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--green-deep);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-identity-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.identity-name {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.identity-role {
|
||||
margin: 0;
|
||||
font-size: 0.74rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
padding-top: 0.15rem;
|
||||
}
|
||||
|
||||
.nav-group + .nav-group {
|
||||
padding-top: 0.7rem;
|
||||
}
|
||||
|
||||
.nav-group-label {
|
||||
margin: 0.15rem 0.55rem 0.3rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.6rem;
|
||||
border: none;
|
||||
border-radius: 0.7rem;
|
||||
background: transparent;
|
||||
color: #3a4a41;
|
||||
font-size: 0.93rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 140ms ease, color 140ms ease;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
color: #6d7d74;
|
||||
border-radius: 0.55rem;
|
||||
transition: color 140ms ease;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--panel-soft);
|
||||
color: #304038;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: var(--color-brand);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-item:hover .nav-icon {
|
||||
color: #304038;
|
||||
}
|
||||
|
||||
.nav-item.active .nav-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -0.85rem;
|
||||
top: 0.45rem;
|
||||
bottom: 0.45rem;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-brand);
|
||||
}
|
||||
|
||||
.report-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--panel);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.panel-header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
border-radius: 0.72rem;
|
||||
background: var(--color-brand-tint);
|
||||
color: var(--color-brand);
|
||||
border: 1px solid color-mix(in srgb, var(--color-brand) 15%, transparent);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.panel-eyebrow {
|
||||
margin: 0;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
margin: 0.2rem 0 0.3rem;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.panel-description {
|
||||
margin: 0;
|
||||
font-size: 0.84rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem 1.35rem 1.35rem;
|
||||
}
|
||||
|
||||
.powerbi-embed {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
min-height: 42rem;
|
||||
}
|
||||
|
||||
.embed-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.25rem;
|
||||
padding: 0 0 0.35rem;
|
||||
background: color-mix(in srgb, var(--panel) 90%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.embed-header h2 {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 1.08rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.embed-header p {
|
||||
margin: 0.18rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.powerbi-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.56rem 0.2rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-brand) 22%, transparent);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #405148;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.powerbi-frame-shell {
|
||||
min-height: 36rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--line) 88%, transparent);
|
||||
overflow: hidden;
|
||||
background: #f6f8f6;
|
||||
}
|
||||
|
||||
.powerbi-frame {
|
||||
width: 100%;
|
||||
height: min(78vh, 980px);
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ── Report placeholders ───────────────────────────────────────── */
|
||||
@@ -598,43 +495,14 @@
|
||||
|
||||
/* ── Responsive ────────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.reporting-layout {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.report-nav {
|
||||
position: static;
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.nav-group-label {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.panel-header {
|
||||
.embed-header {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.powerbi-link {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-header-row,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api';
|
||||
import AppSecondaryRail from '$lib/components/navigation/AppSecondaryRail.svelte';
|
||||
import AppSecondaryRailLayout from '$lib/components/navigation/AppSecondaryRailLayout.svelte';
|
||||
import { clientSession } from '$lib/session';
|
||||
import { toast } from '$lib/toast';
|
||||
import { CircleUserRound, LockKeyhole } from 'lucide-svelte';
|
||||
@@ -81,37 +83,22 @@
|
||||
{ id: 'profile', label: 'Profile', icon: CircleUserRound },
|
||||
{ id: 'security', label: 'Security', icon: LockKeyhole },
|
||||
];
|
||||
|
||||
const railGroups = [{ items: navItems }];
|
||||
</script>
|
||||
|
||||
<div class="settings-layout">
|
||||
<nav class="settings-nav" aria-label="Settings sections">
|
||||
<p class="nav-section-label">Settings</p>
|
||||
|
||||
<div class="nav-identity">
|
||||
<div class="avatar" aria-hidden="true">{initials}</div>
|
||||
<div class="identity-text">
|
||||
<p class="identity-name">{$clientSession?.name ?? 'Unknown'}</p>
|
||||
<p class="identity-role">{$clientSession?.role_name ?? $clientSession?.role ?? 'User'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{#each navItems as item}
|
||||
{@const Icon = item.icon}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="nav-item"
|
||||
class:active={activeSection === item.id}
|
||||
onclick={() => (activeSection = item.id)}
|
||||
>
|
||||
<span class="nav-icon"><Icon size={18} strokeWidth={1.75} /></span>
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
<AppSecondaryRailLayout>
|
||||
{#snippet rail()}
|
||||
<AppSecondaryRail
|
||||
sectionLabel="Settings"
|
||||
identityAvatarText={initials}
|
||||
identityTitle={$clientSession?.name ?? 'Unknown'}
|
||||
identitySubtitle={$clientSession?.role_name ?? $clientSession?.role ?? 'User'}
|
||||
groups={railGroups}
|
||||
activeId={activeSection}
|
||||
onSelect={(id) => (activeSection = id as Section)}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<div class="settings-panel">
|
||||
{#if activeSection === 'profile'}
|
||||
@@ -180,175 +167,19 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</AppSecondaryRailLayout>
|
||||
|
||||
<style>
|
||||
.settings-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 15rem minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
min-height: calc(100vh - 8.5rem);
|
||||
max-height: calc(100vh - 8.5rem);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 1.15rem;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 8.5rem);
|
||||
padding: 1.1rem 0.85rem 0.85rem;
|
||||
background: var(--panel);
|
||||
border-right: 1px solid var(--line);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-section-label {
|
||||
margin: 0 0.55rem 0.3rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0 0.25rem 0.9rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--green-deep);
|
||||
color: #fff;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.identity-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.identity-name {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.identity-role {
|
||||
margin: 0;
|
||||
font-size: 0.74rem;
|
||||
color: var(--muted);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.settings-nav ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
.settings-nav li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.6rem;
|
||||
border: none;
|
||||
border-radius: 0.7rem;
|
||||
background: transparent;
|
||||
color: #3a4a41;
|
||||
font-size: 0.93rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 140ms ease, color 140ms ease;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
color: #6d7d74;
|
||||
border-radius: 0.55rem;
|
||||
transition: color 140ms ease;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--panel-soft);
|
||||
color: #304038;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: var(--color-brand);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-item:hover .nav-icon {
|
||||
color: #304038;
|
||||
}
|
||||
|
||||
.nav-item.active .nav-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -0.85rem;
|
||||
top: 0.45rem;
|
||||
bottom: 0.45rem;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-brand);
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--panel);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
@@ -372,10 +203,7 @@
|
||||
|
||||
.panel-form {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
gap: 1rem;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
padding: 1.5rem 1.75rem;
|
||||
max-width: 42rem;
|
||||
@@ -464,32 +292,6 @@
|
||||
/* ── Responsive ─────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.settings-layout {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.settings-nav {
|
||||
position: static;
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.settings-nav ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: auto;
|
||||
padding-right: 0.9rem;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user