dashboard design changes
This commit is contained in:
13
src/app/(workspace)/settings/api/page.tsx
Normal file
13
src/app/(workspace)/settings/api/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
13
src/app/(workspace)/settings/integrations/page.tsx
Normal file
13
src/app/(workspace)/settings/integrations/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
13
src/app/(workspace)/settings/preferences/page.tsx
Normal file
13
src/app/(workspace)/settings/preferences/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
src/app/(workspace)/settings/profile/page.tsx
Normal file
16
src/app/(workspace)/settings/profile/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
13
src/app/(workspace)/settings/roles/page.tsx
Normal file
13
src/app/(workspace)/settings/roles/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
13
src/app/(workspace)/settings/security/page.tsx
Normal file
13
src/app/(workspace)/settings/security/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
13
src/app/(workspace)/settings/stores/page.tsx
Normal file
13
src/app/(workspace)/settings/stores/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
13
src/app/(workspace)/settings/team/page.tsx
Normal file
13
src/app/(workspace)/settings/team/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user