shared currency field + expenses rendered from react-openapi fields

- add CurrencyField renderer (inherits NumberField for editing) with
  cached Intl formatter; route via uiType "currency" in list/detail/form
- add resourceConfig.fieldTypes override applied in AppProvider, so
  amount becomes currency for both Admin and the Expenses page
- render Expenses page through react-openapi fields (ListCellRenderer,
  DetailFieldRenderer, applyDisplayFormat) instead of custom fields
- resolve relative /uploads entity logos against the API base URL on
  load so entity logos render on the expenses feed
- drop custom avatar, currency/date formatters from the Expense module
This commit is contained in:
2026-08-17 16:27:43 +05:30
parent d6856a538f
commit 3dd833ac2b
13 changed files with 200 additions and 191 deletions

View File

@@ -5,6 +5,7 @@ export { useAppContext } from "./src/context/AppContext";
export { useResource } from "./src/context/useResource"; export { useResource } from "./src/context/useResource";
export { ListCellRenderer, DetailFieldRenderer, applyDisplayFormat } from "./src/components/fields"; export { ListCellRenderer, DetailFieldRenderer, applyDisplayFormat } from "./src/components/fields";
export { FormFieldRenderer } from "./src/components/fields/FormFieldRenderer"; export { FormFieldRenderer } from "./src/components/fields/FormFieldRenderer";
export { CurrencyField, formatCurrency } from "./src/components/fields/renderers/CurrencyField";
export { SseStreamView } from "./src/components/SseStreamView"; export { SseStreamView } from "./src/components/SseStreamView";
export { SseConnectionStatus } from "./src/components/SseConnectionStatus"; export { SseConnectionStatus } from "./src/components/SseConnectionStatus";
export { getApi } from "./src/hooks/useApi"; export { getApi } from "./src/hooks/useApi";

View File

