diff --git a/src/Expense/Expense.tsx b/src/Expense.tsx similarity index 64% rename from src/Expense/Expense.tsx rename to src/Expense.tsx index 076f4e3..8953dc7 100644 --- a/src/Expense/Expense.tsx +++ b/src/Expense.tsx @@ -10,32 +10,15 @@ import { } from "@mui/material"; import ReceiptLongIcon from "@mui/icons-material/ReceiptLong"; import { useNavigate } from "react-router-dom"; -import { useResource, useAppContext, formatCurrency } from "../../react-openapi"; -import { PageHeader } from "../ui/PageHeader"; -import { EmptyState } from "../ui/EmptyState"; -import { ExpenseList } from "./ExpenseList"; -import { ExpenseItem, ExpenseFieldConfigs, isExpense, currentMonthKey, monthKey, monthLabel, parseOccurredAt } from "./types"; - -function StatCard({ label, value, hint }: { label: string; value: string; hint?: string }) { - return ( - - - {label} - - - {value} - - {hint && ( - - {hint} - - )} - - ); -} +import { useResource, useAppContext, formatCurrency } from "../react-openapi"; +import { PageHeader } from "./ui/PageHeader"; +import { EmptyState } from "./ui/EmptyState"; +import { StatCard } from "./common/components/StatCard"; +import { TransactionList } from "./common/components/TransactionList"; +import { buildTxnFieldConfigs } from "./common/utils/fieldConfigs"; +import { isExpense } from "./common/utils/transactions"; +import { currentMonthKey, monthKey, monthLabel, parseOccurredAt } from "./common/utils/dates"; +import type { ExpenseItem } from "./common/types"; export default function Expense() { const navigate = useNavigate(); @@ -43,33 +26,7 @@ export default function Expense() { const { resources } = useAppContext(); const [items, setItems] = useState(null); - const fieldConfigs = useMemo(() => { - if (!resource) return null; - const find = (name: string) => resource.fields.find((f) => f.name === name); - const entity = find("entity"); - const amount = find("amount"); - const account = find("account"); - const tags = find("tags"); - const occurredAt = find("occurred_at"); - const entitiesRes = resources.find((r) => r.name === "entities"); - const accountsRes = resources.find((r) => r.name === "accounts"); - const tagsRes = resources.find((r) => r.name === "tags"); - const logo = entitiesRes?.fields.find((f) => f.name === "logo"); - if (!entity || !amount || !account || !tags || !occurredAt || !logo) return null; - return { - entity, - amount, - account, - tags, - occurredAt, - logo, - formats: { - entity: entitiesRes?.displayFormat ?? "{name}", - account: accountsRes?.displayFormat ?? "{name}", - tags: tagsRes?.displayFormat ?? "{name}", - }, - }; - }, [resource, resources]); + const fieldConfigs = useMemo(() => buildTxnFieldConfigs(resources), [resources]); useEffect(() => { let mounted = true; @@ -80,7 +37,7 @@ export default function Expense() { return () => { mounted = false; }; - }, []); + }, [list]); const sorted = useMemo( () => @@ -161,7 +118,7 @@ export default function Expense() { - {fieldConfigs && } + {fieldConfigs && } )} diff --git a/src/Reports/GenerateReportPanel.tsx b/src/Reports/GenerateReportPanel.tsx index 9845649..c634d05 100644 --- a/src/Reports/GenerateReportPanel.tsx +++ b/src/Reports/GenerateReportPanel.tsx @@ -17,7 +17,8 @@ import AddIcon from "@mui/icons-material/Add"; import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline"; import { useResource, useAppContext, applyDisplayFormat } from "../../react-openapi"; import { useToast } from "../ui/Toast"; -import { apiErrorMessage, groupTypeEnum, isDdmmyyyy, periodHints } from "./types"; +import { isDdmmyyyy } from "../common/utils/dates"; +import { apiErrorMessage, groupTypeEnum, periodHints } from "./types"; interface GroupRow { id: number; diff --git a/src/Reports/Reports.tsx b/src/Reports/Report.tsx similarity index 86% rename from src/Reports/Reports.tsx rename to src/Reports/Report.tsx index 61b3e0e..4b044f9 100644 --- a/src/Reports/Reports.tsx +++ b/src/Reports/Report.tsx @@ -5,30 +5,14 @@ import { useResource, useAppContext, formatCurrency } from "../../react-openapi" import { useToast } from "../ui/Toast"; import { PageHeader } from "../ui/PageHeader"; import { EmptyState } from "../ui/EmptyState"; +import { StatCard } from "../common/components/StatCard"; +import { buildTxnFieldConfigs } from "../common/utils/fieldConfigs"; import { GenerateReportPanel } from "./GenerateReportPanel"; import { ReportList } from "./ReportList"; import { ReportViewer } from "./ReportViewer"; -import { apiErrorMessage, buildReportFieldConfigs, buildTxnFieldConfigs } from "./types"; +import { apiErrorMessage, buildReportFieldConfigs } from "./types"; -function StatCard({ label, value, hint }: { label: string; value: string; hint?: string }) { - return ( - - - {label} - - - {value} - - {hint && ( - - {hint} - - )} - - ); -} - -export default function Reports() { +export default function Report() { const { resources } = useAppContext(); const { showToast } = useToast(); const { list, loading, error } = useResource("reports"); diff --git a/src/Reports/ReportViewer.tsx b/src/Reports/ReportViewer.tsx index db7c5a0..2ac5066 100644 --- a/src/Reports/ReportViewer.tsx +++ b/src/Reports/ReportViewer.tsx @@ -9,9 +9,6 @@ import { Skeleton, TextField, Autocomplete, - Accordion, - AccordionSummary, - AccordionDetails, Table, TableBody, TableCell, @@ -19,14 +16,12 @@ import { TableHead, TableRow, } from "@mui/material"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import CloseIcon from "@mui/icons-material/Close"; import CachedIcon from "@mui/icons-material/Cached"; -import { useAppContext, useResource, ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi"; -import { groupByMonth } from "../Expense/ExpenseList"; -import type { ExpenseItem, ExpenseFieldConfigs } from "../Expense/types"; -import { monthLabel } from "../Expense/types"; -import type { TxnFieldConfigs } from "./types"; +import { useAppContext, useResource, formatCurrency } from "../../react-openapi"; +import { StatCard } from "../common/components/StatCard"; +import { TransactionList } from "../common/components/TransactionList"; +import type { TxnFieldConfigs } from "../common/types"; import { aggregateSlice, buildPivot, metricLabels } from "./types"; interface ReportViewerProps { @@ -37,83 +32,6 @@ interface ReportViewerProps { onRegenerated: (report: any) => void; } -function StatCard({ label, value, color }: { label: string; value: string; color?: string }) { - return ( - - - {label} - - - {value} - - - ); -} - -const ReportTxnRow = React.memo(function ReportTxnRow({ - item, - currency, - fields, -}: { - item: ExpenseItem; - currency: string; - fields: ExpenseFieldConfigs; -}) { - const itemCurrency = item.account?.currency ?? currency; - - return ( - - - {item.entity?.logo ? ( - - ) : ( - - )} - - - - {item.entity ? applyDisplayFormat(item.entity, fields.formats.entity) : "Unknown"} - - - - - - {item.account?.name && ( - - )} - - - - - - ); -}); - export function ReportViewer({ id, version, fields, onClose, onRegenerated }: ReportViewerProps) { const { schemas } = useAppContext(); const { get } = useResource("reports"); @@ -124,7 +42,6 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re const [reload, setReload] = useState(0); const [selectedPeriod, setSelectedPeriod] = useState("*"); const [selectedPayee, setSelectedPayee] = useState("*"); - const [openMonth, setOpenMonth] = useState(null); useEffect(() => { let mounted = true; @@ -165,8 +82,6 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re [data, selectedPeriod, selectedPayee], ); - const months = useMemo(() => groupByMonth(slice.txns), [slice.txns]); - const pivot = useMemo( () => buildPivot(data ?? [], groupOptions.period, groupOptions.payee), [data, groupOptions], @@ -297,11 +212,7 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re const value = metricValue(m.key); if (value == null) return null; return ( - + {m.label} @@ -363,74 +274,15 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re )} - {months.length === 0 ? ( + {slice.txns.length === 0 ? ( No transactions in this slice. - ) : ( - - {months.map((group) => ( - setOpenMonth(isExpanded ? group.key : null)} - TransitionProps={{ unmountOnExit: true }} - sx={{ - border: "1px solid", - borderColor: openMonth === group.key ? "primary.main" : "divider", - borderRadius: 2, - overflow: "hidden", - boxShadow: "none", - backgroundColor: "background.paper", - "&:before": { display: "none" }, - "&:hover": { borderColor: "primary.light" }, - }} - > - } - sx={{ - px: 2, - py: 1, - "& .MuiAccordionSummary-content": { alignItems: "center", gap: 1.5, minWidth: 0 }, - }} - > - - {monthLabel(group.key)} - - - {group.items.length} transaction{group.items.length === 1 ? "" : "s"} - - - - {formatCurrency(group.spent, group.currency)} - - - / - - - {formatCurrency(group.income, group.currency)} - - - - - {fields && - group.items.map((item) => ( - - ))} - - - - ))} - - )} + ) : fields ? ( + + ) : null} )} diff --git a/src/Reports/types.ts b/src/Reports/types.ts index 1c5760c..ebbee8b 100644 --- a/src/Reports/types.ts +++ b/src/Reports/types.ts @@ -1,6 +1,5 @@ import type { FieldConfig, ResourceConfig } from "../../react-openapi"; - -const DDMMYYYY = /^(\d{2})-(\d{2})-(\d{4})$/; +import { parseDdmmyyyy } from "../common/utils/dates"; export interface ParsedGroupKey { period?: { granularity: string; label: string }; @@ -53,15 +52,6 @@ export interface PivotTable { totals: PivotCell[]; } -export interface TxnFieldConfigs { - entity: FieldConfig; - amount: FieldConfig; - account: FieldConfig; - occurredAt: FieldConfig; - logo: FieldConfig; - formats: { entity: string; account: string }; -} - export interface ReportFieldConfigs { groupLabel: FieldConfig; granularity: FieldConfig; @@ -94,36 +84,6 @@ export function parseCacheKey(key: string): ParsedGroupKey { return out; } -export function isDdmmyyyy(value?: string): boolean { - if (!value) return true; - const m = value.match(DDMMYYYY); - if (!m) return false; - const [, dd, mm, yyyy] = m; - const d = new Date(Number(yyyy), Number(mm) - 1, Number(dd)); - return !( - Number.isNaN(d.getTime()) || - d.getDate() !== Number(dd) || - d.getMonth() !== Number(mm) - 1 || - d.getFullYear() !== Number(yyyy) - ); -} - -export function parseDdmmyyyy(value: string): Date { - const m = value.match(DDMMYYYY); - if (!m) throw new Error(`Date is not DD-MM-YYYY: ${value}`); - const [, dd, mm, yyyy] = m; - const d = new Date(Number(yyyy), Number(mm) - 1, Number(dd)); - if ( - Number.isNaN(d.getTime()) || - d.getDate() !== Number(dd) || - d.getMonth() !== Number(mm) - 1 || - d.getFullYear() !== Number(yyyy) - ) { - throw new Error(`Invalid date: ${value}`); - } - return d; -} - export function apiErrorMessage(e: any): string { if (e?.response?.data) { const d = e.response.data; @@ -263,30 +223,6 @@ export function buildPivot(groups: ReportGroupLike[], periodOrder: string[], pay return { rows, payees, totals }; } -export function buildTxnFieldConfigs(resources: ResourceConfig[]): TxnFieldConfigs | null { - const expensesRes = resources.find((r) => r.name === "expenses"); - const entitiesRes = resources.find((r) => r.name === "entities"); - const accountsRes = resources.find((r) => r.name === "accounts"); - const find = (res: ResourceConfig | undefined, name: string) => res?.fields.find((f) => f.name === name); - const entity = find(expensesRes, "entity"); - const amount = find(expensesRes, "amount"); - const account = find(expensesRes, "account"); - const occurredAt = find(expensesRes, "occurred_at"); - const logo = find(entitiesRes, "logo"); - if (!entity || !amount || !account || !occurredAt || !logo) return null; - return { - entity, - amount, - account, - occurredAt, - logo, - formats: { - entity: entitiesRes?.displayFormat ?? "{name}", - account: accountsRes?.displayFormat ?? "{name}", - }, - }; -} - export function buildReportFieldConfigs(resources: ResourceConfig[]): ReportFieldConfigs | null { const reportsRes = resources.find((r) => r.name === "reports"); if (!reportsRes) return null; diff --git a/src/common/components/StatCard.tsx b/src/common/components/StatCard.tsx new file mode 100644 index 0000000..3e4cd77 --- /dev/null +++ b/src/common/components/StatCard.tsx @@ -0,0 +1,26 @@ +import { Paper, Typography } from "@mui/material"; + +interface StatCardProps { + label: string; + value: string; + color?: string; + hint?: string; +} + +export function StatCard({ label, value, color, hint }: StatCardProps) { + return ( + + + {label} + + + {value} + + {hint && ( + + {hint} + + )} + + ); +} \ No newline at end of file diff --git a/src/Expense/ExpenseList.tsx b/src/common/components/TransactionList.tsx similarity index 69% rename from src/Expense/ExpenseList.tsx rename to src/common/components/TransactionList.tsx index 5b10561..d62bf0d 100644 --- a/src/Expense/ExpenseList.tsx +++ b/src/common/components/TransactionList.tsx @@ -12,116 +12,18 @@ import { import { alpha } from "@mui/material/styles"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi"; -import type { ExpenseItem, ExpenseFieldConfigs } from "./types"; -import { isExpense, monthKey, monthLabel, parseOccurredAt } from "./types"; +import { formatCurrency } from "../../../react-openapi"; +import type { ExpenseItem, TxnFieldConfigs } from "../types"; +import { groupByMonth } from "../utils/transactions"; +import { monthLabel } from "../utils/dates"; +import { TransactionRow } from "./TransactionRow"; -interface GroupedMonth { - key: string; +interface TransactionListProps { items: ExpenseItem[]; - spent: number; - income: number; - currency: string; + fields: TxnFieldConfigs; } -function groupByMonth(items: ExpenseItem[]): GroupedMonth[] { - const map = new Map(); - for (const item of items) { - const key = monthKey(item.occurred_at); - const list = map.get(key) ?? []; - list.push(item); - map.set(key, list); - } - return [...map.entries()] - .map(([key, list]) => { - const sorted = [...list].sort( - (a, b) => parseOccurredAt(b.occurred_at).getTime() - parseOccurredAt(a.occurred_at).getTime(), - ); - const currency = sorted.find((it) => it.account?.currency)?.account?.currency ?? "INR"; - const spent = sorted.filter(isExpense).reduce((sum, it) => sum + Math.abs(it.amount ?? 0), 0); - const income = sorted.filter((it) => !isExpense(it)).reduce((sum, it) => sum + (it.amount ?? 0), 0); - return { key, items: sorted, spent, income, currency }; - }) - .sort((a, b) => b.key.localeCompare(a.key)); -} - -interface ExpenseRowProps { - item: ExpenseItem; - currency: string; - fields: ExpenseFieldConfigs; -} - -const ExpenseRow = React.memo(function ExpenseRow({ item, currency, fields }: ExpenseRowProps) { - const itemCurrency = item.account?.currency ?? currency; - - return ( - - - {item.entity?.logo ? ( - - ) : ( - - )} - - - - {item.entity ? applyDisplayFormat(item.entity, fields.formats.entity) : "Unknown"} - - - - - - {item.account?.name && ( - - )} - - - - - - ); -}); - -interface ExpenseListProps { - items: ExpenseItem[]; - fields: ExpenseFieldConfigs; -} - -export function ExpenseList({ items, fields }: ExpenseListProps) { +export function TransactionList({ items, fields }: TransactionListProps) { const [activeMonth, setActiveMonth] = useState(null); const [openMonth, setOpenMonth] = useState(null); const [menuAnchor, setMenuAnchor] = useState(null); @@ -259,7 +161,7 @@ export function ExpenseList({ items, fields }: ExpenseListProps) { {group.items.map((item) => ( - ); -} - -export { groupByMonth }; \ No newline at end of file +} \ No newline at end of file diff --git a/src/common/components/TransactionRow.tsx b/src/common/components/TransactionRow.tsx new file mode 100644 index 0000000..cddb2c3 --- /dev/null +++ b/src/common/components/TransactionRow.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { Box, Typography } from "@mui/material"; +import { ListCellRenderer, CurrencyField, applyDisplayFormat } from "../../../react-openapi"; +import type { ExpenseItem, TxnFieldConfigs } from "../types"; + +interface TransactionRowProps { + item: ExpenseItem; + currency: string; + fields: TxnFieldConfigs; +} + +export const TransactionRow = React.memo(function TransactionRow({ item, currency, fields }: TransactionRowProps) { + const itemCurrency = item.account?.currency ?? currency; + + return ( + + + {item.entity?.logo ? ( + + ) : ( + + )} + + + + {item.entity ? applyDisplayFormat(item.entity, fields.formats.entity) : "Unknown"} + + + + + + {item.account?.name && ( + + )} + + + + + + ); +}); \ No newline at end of file diff --git a/src/common/types.ts b/src/common/types.ts new file mode 100644 index 0000000..a0e9b81 --- /dev/null +++ b/src/common/types.ts @@ -0,0 +1,29 @@ +import type { FieldConfig } from "../../react-openapi"; + +export interface ExpenseItem { + id: string; + entity?: { name?: string; type?: string; logo?: string } | null; + amount: number; + account?: { name?: string; number?: string; type?: string; currency?: string } | null; + tags?: { icon?: string; name?: string }[]; + occurred_at?: string; + created_at?: string; + updated_at?: string; +} + +export interface GroupedMonth { + key: string; + items: ExpenseItem[]; + spent: number; + income: number; + currency: string; +} + +export interface TxnFieldConfigs { + entity: FieldConfig; + amount: FieldConfig; + account: FieldConfig; + occurredAt: FieldConfig; + logo: FieldConfig; + formats: { entity: string; account: string }; +} \ No newline at end of file diff --git a/src/Expense/types.ts b/src/common/utils/dates.ts similarity index 52% rename from src/Expense/types.ts rename to src/common/utils/dates.ts index efe769e..d6b5e83 100644 --- a/src/Expense/types.ts +++ b/src/common/utils/dates.ts @@ -1,36 +1,7 @@ -import type { FieldConfig } from "../../react-openapi"; - -export interface ExpenseItem { - id: string; - entity?: { name?: string; type?: string; logo?: string } | null; - amount: number; - account?: { name?: string; number?: string; type?: string; currency?: string } | null; - tags?: { icon?: string; name?: string }[]; - occurred_at?: string; - created_at?: string; - updated_at?: string; -} - -export interface ExpenseFieldConfigs { - entity: FieldConfig; - amount: FieldConfig; - account: FieldConfig; - tags: FieldConfig; - occurredAt: FieldConfig; - logo: FieldConfig; - formats: { - entity: string; - account: string; - tags: string; - }; -} - -export function isExpense(item: ExpenseItem): boolean { - return (item.amount ?? 0) < 0; -} +const DDMMYYYY = /^(\d{2})-(\d{2})-(\d{4})$/; export function parseOccurredAt(value?: string): Date { - const m = value?.match(/^(\d{2})-(\d{2})-(\d{4})$/); + const m = value?.match(DDMMYYYY); if (!m) { throw new Error(`Expense occurred_at is not DD-MM-YYYY: ${value}`); } @@ -47,6 +18,36 @@ export function parseOccurredAt(value?: string): Date { return d; } +export function isDdmmyyyy(value?: string): boolean { + if (!value) return true; + const m = value.match(DDMMYYYY); + if (!m) return false; + const [, dd, mm, yyyy] = m; + const d = new Date(Number(yyyy), Number(mm) - 1, Number(dd)); + return !( + Number.isNaN(d.getTime()) || + d.getDate() !== Number(dd) || + d.getMonth() !== Number(mm) - 1 || + d.getFullYear() !== Number(yyyy) + ); +} + +export function parseDdmmyyyy(value: string): Date { + const m = value.match(DDMMYYYY); + if (!m) throw new Error(`Date is not DD-MM-YYYY: ${value}`); + const [, dd, mm, yyyy] = m; + const d = new Date(Number(yyyy), Number(mm) - 1, Number(dd)); + if ( + Number.isNaN(d.getTime()) || + d.getDate() !== Number(dd) || + d.getMonth() !== Number(mm) - 1 || + d.getFullYear() !== Number(yyyy) + ) { + throw new Error(`Invalid date: ${value}`); + } + return d; +} + export function monthKey(value?: string): string { const d = parseOccurredAt(value); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`; diff --git a/src/common/utils/fieldConfigs.ts b/src/common/utils/fieldConfigs.ts new file mode 100644 index 0000000..0522faf --- /dev/null +++ b/src/common/utils/fieldConfigs.ts @@ -0,0 +1,26 @@ +import type { ResourceConfig } from "../../../react-openapi"; +import type { TxnFieldConfigs } from "../types"; + +export function buildTxnFieldConfigs(resources: ResourceConfig[]): TxnFieldConfigs | null { + const expensesRes = resources.find((r) => r.name === "expenses"); + const entitiesRes = resources.find((r) => r.name === "entities"); + const accountsRes = resources.find((r) => r.name === "accounts"); + const find = (res: ResourceConfig | undefined, name: string) => res?.fields.find((f) => f.name === name); + const entity = find(expensesRes, "entity"); + const amount = find(expensesRes, "amount"); + const account = find(expensesRes, "account"); + const occurredAt = find(expensesRes, "occurred_at"); + const logo = find(entitiesRes, "logo"); + if (!entity || !amount || !account || !occurredAt || !logo) return null; + return { + entity, + amount, + account, + occurredAt, + logo, + formats: { + entity: entitiesRes?.displayFormat ?? "{name}", + account: accountsRes?.displayFormat ?? "{name}", + }, + }; +} \ No newline at end of file diff --git a/src/common/utils/transactions.ts b/src/common/utils/transactions.ts new file mode 100644 index 0000000..9538088 --- /dev/null +++ b/src/common/utils/transactions.ts @@ -0,0 +1,27 @@ +import type { ExpenseItem, GroupedMonth } from "../types"; +import { monthKey, parseOccurredAt } from "./dates"; + +export function isExpense(item: ExpenseItem): boolean { + return (item.amount ?? 0) < 0; +} + +export function groupByMonth(items: ExpenseItem[]): GroupedMonth[] { + const map = new Map(); + for (const item of items) { + const key = monthKey(item.occurred_at); + const list = map.get(key) ?? []; + list.push(item); + map.set(key, list); + } + return [...map.entries()] + .map(([key, list]) => { + const sorted = [...list].sort( + (a, b) => parseOccurredAt(b.occurred_at).getTime() - parseOccurredAt(a.occurred_at).getTime(), + ); + const currency = sorted.find((it) => it.account?.currency)?.account?.currency ?? "INR"; + const spent = sorted.filter(isExpense).reduce((sum, it) => sum + Math.abs(it.amount ?? 0), 0); + const income = sorted.filter((it) => !isExpense(it)).reduce((sum, it) => sum + (it.amount ?? 0), 0); + return { key, items: sorted, spent, income, currency }; + }) + .sort((a, b) => b.key.localeCompare(a.key)); +} \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 7c4d8ea..b5fe503 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -17,8 +17,8 @@ import { import Home from './Home'; import FetchRequests from './FetchRequest/FetchRequestCreate'; import FetchRequestDetail from './FetchRequest/FetchRequestDetail'; -import Expense from './Expense/Expense'; -import Reports from './Reports/Reports'; +import Expense from './Expense'; +import Reports from './Reports/Report'; import { AppProvider, useAppContext, Admin, ProfileRoutes } from '../react-openapi'; import { AuthProvider, AuthPage, useAuth, ProfileCreate, ProfileEdit, ProfileView } from "../react-auth"; import Header from './Header';