diff --git a/apps/api/src/routes.js b/apps/api/src/routes.js index db0b8c4..3977703 100644 --- a/apps/api/src/routes.js +++ b/apps/api/src/routes.js @@ -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 @@ -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 { @@ -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 }); diff --git a/apps/web/src/components/settings/CategorySettings.jsx b/apps/web/src/components/settings/CategorySettings.jsx index a5b991f..f207c99 100644 --- a/apps/web/src/components/settings/CategorySettings.jsx +++ b/apps/web/src/components/settings/CategorySettings.jsx @@ -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'; @@ -51,25 +50,33 @@ export function CategorySettings({ categories, api, reload }) { return (
-

Kategori

Kelola kategori kas masuk dan kas keluar.

+

Kategori

Nonaktifkan kategori yang tidak dipakai lagi. Transaksi lama tetap tersimpan dan tetap dapat diaudit.

- NamaJenisUrutanStatus + NamaJenisUrutanTransaksiStatusAksi {categories.map((category) => {category.name} {category.type === 'INCOME' ? 'Kas Masuk' : 'Kas Keluar'} {category.sortOrder} + + {category.transactionCount ?? 0} + tercatat + {category.isActive ? 'Aktif' : 'Nonaktif'} - - - toggle(category)}>{category.isActive ? 'Nonaktifkan' : 'Aktifkan'} - + )} - {categories.length === 0 && Belum ada kategori.} + {categories.length === 0 && Belum ada kategori.}
diff --git a/apps/web/src/components/settings/SettingsNav.jsx b/apps/web/src/components/settings/SettingsNav.jsx index 88fd3f5..2d48dbe 100644 --- a/apps/web/src/components/settings/SettingsNav.jsx +++ b/apps/web/src/components/settings/SettingsNav.jsx @@ -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} diff --git a/apps/web/src/components/ui/button.jsx b/apps/web/src/components/ui/button.jsx index f24853f..2135f61 100644 --- a/apps/web/src/components/ui/button.jsx +++ b/apps/web/src/components/ui/button.jsx @@ -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', diff --git a/apps/web/src/components/ui/input-group.jsx b/apps/web/src/components/ui/input-group.jsx index b8155ef..82081f9 100644 --- a/apps/web/src/components/ui/input-group.jsx +++ b/apps/web/src/components/ui/input-group.jsx @@ -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} diff --git a/apps/web/src/components/ui/input.jsx b/apps/web/src/components/ui/input.jsx index c150751..32c9819 100644 --- a/apps/web/src/components/ui/input.jsx +++ b/apps/web/src/components/ui/input.jsx @@ -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} diff --git a/apps/web/src/components/ui/select.jsx b/apps/web/src/components/ui/select.jsx index fe37584..79def4b 100644 --- a/apps/web/src/components/ui/select.jsx +++ b/apps/web/src/components/ui/select.jsx @@ -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} diff --git a/apps/web/src/components/ui/sidebar.jsx b/apps/web/src/components/ui/sidebar.jsx index bbb2b32..0e243a4 100644 --- a/apps/web/src/components/ui/sidebar.jsx +++ b/apps/web/src/components/ui/sidebar.jsx @@ -45,7 +45,8 @@ function Sidebar({ className, children }) {