Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5aee8a5
qa: make collapsed sidebar width deterministic
feril3 Sep 24, 2026
f110ff2
qa: remove native chrome from ghost and link buttons
feril3 Sep 24, 2026
03bea8c
qa: keep editable inputs flat and clearly active
feril3 Sep 24, 2026
1fa30b8
qa: normalize input group surface
feril3 Sep 24, 2026
66ad9fc
qa: normalize select trigger surface
feril3 Sep 24, 2026
570f70e
qa: normalize textarea surface
feril3 Sep 24, 2026
0a419b5
qa: make active settings section easier to scan
feril3 Sep 24, 2026
5d69ed4
product: align transaction summary cards with cash flow terms
feril3 Sep 24, 2026
f9b9573
product: support one person serving as Imam and Khatib
feril3 Sep 24, 2026
9357b3b
product: expose category usage and audit activation changes
feril3 Sep 24, 2026
4eb6697
product: make category activation explicit and auditable
feril3 Sep 24, 2026
a4e5d49
qa: keep Public Display finance totals readable in narrow rail
feril3 Sep 24, 2026
0517065
qa: keep low-value cashflow activity visible on dashboard
feril3 Sep 24, 2026
93af4b9
qa: preserve small-value visibility in report charts
feril3 Sep 24, 2026
dcfbfad
product: label historical inactive category during transaction edit
feril3 Sep 24, 2026
d5d0f40
product: simplify Imam and Khatib sync toggle
feril3 Sep 24, 2026
3ae187d
test: lock final QA and requested operational updates
feril3 Sep 24, 2026
01889dc
test: lock compact Public Display finance totals
feril3 Sep 24, 2026
4fba64a
test: align legacy settings-nav assertion with final active state
feril3 Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions apps/api/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -1437,7 +1437,12 @@ apiRouter.get('/transaction-categories', requireAuth, (req, res) => {
type,
name,
is_active AS isActive,
sort_order AS sortOrder
sort_order AS sortOrder,
(
SELECT COUNT(*)
FROM transactions
WHERE transactions.category_id = transaction_categories.id
) AS transactionCount
FROM transaction_categories
${where}
ORDER BY type ASC, sort_order ASC, name ASC
Expand Down Expand Up @@ -1485,7 +1490,16 @@ apiRouter.put('/transaction-categories/:id', requireAuth, requireRole('ADMIN'),
return res.status(422).json({ message: 'Kategori belum valid.', errors: parsed.error.flatten().fieldErrors });
}

const existing = db.prepare('SELECT id FROM transaction_categories WHERE id = ?').get(req.params.id);
const existing = db.prepare(`
SELECT
id,
type,
name,
is_active AS isActive,
sort_order AS sortOrder
FROM transaction_categories
WHERE id = ?
`).get(req.params.id);
if (!existing) return res.status(404).json({ message: 'Kategori tidak ditemukan.' });

try {
Expand All @@ -1500,7 +1514,20 @@ apiRouter.put('/transaction-categories/:id', requireAuth, requireRole('ADMIN'),
action: 'TRANSACTION_CATEGORY_UPDATE',
entityType: 'TRANSACTION_CATEGORY',
entityId: existing.id,
details: { type: input.type, name: input.name, isActive: input.isActive }
details: {
before: {
type: existing.type,
name: existing.name,
isActive: Boolean(existing.isActive),
sortOrder: existing.sortOrder
},
after: {
type: input.type,
name: input.name,
isActive: input.isActive,
sortOrder: input.sortOrder
}
}
});

return res.json({ id: existing.id });
Expand Down
25 changes: 16 additions & 9 deletions apps/web/src/components/settings/CategorySettings.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useState } from 'react';
import { IconDots, IconPlus } from '@tabler/icons-react';
import { IconPlus } from '@tabler/icons-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
Expand Down Expand Up @@ -51,25 +50,33 @@ export function CategorySettings({ categories, api, reload }) {
return (
<section className="overflow-hidden rounded-xl border bg-card">
<div className="flex flex-col gap-3 border-b bg-muted/25 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-5">
<div><h2 className="text-lg font-semibold">Kategori</h2><p className="mt-1 text-sm text-muted-foreground">Kelola kategori kas masuk dan kas keluar.</p></div>
<div><h2 className="text-lg font-semibold">Kategori</h2><p className="mt-1 text-sm text-muted-foreground">Nonaktifkan kategori yang tidak dipakai lagi. Transaksi lama tetap tersimpan dan tetap dapat diaudit.</p></div>
<Button className="w-full sm:w-auto" onClick={() => setOpen(true)}><IconPlus />Tambah kategori</Button>
</div>
<Table>
<TableHeader><TableRow><TableHead>Nama</TableHead><TableHead>Jenis</TableHead><TableHead>Urutan</TableHead><TableHead>Status</TableHead><TableHead className="w-16" /></TableRow></TableHeader>
<TableHeader><TableRow><TableHead>Nama</TableHead><TableHead>Jenis</TableHead><TableHead>Urutan</TableHead><TableHead>Transaksi</TableHead><TableHead>Status</TableHead><TableHead className="w-32">Aksi</TableHead></TableRow></TableHeader>
<TableBody>
{categories.map((category) => <TableRow key={category.id}>
<TableCell className="font-medium">{category.name}</TableCell>
<TableCell>{category.type === 'INCOME' ? 'Kas Masuk' : 'Kas Keluar'}</TableCell>
<TableCell>{category.sortOrder}</TableCell>
<TableCell>
<span className="text-sm font-medium">{category.transactionCount ?? 0}</span>
<span className="ml-1 text-xs text-muted-foreground">tercatat</span>
</TableCell>
<TableCell><Badge variant={category.isActive ? 'success' : 'outline'}>{category.isActive ? 'Aktif' : 'Nonaktif'}</Badge></TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild><Button variant="ghost" size="icon-sm" aria-label={`Aksi ${category.name}`}><IconDots /></Button></DropdownMenuTrigger>
<DropdownMenuContent align="end"><DropdownMenuItem onSelect={() => toggle(category)}>{category.isActive ? 'Nonaktifkan' : 'Aktifkan'}</DropdownMenuItem></DropdownMenuContent>
</DropdownMenu>
<Button
type="button"
variant={category.isActive ? 'outline' : 'secondary'}
size="sm"
onClick={() => toggle(category)}
>
{category.isActive ? 'Nonaktifkan' : 'Aktifkan'}
</Button>
</TableCell>
</TableRow>)}
{categories.length === 0 && <TableRow><TableCell colSpan={5} className="py-10 text-center text-muted-foreground">Belum ada kategori.</TableCell></TableRow>}
{categories.length === 0 && <TableRow><TableCell colSpan={6} className="py-10 text-center text-muted-foreground">Belum ada kategori.</TableCell></TableRow>}
</TableBody>
</Table>

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/settings/SettingsNav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function SettingsNav({ value, onChange }) {
key={id}
type="button"
onClick={() => onChange(id)}
className={`flex w-full appearance-none items-center rounded-r-md border-0 border-l-2 bg-transparent px-3 py-2.5 text-left text-sm transition-colors ${value === id ? 'border-primary font-semibold text-foreground' : 'border-transparent font-medium text-muted-foreground hover:border-border hover:bg-muted/60 hover:text-foreground'}`}
className={`flex w-full appearance-none items-center rounded-r-md border-0 border-l-2 bg-transparent px-3 py-2.5 text-left text-sm transition-colors ${value === id ? 'border-primary bg-accent/35 font-semibold text-foreground' : 'border-transparent font-medium text-muted-foreground hover:border-border hover:bg-muted/60 hover:text-foreground'}`}
>
{label}
</button>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/ui/button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ const buttonVariants = cva(
default: 'bg-primary px-4 text-primary-foreground hover:bg-primary/92',
secondary: 'border border-transparent bg-secondary px-4 text-secondary-foreground hover:border-border hover:bg-secondary/80',
outline: 'border-input bg-card px-4 text-foreground hover:border-ring/70 hover:bg-muted',
ghost: 'px-3 text-foreground hover:bg-accent hover:text-accent-foreground',
ghost: 'bg-transparent px-3 text-foreground hover:bg-accent hover:text-accent-foreground',
destructive: 'bg-destructive px-4 text-white hover:bg-destructive/90',
link: 'px-1 text-primary underline-offset-4 hover:underline'
link: 'bg-transparent px-1 text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-10',
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ui/input-group.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function InputGroup({
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input bg-card shadow-[0_1px_1px_rgba(23,33,30,0.03)] transition-[border-color,box-shadow,background-color] outline-none hover:border-ring/70 in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/25 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input bg-card transition-[border-color,box-shadow,background-color] outline-none hover:border-ring/70 in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/25 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className
)}
{...props}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ui/input.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const Input = React.forwardRef(function Input({ className, type = 'text', ...pro
ref={ref}
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-card px-3 py-2 text-sm text-foreground shadow-[0_1px_1px_rgba(23,33,30,0.03)] outline-none transition-[border-color,box-shadow,background-color] placeholder:text-muted-foreground hover:border-ring/70 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-80',
'flex h-10 w-full rounded-md border border-input bg-card px-3 py-2 text-sm text-foreground outline-none transition-[border-color,box-shadow,background-color] placeholder:text-muted-foreground hover:border-ring/70 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-80',
className
)}
{...props}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ui/select.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-card py-2 pr-2 pl-2.5 text-sm whitespace-nowrap shadow-[0_1px_1px_rgba(23,33,30,0.03)] transition-[border-color,box-shadow,background-color] outline-none select-none hover:border-ring/70 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-80 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex w-fit appearance-none items-center justify-between gap-1.5 rounded-lg border border-input bg-card py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-[border-color,box-shadow,background-color] outline-none select-none hover:border-ring/70 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-80 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/ui/sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ function Sidebar({ className, children }) {
<aside
data-state={open ? 'expanded' : 'collapsed'}
className={cn(
'hidden min-h-screen shrink-0 border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width] duration-200 lg:flex lg:w-64 lg:flex-col lg:data-[state=collapsed]:w-[72px]',
'hidden min-h-screen shrink-0 border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width] duration-200 lg:flex lg:flex-col',
open ? 'lg:w-64' : 'lg:w-[72px]',
className
)}
>
Expand Down Expand Up @@ -97,7 +98,7 @@ function SidebarMenuButton({ className, active = false, asChild = false, ...prop
return (
<Comp
className={cn(
'flex min-h-10 w-full items-center gap-3 rounded-md px-3 text-left text-[13px] font-medium text-sidebar-foreground/78 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-[18px] [&_svg]:shrink-0',
'flex min-h-10 w-full appearance-none items-center gap-3 rounded-md border-0 bg-transparent px-3 text-left text-[13px] font-medium text-sidebar-foreground/78 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-[18px] [&_svg]:shrink-0',
active && 'bg-sidebar-accent text-sidebar-accent-foreground',
className
)}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ui/textarea.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function Textarea({
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-card px-2.5 py-2 text-base shadow-[0_1px_1px_rgba(23,33,30,0.03)] transition-[border-color,box-shadow,background-color] outline-none placeholder:text-muted-foreground hover:border-ring/70 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-80 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-card px-2.5 py-2 text-base transition-[border-color,box-shadow,background-color] outline-none placeholder:text-muted-foreground hover:border-ring/70 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-80 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/features/transactions/TransactionEditSheet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ export function TransactionEditSheet({ transaction, open, onOpenChange, onSaved
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem key={category.id} value={String(category.id)}>{category.name}</SelectItem>
<SelectItem key={category.id} value={String(category.id)}>
{category.name}{category.isActive ? '' : ' (nonaktif, transaksi lama)'}
</SelectItem>
))}
</SelectContent>
</Select>
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ function formatAxisAmount(value) {
return new Intl.NumberFormat('id-ID', { maximumFractionDigits: 0 }).format(number);
}

function CashflowDot({ cx, cy, payload, dataKey, color }) {
if (!payload || Number(payload[dataKey] ?? 0) <= 0) return null;
return <circle cx={cx} cy={cy} r="3" fill={color} stroke="var(--ui-card)" strokeWidth="1.5" />;
}

function Metric({ label, value, tone = 'default', caption, featured = false }) {
const valueClass = tone === 'income'
? 'text-primary'
Expand Down Expand Up @@ -188,8 +193,8 @@ export default function Dashboard() {
formatter={(value) => formatRupiah(value)}
/>}
/>
<Area type="monotone" dataKey="income" stroke="var(--color-income)" strokeWidth={2.5} fill="url(#incomeFill)" dot={false} activeDot={{ r: 4 }} isAnimationActive={false} />
<Area type="monotone" dataKey="expense" stroke="var(--color-expense)" strokeWidth={2.5} fill="url(#expenseFill)" dot={false} activeDot={{ r: 4 }} isAnimationActive={false} />
<Area type="monotone" dataKey="income" stroke="var(--color-income)" strokeWidth={2.5} fill="url(#incomeFill)" dot={(props) => <CashflowDot {...props} dataKey="income" color="var(--color-income)" />} activeDot={{ r: 4 }} isAnimationActive={false} />
<Area type="monotone" dataKey="expense" stroke="var(--color-expense)" strokeWidth={2.5} fill="url(#expenseFill)" dot={(props) => <CashflowDot {...props} dataKey="expense" color="var(--color-expense)" />} activeDot={{ r: 4 }} isAnimationActive={false} />
</AreaChart>
</ChartContainer>
</CardContent>
Expand Down
16 changes: 13 additions & 3 deletions apps/web/src/pages/PublicDisplay.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ const FINANCE_CAROUSEL_INTERVAL_MS = 8500;
const DISPLAY_TIMEZONE = 'Asia/Jakarta';
const RECENT_TRANSACTION_LIMIT = 4;

function formatCompactRupiah(value) {
const amount = Number(value ?? 0);
const compact = (number, suffix) => `Rp ${new Intl.NumberFormat('id-ID', { maximumFractionDigits: 2 }).format(number)} ${suffix}`;

if (Math.abs(amount) >= 1_000_000_000) return compact(amount / 1_000_000_000, 'M');
if (Math.abs(amount) >= 1_000_000) return compact(amount / 1_000_000, 'jt');
if (Math.abs(amount) >= 1_000) return compact(amount / 1_000, 'rb');
return formatRupiah(amount);
}

function displayDateIso(date = new Date()) {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: DISPLAY_TIMEZONE,
Expand Down Expand Up @@ -371,17 +381,17 @@ export default function PublicDisplay() {

<div className="finance-balance">
<span>Saldo saat ini</span>
<strong>{formatRupiah(data?.finance?.currentBalance ?? 0)}</strong>
<strong title={formatRupiah(data?.finance?.currentBalance ?? 0)}>{formatCompactRupiah(data?.finance?.currentBalance ?? 0)}</strong>
</div>

<div className="finance-flow-summary">
<div>
<span>Kas masuk</span>
<strong className="finance-income">+{formatRupiah(data?.finance?.totalIncome ?? 0)}</strong>
<strong className="finance-income" title={formatRupiah(data?.finance?.totalIncome ?? 0)}>+{formatCompactRupiah(data?.finance?.totalIncome ?? 0)}</strong>
</div>
<div>
<span>Kas keluar</span>
<strong className="finance-expense">-{formatRupiah(data?.finance?.totalExpense ?? 0)}</strong>
<strong className="finance-expense" title={formatRupiah(data?.finance?.totalExpense ?? 0)}>-{formatCompactRupiah(data?.finance?.totalExpense ?? 0)}</strong>
</div>
</div>

Expand Down
Loading
Loading