dashboard design changes

This commit is contained in:
2026-08-05 18:34:37 +05:30
parent 2c8c394795
commit 9d7bbad32c
92 changed files with 5104 additions and 608 deletions

View File

@@ -0,0 +1,13 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {ApiWebhooksManager} from '@/features/settings/ApiWebhooksManager';
export default function ApiSettingsPage() {
return (
<SettingsPage
title="API & Webhooks"
description="Developer credentials, secret signing tokens, webhook subscriptions and dispatch audit logs."
>
<ApiWebhooksManager />
</SettingsPage>
);
}

View File

@@ -1,60 +1,13 @@
import {VStack} from '@astryxdesign/core/Layout';
import {Badge} from '@astryxdesign/core/Badge';
import {Text} from '@astryxdesign/core/Text';
import {Divider} from '@astryxdesign/core/Divider';
import {PageHeader} from '@/components/primitives/PageHeader';
import {StaticPanel} from '@/components/patterns/PanelCard';
import {StatPair, StatRow} from '@/components/patterns/StatPair';
import {EmptyPanel} from '@/components/patterns/EmptyPanel';
import {readProfile} from '@/lib/mock/settings';
import {formatInr} from '@/lib/format';
import {SettingsPage} from '@/features/settings/SettingsPage';
import {BillingOverview} from '@/features/settings/BillingOverview';
/**
* Billing is read-only for now.
*
* The plan and settlement figures are real shapes, but there is no payment
* provider wired up — so rather than render fake "Change plan" flows that do
* nothing, the actions are absent and the invoice list says plainly that it
* is not connected yet. A disabled button that never becomes enabled is worse
* than no button.
*/
export default function SettingsBillingPage() {
const profile = readProfile();
return (
<VStack gap={5}>
<PageHeader
title="Billing"
description="Plan, LYT settlement and invoices."
/>
<StaticPanel
title="Plan"
subtitle={profile.businessName}
actions={<Badge variant="success" label="Active" />}
>
<VStack gap={4}>
<StatRow>
<StatPair label="Plan" value="Growth — 5 stores" />
<StatPair label="Billed" value="Monthly" />
<StatPair label="Next charge" value="1 Sep 2026" />
<StatPair label="Amount" value={formatInr(14999)} align="end" />
</StatRow>
<Divider />
<Text size="sm" color="secondary">
LYTs are settled monthly against redemptions, at 1 LYT = ₹1. Your
outstanding liability is shown on the Lyts page.
</Text>
</VStack>
</StaticPanel>
<StaticPanel title="Invoices" subtitle="Past statements">
<EmptyPanel
icon="revenue"
title="No billing provider connected"
description="Invoices appear here once payments are wired up."
/>
</StaticPanel>
</VStack>
<SettingsPage
title="Billing & LYT Settlement"
description="Subscription plans, quota consumption, settlement bank accounts and invoice history."
>
<BillingOverview />
</SettingsPage>
);
}

View File

@@ -0,0 +1,13 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {IntegrationsGrid} from '@/features/settings/IntegrationsGrid';
export default function IntegrationsSettingsPage() {
return (
<SettingsPage
title="Integrations & Connectors"
description="E-commerce POS sync, payment gateways, WhatsApp marketing and ad channels."
>
<IntegrationsGrid />
</SettingsPage>
);
}

View File

