Stripe-style UI overhaul + Expenses feed with month grouping (#15)

## Title
Stripe-style UI overhaul + Expenses feed with month grouping

## Summary
Rework the khata-ui frontend to a Stripe-grade design system and add a
rich, monthly-grouped Expenses experience driven by react-openapi's
shared, spec-configured field renderers.

## Highlights

### Design overhaul
- New design system: single indigo (#635BFF), 6px radius, Inter type
  scale, streamlined surfaces/inputs/feedback tokens
- Sticky app bar with brand-left header + inline footer, route titles
- Split-panel auth pages, Stripe-style Home hero + feature cards
- Toast provider + shared PageHeader/EmptyState UI primitives
- Admin polish: right-aligned numerics, row-hover actions, skeletons,
  richer empty states, breadcrumbed fetch-request flows

### Expenses page
- Monthly grouped feed with stat cards (total spent / this month / income)
- Month cards as single-open accordions with red-spent / green-income totals
- Flat transaction rows: logo, name/date, account, amount (no accordion)
- Floating month–year selector pill (scroll-spy) that expands + scrolls
  to the selected month
- Strict DD-MM-YYYY date parsing (day-first), no format guessing

### Shared react-openapi fields
- New `CurrencyField` + `formatCurrency` (cached Intl, sign-colored,
  sign-less amounts; form editing reuses NumberField)
- `resourceConfig.fieldTypes` override mechanism (amount → currency)
- Expenses page rendered via ListCellRenderer / DetailFieldRenderer /
  applyDisplayFormat — zero custom field renderers
- `resolveMediaUrl` resolves relative `/uploads/...` against the API
  base for every image field (Admin + Expenses logos)

### Fixes
- Perf: memoized rows, formatter caches, single-open accordions —
  cuts slow-click INP and DOM-nesting warnings
- Removed unused @mui/x-data-grid@7, which hoisted @mui/system@7 and
  crashed Box/createTheme at runtime (v5/v7 mix)

## Commits (10)
- 9808a1f design overhaul + expenses page
- d6856a5 fix expenses list performance + dom nesting
- 3dd833a shared currency field + expenses rendered from react-openapi fields
- e8c585b fix expenses grouping to parse occurred_at as DD-MM-YYYY strictly
- cc940ee expense list: split month totals, sticky headers + month pill
- 47d799f expense list: clickable month pill + fix scroll-to-month
- 537aef9 resolve relative media URLs against the API base in react-openapi
- 8e9a13d expense list: replace accordion with flat Stripe-style rows
- 62dd06c expense list: month accordions + month-year selector; drop unused x-data-grid
- 806bd42 dropped amount sign from CurrencyField.tsx

Reviewed-on: #15
Co-authored-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
Co-committed-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
This commit is contained in:
2026-08-18 13:51:28 +00:00
committed by aetos
parent 51762f8d18
commit 25ec534597
37 changed files with 1805 additions and 1067 deletions

View File

@@ -1,11 +1,11 @@
import * as React from "react";
import { Box, Typography, Button, Container, Grid, Paper, Chip } from "@mui/material";
import { useTheme, alpha } from "@mui/material/styles";
import { Box, Typography, Button, Container, Grid, Paper, alpha } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import DashboardIcon from "@mui/icons-material/Dashboard";
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
import SyncIcon from "@mui/icons-material/Sync";
import BarChartIcon from "@mui/icons-material/BarChart";
import SettingsIcon from "@mui/icons-material/Settings";
import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import { useAuth } from "../react-auth";
@@ -14,11 +14,9 @@ interface FeatureCardProps {
title: string;
description: string;
path: string;
label?: string;
accent: string;
}
function FeatureCard({ icon, title, description, path, label, accent }: FeatureCardProps) {
function FeatureCard({ icon, title, description, path }: FeatureCardProps) {
const navigate = useNavigate();
const theme = useTheme();
@@ -35,206 +33,154 @@ function FeatureCard({ icon, title, description, path, label, accent }: FeatureC
height: "100%",
display: "flex",
flexDirection: "column",
position: "relative",
overflow: "hidden",
transition: "all 0.25s ease",
"&::before": {
content: '""',
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 3,
background: accent,
opacity: 0,
transition: "opacity 0.25s ease",
},
transition: "border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease",
"&:hover": {
transform: "translateY(-4px)",
boxShadow: `0 12px 32px ${alpha(theme.palette.common.black, theme.palette.mode === "dark" ? 0.3 : 0.08)}`,
borderColor: "transparent",
"&::before": { opacity: 1 },
transform: "translateY(-2px)",
boxShadow: `0 8px 24px ${alpha(theme.palette.common.black, theme.palette.mode === "dark" ? 0.35 : 0.08)}`,
"& .feature-arrow": {
opacity: 1,
transform: "translateX(0)",
},
},
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5, mb: 1.5 }}>
<Box
sx={{
width: 40,
height: 40,
borderRadius: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: alpha(accent, 0.12),
color: accent,
}}
>
{icon}
</Box>
<Typography variant="subtitle1" fontWeight={700}>
{title}
</Typography>
<Box
sx={{
width: 40,
height: 40,
borderRadius: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "action.hover",
color: "primary.main",
mb: 2,
}}
>
{icon}
</Box>
<Typography variant="subtitle1" fontWeight={600} sx={{ mb: 0.75 }}>
{title}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ flex: 1, lineHeight: 1.6 }}>
{description}
</Typography>
{label && (
<Chip
label={label}
size="small"
variant="outlined"
sx={{ mt: 2, alignSelf: "flex-start", textTransform: "capitalize" }}
/>
)}
<Box
className="feature-arrow"
sx={{
mt: 2,
display: "flex",
alignItems: "center",
gap: 0.5,
color: "primary.main",
fontWeight: 600,
opacity: 0,
transform: "translateX(-4px)",
transition: "opacity 180ms ease, transform 180ms ease",
}}
>
<Typography variant="caption" fontWeight={600} color="primary">
Open
</Typography>
<ArrowForwardIcon sx={{ fontSize: 14 }} />
</Box>
</Paper>
);
}
export default function Home() {
const navigate = useNavigate();
const theme = useTheme();
const { currentUser } = useAuth();
const features = [
{
icon: <DashboardIcon />,
title: "Dashboard",
description: "Visualise inflows and outflows with interactive charts, drill into categories, and track trends over daily, weekly, and monthly periods.",
path: "/dashboard",
accent: theme.palette.mode === "dark" ? "#818cf8" : "#6366f1",
icon: <ReceiptLongIcon />,
title: "Expenses",
description:
"Browse every transaction imported from your accounts — grouped by month, with entity, account, tags, and amount at a glance.",
path: "/expenses",
},
{
icon: <SyncIcon />,
title: "Fetch Requests",
description: "Upload bank statements or configure email ingestion to auto-import transactions. Track pipeline status from pending through to completion.",
description:
"Upload bank statements or configure email ingestion to auto-import transactions. Track pipeline status from pending through to completion.",
path: "/fetch-requests",
accent: theme.palette.mode === "dark" ? "#34d399" : "#10b981",
},
{
icon: <BarChartIcon />,
title: "Report Snapshots",
description: "Generate cached report snapshots with custom filters — accounts, date ranges, amount bounds — then pin a snapshot on the dashboard for consistent comparisons.",
path: "/reports",
accent: theme.palette.mode === "dark" ? "#fbbf24" : "#f59e0b",
},
{
icon: <SettingsIcon />,
title: "Admin",
description: "Full CRUD over accounts, expenses, tags, and payors. Manage your data programmatically through the OpenAPI-driven admin panel.",
icon: <AccountBalanceWalletIcon />,
title: "Accounts & Entities",
description:
"Manage your accounts, expenses, tags, and payors through the OpenAPI-driven admin panel with full CRUD and server-side filters.",
path: "/admin",
accent: theme.palette.mode === "dark" ? "#e879f9" : "#d946ef",
},
{
icon: <AccountCircleIcon />,
title: "Profile",
description:
"View and edit your user profile — username, email, and account preferences — from the profile page.",
path: "/profile/me",
},
];
return (
<Box
sx={{
minHeight: "calc(100vh - 64px)",
display: "flex",
flexDirection: "column",
position: "relative",
overflow: "hidden",
"&::before": {
content: '""',
position: "absolute",
top: "-15%",
left: "-8%",
width: "45%",
height: "55%",
background: "radial-gradient(circle, rgba(99,102,241,0.12) 0%, transparent 70%)",
zIndex: 0,
},
"&::after": {
content: '""',
position: "absolute",
bottom: "-15%",
right: "-8%",
width: "45%",
height: "55%",
background: "radial-gradient(circle, rgba(236,72,153,0.1) 0%, transparent 70%)",
zIndex: 0,
},
}}
>
<Container maxWidth="lg" sx={{ position: "relative", zIndex: 1, flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", py: 6 }}>
<Box
sx={{
textAlign: "center",
mb: 6,
}}
>
<Box sx={{ width: "100%" }}>
<Container maxWidth="lg" sx={{ py: { xs: 6, md: 10 } }}>
<Box sx={{ maxWidth: 680, mx: { xs: "auto", md: 0 }, textAlign: { xs: "center", md: "left" } }}>
<Typography
variant="overline"
sx={{ color: "primary.main", mb: 1.5, display: "block", fontWeight: 700 }}
>
Khata · Financial Ledger
</Typography>
<Typography
variant="h1"
sx={{
fontWeight: 800,
fontSize: { xs: "2.5rem", sm: "3.5rem", md: "5rem" },
background: "linear-gradient(135deg, #6366f1 0%, #ec4899 50%, #f59e0b 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
color: "text.primary",
fontSize: { xs: "2.25rem", md: "3.25rem" },
letterSpacing: "-0.03em",
mb: 2,
}}
>
Welcome to Khata
Your intelligent, extensible financial ledger.
</Typography>
<Typography
variant="h6"
variant="body1"
color="text.secondary"
sx={{
maxWidth: 580,
mx: "auto",
lineHeight: 1.7,
fontWeight: 400,
fontSize: { xs: "1rem", md: "1.15rem" },
}}
sx={{ maxWidth: 540, lineHeight: 1.7, fontSize: "1.0625rem" }}
>
Your intelligent, extensible financial ledger. Import transactions, generate reports, and stay on top of your cashflow.
Import transactions from bank statements, enrich them with entity and tag intelligence,
and stay on top of your cashflow — all in one place.
</Typography>
<Box sx={{ mt: 4, display: "flex", gap: 2, justifyContent: "center", flexWrap: "wrap" }}>
<Box sx={{ mt: 4, display: "flex", gap: 2, justifyContent: { xs: "center", md: "flex-start" }, flexWrap: "wrap" }}>
<Button
variant="contained"
size="large"
endIcon={<ArrowForwardIcon />}
onClick={() => navigate("/dashboard")}
sx={{
px: 4,
py: 1.4,
borderRadius: "50px",
fontWeight: 700,
background: "linear-gradient(135deg, #6366f1 0%, #ec4899 100%)",
transition: "transform 0.2s ease, box-shadow 0.2s",
"&:hover": {
transform: "translateY(-2px)",
boxShadow: `0 8px 24px ${alpha(theme.palette.primary.main, 0.35)}`,
},
}}
onClick={() => navigate(currentUser ? "/fetch-requests" : "/login")}
sx={{ px: 3.5, py: 1.4, fontWeight: 600 }}
>
Enter Dashboard
{currentUser ? "Enter App" : "Get Started"}
</Button>
<Button
variant="outlined"
size="large"
onClick={() => navigate("/fetch-requests")}
sx={{
px: 4,
py: 1.4,
borderRadius: "50px",
fontWeight: 600,
borderWidth: 2,
"&:hover": { borderWidth: 2 },
}}
sx={{ px: 3.5, py: 1.4, fontWeight: 600 }}
>
Import Data
</Button>
</Box>
</Box>
<Grid container spacing={3}>
<Grid container spacing={3} sx={{ mt: { xs: 4, md: 8 } }}>
{features.map((f) => (
<Grid key={f.title} item xs={12} sm={6} md={3}>
<FeatureCard {...f} />