@@ -2,6 +2,7 @@ import React from "react";
import { Box, Typography, Avatar } from "@mui/material"; import { Box, Typography, Avatar } from "@mui/material";
import type { FieldConfig } from "../../types"; import type { FieldConfig } from "../../types";
import { ListCellRenderer } from "./ListCellRenderer"; import { ListCellRenderer } from "./ListCellRenderer";
import { CurrencyField } from "./renderers/CurrencyField";
interface DetailFieldProps { interface DetailFieldProps {
field: FieldConfig; field: FieldConfig;
@@ -18,7 +19,9 @@ export function DetailFieldRenderer({ field, value, displayFormat, basePath }: D
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ mb: 0.25, display: "block" }}> <Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ mb: 0.25, display: "block" }}>
{field.label} {field.label}
</Typography> </Typography>
{field.uiType === "image" ? ( {field.uiType === "currency" ? (
<CurrencyField value={Number(value)} large />
) : field.uiType === "image" ? (
<Avatar src={value} variant="rounded" sx={{ width: 120, height: 120 }} /> <Avatar src={value} variant="rounded" sx={{ width: 120, height: 120 }} />
) : ( ) : (
<ListCellRenderer field={field} value={value} displayFormat={displayFormat} basePath={basePath} /> <ListCellRenderer field={field} value={value} displayFormat={displayFormat} basePath={basePath} />

View File

@@ -96,6 +96,17 @@ export function FormFieldRenderer({ field, value, onChange, error, fkOptions, fk
return <BooleanField field={field} value={value} onChange={onChange} />; return <BooleanField field={field} value={value} onChange={onChange} />;
} }
if (field.uiType === "currency") {
return (
<NumberField
field={field}
value={value}
onChange={onChange}
error={error}
/>
);
}
if (field.type === "integer" || field.type === "number") { if (field.type === "integer" || field.type === "number") {
return ( return (
<NumberField <NumberField

View File

@@ -4,6 +4,7 @@ import { Box, Typography, Chip, Avatar, Dialog, DialogTitle, DialogContent, Dial
import type { FieldConfig } from "../../types"; import type { FieldConfig } from "../../types";
import { applyDisplayFormat } from "./utils"; import { applyDisplayFormat } from "./utils";
import { InlineRefField } from "./renderers/InlineRefField"; import { InlineRefField } from "./renderers/InlineRefField";
import { CurrencyField } from "./renderers/CurrencyField";
import { extractFields } from "../../transformers/field-config"; import { extractFields } from "../../transformers/field-config";
import { useAppContext } from "../../context/AppContext"; import { useAppContext } from "../../context/AppContext";
@@ -140,6 +141,10 @@ export function ListCellRenderer({ field, value, displayFormat, basePath }: List
return <Avatar src={value} variant="rounded" sx={{ width: 40, height: 40 }} />; return <Avatar src={value} variant="rounded" sx={{ width: 40, height: 40 }} />;
} }
if (field.uiType === "currency" && value != null && !Number.isNaN(Number(value))) {
return <CurrencyField value={Number(value)} />;
}
if (field.type === "boolean") { if (field.type === "boolean") {
return <Chip label={value ? "Yes" : "No"} size="small" color={value ? "success" : "default"} />; return <Chip label={value ? "Yes" : "No"} size="small" color={value ? "success" : "default"} />;
} }

View File

@@ -3,3 +3,4 @@ export { ListCellRenderer } from "./ListCellRenderer";
export { DetailFieldRenderer } from "./DetailFieldRenderer"; export { DetailFieldRenderer } from "./DetailFieldRenderer";
export { applyDisplayFormat } from "./utils"; export { applyDisplayFormat } from "./utils";
export { JsonField } from "./renderers/JsonField"; export { JsonField } from "./renderers/JsonField";
export { CurrencyField, formatCurrency } from "./renderers/CurrencyField";

View File

@@ -0,0 +1,42 @@
import React from "react";
import { Typography } from "@mui/material";
const CURRENCIES = ["INR", "USD", "EUR", "GBP", "AED", "SGD"];
const _currencyFormatters = new Map<string, Intl.NumberFormat>();
export function formatCurrency(amount: number, currency?: string): string {
const code = currency && CURRENCIES.includes(currency) ? currency : "INR";
let formatter = _currencyFormatters.get(code);
if (!formatter) {
formatter = new Intl.NumberFormat("en-IN", {
style: "currency",
currency: code,
maximumFractionDigits: 2,
});
_currencyFormatters.set(code, formatter);
}
return formatter.format(amount);
}
interface CurrencyFieldProps {
value: number;
currency?: string;
large?: boolean;
}
export function CurrencyField({ value, currency, large }: CurrencyFieldProps) {
const negative = value < 0;
return (
<Typography
component="span"
sx={{
fontWeight: 700,
fontSize: large ? "0.9375rem" : "0.8125rem",
fontVariantNumeric: "tabular-nums",
color: negative ? "error.main" : "success.main",
}}
>
{formatCurrency(value, currency)}
</Typography>
);
}

View File

@@ -43,6 +43,20 @@ function extractProfileOperations(spec: any): ProfileOperation[] {
return ops; return ops;
} }
function applyResourceOverrides(configs: ResourceConfig[], specConfiguration: SpecConfiguration): ResourceConfig[] {
for (const resource of configs) {
const fieldTypes = specConfiguration.resourceConfig?.[resource.name]?.fieldTypes;
if (!fieldTypes) continue;
// fields and orderedFields share the same FieldConfig object references,
// so an in-place mutation updates both.
for (const field of resource.fields) {
const override = fieldTypes[field.name];
if (override) field.uiType = override;
}
}
return configs;
}
const DEFAULT_AUTH_CONFIG: AuthConfig = { const DEFAULT_AUTH_CONFIG: AuthConfig = {
serverUrl: "", serverUrl: "",
loginPath: "/login", loginPath: "/login",
@@ -88,7 +102,7 @@ export function AppProvider({ specConfiguration, children }: AppProviderProps) {
if (errs.length === 0) { if (errs.length === 0) {
const configs = buildResourceConfigs(spec); const configs = buildResourceConfigs(spec);
if (!cancelled) { if (!cancelled) {
setResources(configs); setResources(applyResourceOverrides(configs, specConfiguration));
} }
const baseUrl = specConfiguration.baseApiUrl ?? spec.servers?.[0]?.url ?? ""; const baseUrl = specConfiguration.baseApiUrl ?? spec.servers?.[0]?.url ?? "";

View File

@@ -4,6 +4,8 @@ export interface ResourceConfiguration {
filterOptions?: { filterOptions?: {
mode?: FilterMode; mode?: FilterMode;
}; };
/** Map of field name → uiType override (e.g. { amount: "currency" }). */
fieldTypes?: Record<string, string>;
} }
export interface ProfileComponents { export interface ProfileComponents {

View File

@@ -10,11 +10,13 @@ import {
} from "@mui/material"; } from "@mui/material";
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong"; import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useResource } from "../../react-openapi"; import { useResource, useAppContext, formatCurrency } from "../../react-openapi";
import { PageHeader } from "../ui/PageHeader"; import { PageHeader } from "../ui/PageHeader";
import { EmptyState } from "../ui/EmptyState"; import { EmptyState } from "../ui/EmptyState";
import { ExpenseList } from "./ExpenseList"; import { ExpenseList } from "./ExpenseList";
import { ExpenseItem, formatCurrency, isExpense, monthKey, monthLabel } from "./types"; import { ExpenseItem, ExpenseFieldConfigs, isExpense, monthKey, monthLabel, resolveLogoUrl } from "./types";
const API_BASE = import.meta.env.VITE_API_BASE_URL;
function StatCard({ label, value, hint }: { label: string; value: string; hint?: string }) { function StatCard({ label, value, hint }: { label: string; value: string; hint?: string }) {
return ( return (
@@ -39,13 +41,50 @@ function StatCard({ label, value, hint }: { label: string; value: string; hint?:
export default function Expense() { export default function Expense() {
const navigate = useNavigate(); const navigate = useNavigate();
const { list, loading, error } = useResource("expenses"); const { list, loading, error, resource } = useResource("expenses");
const { resources } = useAppContext();
const [items, setItems] = useState<ExpenseItem[] | null>(null); const [items, setItems] = useState<ExpenseItem[] | null>(null);
const fieldConfigs = useMemo<ExpenseFieldConfigs | null>(() => {
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]);
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
list({ limit: 0 }).then((res) => { list({ limit: 0 }).then((res) => {
if (mounted) setItems(res.items as ExpenseItem[]); if (!mounted) return;
const rows = (res.items ?? []) as ExpenseItem[];
const normalized = rows.map((it) => ({
...it,
entity: it.entity
? { ...it.entity, logo: resolveLogoUrl(it.entity.logo, API_BASE) }
: it.entity,
}));
setItems(normalized);
}); });
return () => { return () => {
mounted = false; mounted = false;
@@ -132,9 +171,9 @@ export default function Expense() {
<StatCard label="Income" value={formatCurrency(summary.totalIncome, summary.currency)} /> <StatCard label="Income" value={formatCurrency(summary.totalIncome, summary.currency)} />
</Box> </Box>
<ExpenseList items={sorted} /> {fieldConfigs && <ExpenseList items={sorted} fields={fieldConfigs} />}
</> </>
)} )}
</Container> </Container>
); );
} }

View File

@@ -1,87 +1,24 @@
import React from "react"; import React from "react";
import { Box, Typography, Chip, Divider } from "@mui/material"; import { Box, Divider } from "@mui/material";
import type { ExpenseItem } from "./types"; import { DetailFieldRenderer } from "../../react-openapi";
import { formatCurrency, formatDate, formatDateTime } from "./types"; import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
function Row({ label, value }: { label: string; value: React.ReactNode }) { interface ExpenseDetailProps {
return ( item: ExpenseItem;
<Box sx={{ display: "flex", gap: 2, alignItems: "baseline", py: 0.75 }}> fields: ExpenseFieldConfigs;
<Typography variant="body2" color="text.secondary" sx={{ width: 160, flexShrink: 0 }}>
{label}
</Typography>
<Box sx={{ flex: 1, minWidth: 0 }}>{value}</Box>
</Box>
);
} }
export function ExpenseDetail({ item }: { item: ExpenseItem }) { export function ExpenseDetail({ item, fields }: ExpenseDetailProps) {
const account = item.account;
const tags = item.tags ?? [];
const last4 = account?.number ? `${account.number.slice(-4)}` : "";
return ( return (
<Box sx={{ pt: 1, pb: 0.5 }}> <Box sx={{ pt: 1, pb: 0.5 }}>
<Divider sx={{ mb: 1.5 }} /> <Divider sx={{ mb: 1.5 }} />
<Row <Box sx={{ display: "flex", flexDirection: "column", gap: 0.5 }}>
label="Amount" <DetailFieldRenderer field={fields.entity} value={item.entity} displayFormat={fields.formats.entity} />
value={ <DetailFieldRenderer field={fields.amount} value={item.amount} />
<Typography variant="body2" fontWeight={600}> <DetailFieldRenderer field={fields.account} value={item.account} displayFormat={fields.formats.account} />
{formatCurrency(item.amount, account?.currency)} <DetailFieldRenderer field={fields.tags} value={item.tags} displayFormat={fields.formats.tags} />
</Typography> <DetailFieldRenderer field={fields.occurredAt} value={item.occurred_at} />
} </Box>
/>
<Row
label="Account"
value={
<Box sx={{ display: "flex", alignItems: "center", flexWrap: "wrap", gap: 1 }}>
<Typography component="span" variant="body2">
{account ? `${account.name}${last4 ? ` (${last4})` : ""}` : "—"}
</Typography>
{account?.type && (
<Chip
size="small"
label={account.type.replace(/_/g, " ")}
variant="outlined"
sx={{ fontSize: 11, height: 20 }}
/>
)}
</Box>
}
/>
<Row
label="Tags"
value={
tags.length > 0 ? (
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap" }}>
{tags.map((tag, i) => (
<Chip
key={`${tag.name}-${i}`}
size="small"
label={`${tag.icon ?? ""} ${tag.name ?? ""}`.trim()}
sx={{ fontSize: 12, height: 24 }}
/>
))}
</Box>
) : (
<Typography variant="body2" color="text.disabled">
No tags
</Typography>
)
}
/>
<Row label="Date" value={<Typography variant="body2">{formatDate(item.occurred_at)}</Typography>} />
<Row
label="Transaction ID"
value={
<Typography variant="body2" sx={{ fontFamily: "monospace", fontSize: "0.8125rem" }}>
{item.id}
</Typography>
}
/>
<Row
label="Created"
value={<Typography variant="body2" color="text.secondary">{formatDateTime(item.created_at)}</Typography>}
/>
</Box> </Box>
); );
} }

View File

@@ -5,13 +5,11 @@ import {
Accordion, Accordion,
AccordionSummary, AccordionSummary,
AccordionDetails, AccordionDetails,
Avatar,
Chip,
useTheme,
} from "@mui/material"; } from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import type { ExpenseItem } from "./types"; import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi";
import { formatCurrency, formatDate, isExpense, monthKey, monthLabel } from "./types"; import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
import { monthKey, monthLabel } from "./types";
import { ExpenseDetail } from "./ExpenseDetail"; import { ExpenseDetail } from "./ExpenseDetail";
interface GroupedMonth { interface GroupedMonth {
@@ -41,40 +39,15 @@ function groupByMonth(items: ExpenseItem[]): GroupedMonth[] {
.sort((a, b) => b.key.localeCompare(a.key)); .sort((a, b) => b.key.localeCompare(a.key));
} }
function EntityAvatar({ entity }: { entity: ExpenseItem["entity"] }) {
const theme = useTheme();
const name = entity?.name ?? "?";
const letter = name.trim().charAt(0).toUpperCase() || "?";
const logo = entity?.logo;
const isImage = typeof logo === "string" && (logo.startsWith("http") || logo.startsWith("data:"));
return (
<Avatar
src={isImage ? logo : undefined}
sx={{
width: 36,
height: 36,
fontSize: 15,
fontWeight: 700,
bgcolor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
borderRadius: 2,
}}
>
{letter}
</Avatar>
);
}
interface ExpenseCardProps { interface ExpenseCardProps {
item: ExpenseItem; item: ExpenseItem;
expanded: boolean; expanded: boolean;
currency: string; currency: string;
fields: ExpenseFieldConfigs;
onToggle: (id: string) => void; onToggle: (id: string) => void;
} }
const ExpenseCard = React.memo(function ExpenseCard({ item, expanded, currency, onToggle }: ExpenseCardProps) { const ExpenseCard = React.memo(function ExpenseCard({ item, expanded, currency, fields, onToggle }: ExpenseCardProps) {
const negative = isExpense(item);
const itemCurrency = item.account?.currency ?? currency; const itemCurrency = item.account?.currency ?? currency;
return ( return (
@@ -105,7 +78,21 @@ const ExpenseCard = React.memo(function ExpenseCard({ item, expanded, currency,
}, },
}} }}
> >
<EntityAvatar entity={item.entity} /> <Box
sx={{
width: 40,
flexShrink: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{item.entity?.logo ? (
<ListCellRenderer field={fields.logo} value={item.entity.logo} />
) : (
<Box sx={{ width: 40, height: 40, borderRadius: 2, bgcolor: "action.hover" }} />
)}
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}> <Box sx={{ flex: 1, minWidth: 0 }}>
<Typography <Typography
variant="body1" variant="body1"
@@ -113,43 +100,34 @@ const ExpenseCard = React.memo(function ExpenseCard({ item, expanded, currency,
noWrap noWrap
sx={{ fontSize: "0.9375rem", lineHeight: 1.3 }} sx={{ fontSize: "0.9375rem", lineHeight: 1.3 }}
> >
{item.entity?.name ?? "Unknown"} {item.entity ? applyDisplayFormat(item.entity, fields.formats.entity) : "Unknown"}
</Typography> </Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mt: 0.25 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1, mt: 0.25, flexWrap: "wrap" }}>
<Typography variant="caption" color="text.secondary"> <ListCellRenderer field={fields.occurredAt} value={item.occurred_at} />
{formatDate(item.occurred_at)}
</Typography>
{item.account?.name && ( {item.account?.name && (
<Chip <ListCellRenderer
size="small" field={fields.account}
label={item.account.name} value={item.account}
variant="outlined" displayFormat={fields.formats.account}
sx={{ height: 20, fontSize: 11, "& .MuiChip-label": { px: 1 } }}
/> />
)} )}
</Box> </Box>
</Box> </Box>
<Typography <CurrencyField value={item.amount} currency={itemCurrency} large />
variant="body1"
fontWeight={700}
sx={{
fontSize: "0.9375rem",
fontVariantNumeric: "tabular-nums",
color: negative ? "error.main" : "success.main",
flexShrink: 0,
}}
>
{formatCurrency(item.amount, itemCurrency)}
</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{ pt: 0 }}> <AccordionDetails sx={{ pt: 0 }}>
<ExpenseDetail item={item} /> <ExpenseDetail item={item} fields={fields} />
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
); );
}); });
export function ExpenseList({ items }: { items: ExpenseItem[] }) { interface ExpenseListProps {
items: ExpenseItem[];
fields: ExpenseFieldConfigs;
}
export function ExpenseList({ items, fields }: ExpenseListProps) {
const [expandedId, setExpandedId] = useState<string | null>(null); const [expandedId, setExpandedId] = useState<string | null>(null);
const groups = useMemo(() => groupByMonth(items), [items]); const groups = useMemo(() => groupByMonth(items), [items]);
const handleToggle = useCallback((id: string) => { const handleToggle = useCallback((id: string) => {
@@ -180,6 +158,7 @@ export function ExpenseList({ items }: { items: ExpenseItem[] }) {
item={item} item={item}
expanded={expandedId === item.id} expanded={expandedId === item.id}
currency={group.currency} currency={group.currency}
fields={fields}
onToggle={handleToggle} onToggle={handleToggle}
/> />
))} ))}
@@ -190,4 +169,4 @@ export function ExpenseList({ items }: { items: ExpenseItem[] }) {
); );
} }
export { groupByMonth }; export { groupByMonth };

View File

@@ -1,3 +1,5 @@
import type { FieldConfig } from "../../react-openapi";
export interface ExpenseItem { export interface ExpenseItem {
id: string; id: string;
entity?: { name?: string; type?: string; logo?: string } | null; entity?: { name?: string; type?: string; logo?: string } | null;
@@ -9,57 +11,29 @@ export interface ExpenseItem {
updated_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 { export function isExpense(item: ExpenseItem): boolean {
return (item.amount ?? 0) < 0; return (item.amount ?? 0) < 0;
} }
const CURRENCIES = ["INR", "USD", "EUR", "GBP", "AED", "SGD"]; export function resolveLogoUrl(logo?: string, base?: string): string | undefined {
const _currencyFormatters = new Map<string, Intl.NumberFormat>(); if (!logo) return undefined;
if (logo.startsWith("http") || logo.startsWith("data:")) return logo;
export function formatCurrency(amount: number, currency?: string): string { if (logo.startsWith("/") && base) return `${base.replace(/\/+$/, "")}${logo}`;
const code = currency && CURRENCIES.includes(currency) ? currency : "INR"; return logo;
let formatter = _currencyFormatters.get(code);
if (!formatter) {
formatter = new Intl.NumberFormat("en-IN", {
style: "currency",
currency: code,
maximumFractionDigits: 2,
});
_currencyFormatters.set(code, formatter);
}
return formatter.format(amount);
}
const _dateCache = new Map<string, string>();
export function formatDate(iso?: string): string {
if (!iso) return "—";
const cached = _dateCache.get(iso);
if (cached) return cached;
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "—";
const out = d.toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" });
_dateCache.set(iso, out);
return out;
}
const _dateTimeCache = new Map<string, string>();
export function formatDateTime(iso?: string): string {
if (!iso) return "—";
const cached = _dateTimeCache.get(iso);
if (cached) return cached;
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "—";
const out = d.toLocaleString("en-IN", {
day: "numeric",
month: "short",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
_dateTimeCache.set(iso, out);
return out;
} }
export function monthKey(iso?: string): string { export function monthKey(iso?: string): string {
@@ -74,4 +48,4 @@ export function monthLabel(key: string): string {
if (!y || !m) return key; if (!y || !m) return key;
const d = new Date(y, m - 1, 1); const d = new Date(y, m - 1, 1);
return d.toLocaleDateString("en-IN", { month: "long", year: "numeric" }); return d.toLocaleDateString("en-IN", { month: "long", year: "numeric" });
} }

View File

@@ -11,6 +11,7 @@ export const specConfiguration: SpecConfiguration = {
resourceConfig: { resourceConfig: {
expenses: { expenses: {
filterOptions: { mode: "client" }, filterOptions: { mode: "client" },
fieldTypes: { amount: "currency" },
}, },
}, },
}; };