@@ -8,7 +8,7 @@ import {
VStack,
} from '@astryxdesign/core/Layout';
import {SideNav, SideNavItem, SideNavSection} from '@astryxdesign/core/SideNav';
import {TabList, Tab} from '@astryxdesign/core/TabList';
import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
import {Icon} from '@astryxdesign/core/Icon';
import {useRouter} from 'next/navigation';
import {useBreakpoint} from '@/lib/breakpoints';
@@ -26,8 +26,8 @@ import {SETTINGS_NAV, isSettingsActive} from '@/features/settings/settings-nav';
* and behaves exactly like a primary nav item — same selected treatment, same
* hover, same keyboard handling, no second definition to keep in sync.
*
* Below the laptop breakpoint a 220px panel would eat most of the width, so
* the sub-nav becomes a horizontal TabList above the content instead.
* Below the laptop breakpoint a 240px panel would eat most of the width, so
* the sub-nav collapses to a section picker above the content instead.
*/
export default function SettingsLayout({
children,
@@ -41,27 +41,36 @@ export default function SettingsLayout({
if (isNarrow) {
const active =
SETTINGS_NAV.find((s) => isSettingsActive(pathname, s.href))?.href ??
SETTINGS_NAV[0].href;
SETTINGS_NAV.find((s) => isSettingsActive(pathname, s.href)) ??
SETTINGS_NAV[0];
// A dropdown, not a TabList.
//
// This was a TabList when Settings had three sections. At eleven it stops
// working: `layout="fill"` cannot fit eleven labels in a tablet-width
// column, so the strip wrapped into a full-height vertical list and
// squeezed the page content out entirely. A picker is what every dense
// settings UI uses at this width, and it stays one tap regardless of how
// many sections the module grows to.
return (
<VStack gap={5}>
<TabList
value={active}
onChange={(href) => router.push(href)}
layout="fill"
size="sm"
hasDivider
>
{SETTINGS_NAV.map((s) => (
<Tab
key={s.href}
value={s.href}
label={s.label}
icon={<Icon icon={s.icon} size="sm" />}
/>
))}
</TabList>
<VStack width="100%">
{/* Only the picker is padded here — SettingsPage still owns every
page gutter, so the container contract stays in one place. */}
<VStack paddingInline={8} width="100%" className="pt-8">
<DropdownMenu
button={{
variant: 'secondary',
label: active.label,
icon: <Icon icon={active.icon} size="sm" />,
}}
menuWidth={260}
items={SETTINGS_NAV.map((s) => ({
label: s.label,
icon: s.icon,
onClick: () => router.push(s.href),
}))}
/>
</VStack>
{children}
</VStack>
);
@@ -71,22 +80,41 @@ export default function SettingsLayout({
<Layout
height="fill"
start={
<LayoutPanel width={220} padding={0} role="navigation" label="Settings">
<SideNav>
<SideNavSection title="Settings" isHeaderHidden>
{SETTINGS_NAV.map((s) => (
<SideNavItem
key={s.href}
label={s.label}
icon={s.icon}
href={s.href}
isSelected={isSettingsActive(pathname, s.href)}
/>
))}
</SideNavSection>
</SideNav>
// 240px rather than 220: "Roles & Permissions" and "API & Webhooks"
// were wrapping once the panel gained its own inset padding.
<LayoutPanel width={240} padding={0} role="navigation" label="Settings">
{/*
The panel's own inset. Without it the nav items sat flush against
the workspace rail on one side and the content gutter on the other,
and the first item started hard against the top of the viewport.
paddingBlock 6 (24px) is what gives the list somewhere to begin.
*/}
<VStack paddingInline={3} paddingBlock={6} width="100%">
{/*
w-full because SideNav's own width is a fixed 260px — inside a
240px panel that already spends 24px on padding it overhung by
32px and gave the sub-nav a horizontal scrollbar along its
bottom edge (measured: scrollWidth 272 in a 240px panel).
*/}
<SideNav className="w-full">
<SideNavSection title="Settings" isHeaderHidden>
{SETTINGS_NAV.map((s) => (
<SideNavItem
key={s.href}
label={s.label}
icon={s.icon}
href={s.href}
isSelected={isSettingsActive(pathname, s.href)}
/>
))}
</SideNavSection>
</SideNav>
</VStack>
</LayoutPanel>
}
// padding stays 0 here on purpose — SettingsPage owns the page gutters,
// so every screen gets the identical container whether it renders in
// this branch or the narrow picker one above.
content={<LayoutContent padding={0}>{children}</LayoutContent>}
/>
);

View File

@@ -1,19 +1,13 @@
import {VStack} from '@astryxdesign/core/Layout';
import {PageHeader} from '@/components/primitives/PageHeader';
import {SettingsPage} from '@/features/settings/SettingsPage';
import {NotificationsForm} from '@/features/settings/NotificationsForm';
import {readProfile} from '@/lib/mock/settings';
/** Server Component — same direct-read hybrid as the profile page. */
export default function SettingsNotificationsPage() {
const {notifications} = readProfile();
export default function NotificationsSettingsPage() {
return (
<VStack gap={5}>
<PageHeader
title="Notifications"
description="Choose which events are worth interrupting you for."
/>
<NotificationsForm initialData={notifications} />
</VStack>
<SettingsPage
title="Notifications"
description="Delivery channels, instant alerts, weekly digest dispatches and trigger criteria."
>
<NotificationsForm />
</SettingsPage>
);
}

View File

@@ -1,30 +1,13 @@
import {VStack} from '@astryxdesign/core/Layout';
import {PageHeader} from '@/components/primitives/PageHeader';
import {ProfileForm} from '@/features/settings/ProfileForm';
import {readProfile} from '@/lib/mock/settings';
/**
* A SERVER Component — the one place in the app that fetches on the server.
*
* Every other page fetches on the client because its scope (store, range)
* changes from controls that live above it in the tree, and a route
* navigation would remount the Copilot. Settings has none of that: there is
* exactly one server-known record at page load, so reading it directly here
* (no HTTP hop, no loading skeleton, no empty-input flash) is strictly better.
*
* The same readProfile() backs /api/settings/profile, so the two transports
* cannot drift.
*/
export default function SettingsProfilePage() {
const profile = readProfile();
import {SettingsPage} from '@/features/settings/SettingsPage';
import {BusinessForm} from '@/features/settings/BusinessForm';
export default function BusinessSettingsPage() {
return (
<VStack gap={5}>
<PageHeader
title="Settings"
description="Business details, contact information and LYT earn rate."
/>
<ProfileForm initialData={profile} />
</VStack>
<SettingsPage
title="Business Settings"
description="Company profile, GSTIN registration, registered address and LYT earn defaults."
>
<BusinessForm />
</SettingsPage>
);
}

View File

@@ -0,0 +1,13 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {PreferencesForm} from '@/features/settings/PreferencesForm';
export default function PreferencesSettingsPage() {
return (
<SettingsPage
title="Workspace Preferences"
description="Theme customization, reporting currency, localized language and default landing views."
>
<PreferencesForm />
</SettingsPage>
);
}

View File

@@ -0,0 +1,16 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {ProfileForm} from '@/features/settings/ProfileForm';
import {readProfile} from '@/lib/mock/settings';
export default function MerchantProfilePage() {
const profile = readProfile();
return (
<SettingsPage
title="Personal Profile"
description="Your user credentials, contact details, account email and timezone preference."
>
<ProfileForm initialData={profile} />
</SettingsPage>
);
}

View File

@@ -0,0 +1,13 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {RoleMatrix} from '@/features/settings/RoleMatrix';
export default function RolesSettingsPage() {
return (
<SettingsPage
title="Roles & Permissions"
description="Enterprise role definition, module permission matrix and access control boundaries."
>
<RoleMatrix />
</SettingsPage>
);
}

View File

@@ -0,0 +1,13 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {SecurityManager} from '@/features/settings/SecurityManager';
export default function SecuritySettingsPage() {
return (
<SettingsPage
title="Security & Audit Logs"
description="Two-Factor authentication, password management, active login sessions and security audit history."
>
<SecurityManager />
</SettingsPage>
);
}

View File

@@ -0,0 +1,13 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {StoreManagement} from '@/features/settings/StoreManagement';
export default function StoreSettingsPage() {
return (
<SettingsPage
title="Store Locations"
description="Branch operations, outlet directory, operating schedules and manager assignments."
>
<StoreManagement />
</SettingsPage>
);
}

View File

@@ -0,0 +1,13 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {TeamManagement} from '@/features/settings/TeamManagement';
export default function TeamSettingsPage() {
return (
<SettingsPage
title="Team & Staff"
description="Manage employee access, invite new team members, assign store locations and reset credentials."
>
<TeamManagement />
</SettingsPage>
);
}