From d91ec8869e6b39addaa2bfc8d5e4f05135fbab78 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sun, 2 Aug 2026 23:40:12 +0700 Subject: [PATCH] refactor: replace tooltip buttons with row actions component in supplier and role columns feat: implement delete confirmation dialog component for better UX refactor: update supplier and role index pages to utilize new form dialog and delete confirm dialog components feat: add reusable form dialog component for creating and editing entities feat: introduce filter popover component for enhanced filtering options in data tables feat: create image preview button component for displaying images with modal preview feat: add page header component for consistent page layout feat: implement row actions component for handling actions on table rows feat: add status badge component for displaying entity statuses feat: create toggle status component for easily toggling entity states feat: implement draft save hook for auto-saving form data feat: add server table hook for managing server-side pagination and filtering feat: create utility functions for formatting dates and numbers feat: add constants for month names and measurement units --- .../js/components/delete-confirm-dialog.tsx | 39 + resources/js/components/filter-popover.tsx | 61 ++ resources/js/components/form-dialog.tsx | 81 ++ .../js/components/image-preview-button.tsx | 46 + resources/js/components/page-header.tsx | 21 + resources/js/components/row-actions.tsx | 67 ++ resources/js/components/status-badge.tsx | 27 + resources/js/components/toggle-status.tsx | 40 + resources/js/hooks/use-draft-save.ts | 76 ++ resources/js/hooks/use-product-draft.ts | 69 +- resources/js/hooks/use-purchase-draft.ts | 68 +- resources/js/hooks/use-raw-material-draft.ts | 63 +- resources/js/hooks/use-restock-draft.ts | 75 +- resources/js/hooks/use-server-table.ts | 118 +++ resources/js/lib/constants.ts | 21 + resources/js/lib/draft-store.ts | 62 ++ resources/js/lib/format.ts | 45 + resources/js/lib/product-draft.ts | 48 +- resources/js/lib/purchase-draft.ts | 45 +- resources/js/lib/raw-material-draft.ts | 48 +- resources/js/lib/restock-draft.ts | 45 +- .../admin/finance/cash-account/columns.tsx | 57 +- .../admin/finance/cash-account/index.tsx | 840 ++++++------------ .../cash-account/transaction-columns.tsx | 109 +-- .../finance/employee-advance/columns.tsx | 142 +-- .../admin/finance/employee-advance/index.tsx | 477 ++++------ .../pages/admin/finance/expense/columns.tsx | 109 +-- .../js/pages/admin/finance/expense/index.tsx | 550 ++++-------- .../admin/finance/payroll-period/columns.tsx | 101 +-- .../admin/finance/payroll-period/index.tsx | 114 +-- .../finance/payroll-period/show-columns.tsx | 92 +- .../admin/finance/payroll-period/show.tsx | 47 +- .../js/pages/admin/hr/employee/columns.tsx | 107 +-- .../js/pages/admin/hr/employee/index.tsx | 328 +++---- .../pages/admin/hr/leave-request/columns.tsx | 124 +-- .../js/pages/admin/hr/leave-request/index.tsx | 605 +++++-------- .../js/pages/admin/manage/purchase/create.tsx | 34 +- .../js/pages/admin/manage/purchase/edit.tsx | 31 +- .../js/pages/admin/manage/purchase/index.tsx | 90 +- .../admin/manage/purchase/purchase-card.tsx | 138 +-- .../manage/purchase/purchase-sub-row.tsx | 76 +- .../js/pages/admin/manage/restock/create.tsx | 35 +- .../js/pages/admin/manage/restock/edit.tsx | 44 +- .../js/pages/admin/manage/restock/index.tsx | 86 +- .../admin/manage/restock/restock-card.tsx | 88 +- .../admin/manage/restock/restock-sub-row.tsx | 51 +- .../pages/admin/master/category/columns.tsx | 57 +- .../js/pages/admin/master/category/index.tsx | 284 ++---- .../pages/admin/master/customer/columns.tsx | 57 +- .../js/pages/admin/master/customer/index.tsx | 378 +++----- .../js/pages/admin/master/product/columns.tsx | 73 +- .../js/pages/admin/master/product/index.tsx | 402 ++++----- .../admin/master/product/product-card.tsx | 85 +- .../product/variant/stock-mutations.tsx | 100 ++- .../admin/master/product/variant/sub-row.tsx | 191 ++-- .../admin/master/raw-material/create.tsx | 393 +++++--- .../pages/admin/master/raw-material/edit.tsx | 396 ++++++--- .../pages/admin/master/raw-material/index.tsx | 265 ++---- .../master/raw-material/raw-material-card.tsx | 108 +-- .../master/raw-material/variant/sub-row.tsx | 129 +-- .../pages/admin/master/supplier/columns.tsx | 57 +- .../js/pages/admin/master/supplier/index.tsx | 378 +++----- resources/js/pages/admin/roles/columns.tsx | 55 +- resources/js/pages/admin/roles/index.tsx | 91 +- 64 files changed, 3649 insertions(+), 5490 deletions(-) create mode 100644 resources/js/components/delete-confirm-dialog.tsx create mode 100644 resources/js/components/filter-popover.tsx create mode 100644 resources/js/components/form-dialog.tsx create mode 100644 resources/js/components/image-preview-button.tsx create mode 100644 resources/js/components/page-header.tsx create mode 100644 resources/js/components/row-actions.tsx create mode 100644 resources/js/components/status-badge.tsx create mode 100644 resources/js/components/toggle-status.tsx create mode 100644 resources/js/hooks/use-draft-save.ts create mode 100644 resources/js/hooks/use-server-table.ts create mode 100644 resources/js/lib/constants.ts create mode 100644 resources/js/lib/draft-store.ts create mode 100644 resources/js/lib/format.ts diff --git a/resources/js/components/delete-confirm-dialog.tsx b/resources/js/components/delete-confirm-dialog.tsx new file mode 100644 index 0000000..1ec8081 --- /dev/null +++ b/resources/js/components/delete-confirm-dialog.tsx @@ -0,0 +1,39 @@ +import { ConfirmDialog } from '@/components/confirm-dialog'; + +type DeleteConfirmDialogProps = { + target: T | null; + onOpenChange: (open: boolean) => void; + title: string; + description?: string | ((target: T) => string); + confirmLabel?: string; + variant?: 'default' | 'destructive'; + onConfirm: () => void; +}; + +export function DeleteConfirmDialog({ + target, + onOpenChange, + title, + description, + confirmLabel = 'Hapus', + variant, + onConfirm, +}: DeleteConfirmDialogProps) { + return ( + + ); +} diff --git a/resources/js/components/filter-popover.tsx b/resources/js/components/filter-popover.tsx new file mode 100644 index 0000000..be49f71 --- /dev/null +++ b/resources/js/components/filter-popover.tsx @@ -0,0 +1,61 @@ +import type { ReactNode } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { Filter, X } from 'lucide-react'; + +type FilterPopoverProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + filters: Record; + hasActiveFilters: boolean; + onClear: () => void; + children: ReactNode; +}; + +export function FilterPopover({ + open, + onOpenChange, + filters, + hasActiveFilters, + onClear, + children, +}: FilterPopoverProps) { + return ( + + + + + +
+
+ Filter + {hasActiveFilters && ( + + )} +
+ {children} +
+
+
+ ); +} diff --git a/resources/js/components/form-dialog.tsx b/resources/js/components/form-dialog.tsx new file mode 100644 index 0000000..e474904 --- /dev/null +++ b/resources/js/components/form-dialog.tsx @@ -0,0 +1,81 @@ +import { Form } from '@inertiajs/react'; +import type { ReactNode } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; + +type FormDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + action: React.ComponentProps['action']; + resetOnSuccess?: boolean; + onSuccess?: () => void; + submitDisabled?: boolean; + submitLabel?: ReactNode; + submittingLabel?: ReactNode; + children: + | ReactNode + | ((ctx: { + errors: Record; + processing: boolean; + }) => ReactNode); +}; + +export function FormDialog({ + open, + onOpenChange, + title, + action, + resetOnSuccess, + onSuccess, + submitDisabled = false, + submitLabel = 'Simpan', + submittingLabel = 'Menyimpan...', + children, +}: FormDialogProps) { + return ( + + +
+ {({ errors, processing }) => ( + <> + + {title} + +
+ {typeof children === 'function' + ? children({ errors, processing }) + : children} +
+ + + + + + )} +
+
+
+ ); +} diff --git a/resources/js/components/image-preview-button.tsx b/resources/js/components/image-preview-button.tsx new file mode 100644 index 0000000..0315f0d --- /dev/null +++ b/resources/js/components/image-preview-button.tsx @@ -0,0 +1,46 @@ +import { useState } from 'react'; +import { ImagePreviewModal } from '@/components/image-preview-modal'; + +type ImagePreviewButtonProps = { + srcs: string[]; + title?: string; + alt?: string; + className?: string; +}; + +export function ImagePreviewButton({ + srcs, + title, + alt, + className = 'h-10 w-10', +}: ImagePreviewButtonProps) { + const [open, setOpen] = useState(false); + + return ( + <> + + + + ); +} diff --git a/resources/js/components/page-header.tsx b/resources/js/components/page-header.tsx new file mode 100644 index 0000000..3e8c425 --- /dev/null +++ b/resources/js/components/page-header.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from 'react'; + +type PageHeaderProps = { + title: string; + description?: ReactNode; + actions?: ReactNode; +}; + +export function PageHeader({ title, description, actions }: PageHeaderProps) { + return ( +
+
+

+ {title} +

+ {description} +
+ {actions} +
+ ); +} diff --git a/resources/js/components/row-actions.tsx b/resources/js/components/row-actions.tsx new file mode 100644 index 0000000..5662135 --- /dev/null +++ b/resources/js/components/row-actions.tsx @@ -0,0 +1,67 @@ +import { Link } from '@inertiajs/react'; +import type { ReactNode } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; + +export type RowAction = { + label: string; + icon: ReactNode; + iconClassName?: string; + show?: boolean; + onClick?: () => void; + href?: string; +}; + +type RowActionsProps = { + actions: RowAction[]; + wrapperClassName?: string; +}; + +export function RowActions({ + actions, + wrapperClassName = 'flex items-center justify-center gap-1', +}: RowActionsProps) { + return ( + +
+ {actions + .filter((action) => action.show !== false) + .map((action) => ( + + + {action.href ? ( + + ) : ( + + )} + + + {action.label} + + + ))} +
+
+ ); +} diff --git a/resources/js/components/status-badge.tsx b/resources/js/components/status-badge.tsx new file mode 100644 index 0000000..a348a2a --- /dev/null +++ b/resources/js/components/status-badge.tsx @@ -0,0 +1,27 @@ +import { Badge } from '@/components/ui/badge'; + +type StatusBadgeConfig = Record; + +type StatusBadgeProps = { + status: string; + config: StatusBadgeConfig; + fallbackKey?: string; +}; + +export function StatusBadge({ + status, + config, + fallbackKey = 'pending', +}: StatusBadgeProps) { + const resolved = config[status] ?? config[fallbackKey]; + + if (!resolved) { + return null; + } + + return ( + + {resolved.label} + + ); +} diff --git a/resources/js/components/toggle-status.tsx b/resources/js/components/toggle-status.tsx new file mode 100644 index 0000000..622154c --- /dev/null +++ b/resources/js/components/toggle-status.tsx @@ -0,0 +1,40 @@ +import { router } from '@inertiajs/react'; +import { Switch } from '@/components/ui/switch'; + +type ToggleStatusProps = { + url: string; + checked: boolean; + onToggle?: () => void; + label?: string; + wrapperClassName?: string; +}; + +export function ToggleStatus({ + url, + checked, + onToggle, + label, + wrapperClassName = 'flex items-center gap-2', +}: ToggleStatusProps) { + function handleToggle() { + onToggle?.(); + router.post(url, {}, { preserveScroll: true }); + } + + return ( +
+ + {label && ( + + {label} + + )} +
+ ); +} diff --git a/resources/js/hooks/use-draft-save.ts b/resources/js/hooks/use-draft-save.ts new file mode 100644 index 0000000..bf782d3 --- /dev/null +++ b/resources/js/hooks/use-draft-save.ts @@ -0,0 +1,76 @@ +import { router } from '@inertiajs/react'; +import { useEffect, useRef } from 'react'; +import type { DraftStore, DraftType } from '@/lib/draft-store'; + +type UseDraftSaveOptions = { + type: DraftType; + data: D; + userId?: number; + extraId?: number; + delay?: number; + store: Pick, 'save' | 'clear'>; +}; + +export function useDraftSave({ + type, + data, + userId, + extraId, + delay = 500, + store, +}: UseDraftSaveOptions) { + const timeoutRef = useRef | null>(null); + const dataRef = useRef(data); + dataRef.current = data; + const submittedRef = useRef(false); + + useEffect(() => { + const offBefore = router.on('before', (event) => { + if (event.detail.visit.method !== 'get') { + submittedRef.current = true; + } + }); + const offError = router.on('error', () => { + submittedRef.current = false; + }); + + return () => { + offBefore(); + offError(); + }; + }, []); + + useEffect(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(() => { + if (!submittedRef.current) { + store.save(type, dataRef.current, userId, extraId); + } + + timeoutRef.current = null; + }, delay); + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [data, type, userId, extraId, delay, store]); + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + if (submittedRef.current) { + store.clear(type, userId, extraId); + } else { + store.save(type, dataRef.current, userId, extraId); + } + }; + }, [type, userId, extraId, store]); +} diff --git a/resources/js/hooks/use-product-draft.ts b/resources/js/hooks/use-product-draft.ts index dde26e2..f8ca12c 100644 --- a/resources/js/hooks/use-product-draft.ts +++ b/resources/js/hooks/use-product-draft.ts @@ -1,10 +1,6 @@ -import { - clearProductDraft, - saveProductDraft, - type ProductDraftData, -} from '@/lib/product-draft'; -import { router } from '@inertiajs/react'; -import { useEffect, useRef } from 'react'; +import { useDraftSave } from '@/hooks/use-draft-save'; +import { clearProductDraft, saveProductDraft } from '@/lib/product-draft'; +import type { ProductDraftData } from '@/lib/product-draft'; type DraftType = 'create' | 'edit'; @@ -15,55 +11,12 @@ export function useProductDraftSave( productId?: number, delay = 500, ) { - const timeoutRef = useRef | null>(null); - const dataRef = useRef(data); - dataRef.current = data; - const submittedRef = useRef(false); - - useEffect(() => { - const offBefore = router.on('before', (event) => { - if (event.detail.visit.method !== 'get') { - submittedRef.current = true; - } - }); - const offError = router.on('error', () => { - submittedRef.current = false; - }); - - return () => { - offBefore(); - offError(); - }; - }, []); - - useEffect(() => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - timeoutRef.current = setTimeout(() => { - if (!submittedRef.current) { - saveProductDraft(type, dataRef.current, userId, productId); - } - timeoutRef.current = null; - }, delay); - - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, [data, type, userId, productId, delay]); - - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - if (submittedRef.current) { - clearProductDraft(type, userId, productId); - } else { - saveProductDraft(type, dataRef.current, userId, productId); - } - }; - }, [type, userId, productId]); + return useDraftSave({ + type, + data, + userId, + extraId: productId, + delay, + store: { save: saveProductDraft, clear: clearProductDraft }, + }); } diff --git a/resources/js/hooks/use-purchase-draft.ts b/resources/js/hooks/use-purchase-draft.ts index 46263e5..aba1306 100644 --- a/resources/js/hooks/use-purchase-draft.ts +++ b/resources/js/hooks/use-purchase-draft.ts @@ -1,10 +1,6 @@ -import { - clearPurchaseDraft, - savePurchaseDraft, - type PurchaseDraftData, -} from '@/lib/purchase-draft'; -import { router } from '@inertiajs/react'; -import { useEffect, useRef } from 'react'; +import { useDraftSave } from '@/hooks/use-draft-save'; +import { clearPurchaseDraft, savePurchaseDraft } from '@/lib/purchase-draft'; +import type { PurchaseDraftData } from '@/lib/purchase-draft'; type DraftType = 'create' | 'edit'; @@ -14,55 +10,11 @@ export function usePurchaseDraftSave( userId?: number, delay = 500, ) { - const timeoutRef = useRef | null>(null); - const dataRef = useRef(data); - dataRef.current = data; - const submittedRef = useRef(false); - - useEffect(() => { - const offBefore = router.on('before', (event) => { - if (event.detail.visit.method !== 'get') { - submittedRef.current = true; - } - }); - const offError = router.on('error', () => { - submittedRef.current = false; - }); - - return () => { - offBefore(); - offError(); - }; - }, []); - - useEffect(() => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - timeoutRef.current = setTimeout(() => { - if (!submittedRef.current) { - savePurchaseDraft(type, dataRef.current, userId); - } - timeoutRef.current = null; - }, delay); - - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, [data, type, userId, delay]); - - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - if (submittedRef.current) { - clearPurchaseDraft(type, userId); - } else { - savePurchaseDraft(type, dataRef.current, userId); - } - }; - }, [type, userId]); + return useDraftSave({ + type, + data, + userId, + delay, + store: { save: savePurchaseDraft, clear: clearPurchaseDraft }, + }); } diff --git a/resources/js/hooks/use-raw-material-draft.ts b/resources/js/hooks/use-raw-material-draft.ts index d74cc06..50874f1 100644 --- a/resources/js/hooks/use-raw-material-draft.ts +++ b/resources/js/hooks/use-raw-material-draft.ts @@ -1,10 +1,9 @@ +import { useDraftSave } from '@/hooks/use-draft-save'; import { clearRawMaterialDraft, saveRawMaterialDraft, - type RawMaterialDraftData, } from '@/lib/raw-material-draft'; -import { router } from '@inertiajs/react'; -import { useEffect, useRef } from 'react'; +import type { RawMaterialDraftData } from '@/lib/raw-material-draft'; type DraftType = 'create' | 'edit'; @@ -15,54 +14,12 @@ export function useRawMaterialDraftSave( rawMaterialId?: number, delay = 500, ) { - const timeoutRef = useRef | null>(null); - const dataRef = useRef(data); - dataRef.current = data; - const submittedRef = useRef(false); - - useEffect(() => { - const offBefore = router.on('before', (event) => { - if (event.detail.visit.method !== 'get') { - submittedRef.current = true; - } - }); - const offError = router.on('error', () => { - submittedRef.current = false; - }); - - return () => { - offBefore(); - offError(); - }; - }, []); - useEffect(() => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - timeoutRef.current = setTimeout(() => { - if (!submittedRef.current) { - saveRawMaterialDraft(type, dataRef.current, userId, rawMaterialId); - } - timeoutRef.current = null; - }, delay); - - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, [data, type, userId, rawMaterialId, delay]); - - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - if (submittedRef.current) { - clearRawMaterialDraft(type, userId, rawMaterialId); - } else { - saveRawMaterialDraft(type, dataRef.current, userId, rawMaterialId); - } - }; - }, [type, userId, rawMaterialId]); + return useDraftSave({ + type, + data, + userId, + extraId: rawMaterialId, + delay, + store: { save: saveRawMaterialDraft, clear: clearRawMaterialDraft }, + }); } diff --git a/resources/js/hooks/use-restock-draft.ts b/resources/js/hooks/use-restock-draft.ts index aa4196f..94e4c6c 100644 --- a/resources/js/hooks/use-restock-draft.ts +++ b/resources/js/hooks/use-restock-draft.ts @@ -1,11 +1,6 @@ -import { router } from '@inertiajs/react'; -import { useEffect, useRef } from 'react'; -import { - clearRestockDraft, - saveRestockDraft - -} from '@/lib/restock-draft'; -import type {RestockDraftData} from '@/lib/restock-draft'; +import { useDraftSave } from '@/hooks/use-draft-save'; +import { clearRestockDraft, saveRestockDraft } from '@/lib/restock-draft'; +import type { RestockDraftData } from '@/lib/restock-draft'; type DraftType = 'create' | 'edit'; @@ -15,61 +10,11 @@ export function useRestockDraftSave( userId?: number, delay = 500, ) { - const timeoutRef = useRef | null>(null); - const dataRef = useRef(data); - const submittedRef = useRef(false); - - useEffect(() => { - dataRef.current = data; - }, [data]); - - useEffect(() => { - const offBefore = router.on('before', (event) => { - if (event.detail.visit.method !== 'get') { - submittedRef.current = true; - } - }); - const offError = router.on('error', () => { - submittedRef.current = false; - }); - - return () => { - offBefore(); - offError(); - }; - }, []); - - useEffect(() => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - - timeoutRef.current = setTimeout(() => { - if (!submittedRef.current) { - saveRestockDraft(type, dataRef.current, userId); - } - - timeoutRef.current = null; - }, delay); - - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, [data, type, userId, delay]); - - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - - if (submittedRef.current) { - clearRestockDraft(type, userId); - } else { - saveRestockDraft(type, dataRef.current, userId); - } - }; - }, [type, userId]); + return useDraftSave({ + type, + data, + userId, + delay, + store: { save: saveRestockDraft, clear: clearRestockDraft }, + }); } diff --git a/resources/js/hooks/use-server-table.ts b/resources/js/hooks/use-server-table.ts new file mode 100644 index 0000000..ed4521b --- /dev/null +++ b/resources/js/hooks/use-server-table.ts @@ -0,0 +1,118 @@ +import { router } from '@inertiajs/react'; +import { useCallback, useState } from 'react'; +import type { PaginationState } from '@/components/data-table'; + +type UseServerTableOptions = { + route: () => string; + pagination: PaginationState; + filters?: Record; + filterWithParams?: boolean; +}; + +export function useServerTable({ + route, + pagination, + filters, + filterWithParams = true, +}: UseServerTableOptions) { + const [search, setSearch] = useState(''); + const [filterOpen, setFilterOpen] = useState(false); + + const handlePageChange = useCallback( + (page: number) => { + router.get( + route(), + { + ...filters, + page, + per_page: pagination.per_page, + search, + }, + { preserveState: true, replace: true }, + ); + }, + [route, filters, pagination.per_page, search], + ); + + const handlePerPageChange = useCallback( + (perPage: number) => { + router.get( + route(), + { + ...filters, + page: 1, + per_page: perPage, + search, + }, + { preserveState: true, replace: true }, + ); + }, + [route, filters, search], + ); + + const handleSearchChange = useCallback( + (value: string) => { + setSearch(value); + router.get( + route(), + { + ...filters, + page: 1, + per_page: pagination.per_page, + search: value, + }, + { preserveState: true, replace: true }, + ); + }, + [route, filters, pagination.per_page], + ); + + function applyFilter(key: string, value: string) { + const newFilters = { ...filters }; + + if (value === '' || value === 'all') { + delete newFilters[key]; + } else { + newFilters[key] = value; + } + + router.get( + route(), + filterWithParams + ? { + ...newFilters, + page: 1, + per_page: pagination.per_page, + search, + } + : newFilters, + { preserveState: true, replace: true }, + ); + } + + function clearFilters() { + router.get( + route(), + filterWithParams + ? { + page: 1, + per_page: pagination.per_page, + search, + } + : {}, + { preserveState: true, replace: true }, + ); + setFilterOpen(false); + } + + return { + search, + filterOpen, + setFilterOpen, + handlePageChange, + handlePerPageChange, + handleSearchChange, + applyFilter, + clearFilters, + }; +} diff --git a/resources/js/lib/constants.ts b/resources/js/lib/constants.ts new file mode 100644 index 0000000..d4580ac --- /dev/null +++ b/resources/js/lib/constants.ts @@ -0,0 +1,21 @@ +export const MONTH_NAMES = [ + '', + 'Januari', + 'Februari', + 'Maret', + 'April', + 'Mei', + 'Juni', + 'Juli', + 'Agustus', + 'September', + 'Oktober', + 'November', + 'Desember', +]; + +export const UNITS = [ + { value: 'kg', label: 'Kilogram' }, + { value: 'meter', label: 'Meter' }, + { value: 'yard', label: 'Yard' }, +]; diff --git a/resources/js/lib/draft-store.ts b/resources/js/lib/draft-store.ts new file mode 100644 index 0000000..8435728 --- /dev/null +++ b/resources/js/lib/draft-store.ts @@ -0,0 +1,62 @@ +export type DraftType = 'create' | 'edit'; + +export type DraftStore = { + save(type: DraftType, data: D, userId?: number, extraId?: number): boolean; + load(type: DraftType, userId?: number, extraId?: number): D | null; + clear(type: DraftType, userId?: number, extraId?: number): void; +}; + +export function createDraftStore(prefix: string): DraftStore { + function getKey( + type: DraftType, + userId?: number, + extraId?: number, + ): string { + if (type === 'edit' && extraId) { + return `${prefix}-edit-${userId ?? 'anon'}-${extraId}`; + } + + return `${prefix}-create-${userId ?? 'anon'}`; + } + + return { + save(type, data, userId, extraId) { + if (type !== 'create') { + return false; + } + + try { + const key = getKey(type, userId, extraId); + localStorage.setItem(key, JSON.stringify(data)); + + return true; + } catch { + return false; + } + }, + + load(type, userId, extraId) { + try { + const key = getKey(type, userId, extraId); + const raw = localStorage.getItem(key); + + if (!raw) { + return null; + } + + return JSON.parse(raw) as D; + } catch { + return null; + } + }, + + clear(type, userId, extraId) { + try { + const key = getKey(type, userId, extraId); + localStorage.removeItem(key); + } catch { + // ignore + } + }, + }; +} diff --git a/resources/js/lib/format.ts b/resources/js/lib/format.ts new file mode 100644 index 0000000..d244671 --- /dev/null +++ b/resources/js/lib/format.ts @@ -0,0 +1,45 @@ +export function formatNumber( + num: number, + options?: Intl.NumberFormatOptions, +): string { + return new Intl.NumberFormat('id-ID', options).format(num); +} + +export function formatDate(dateString: string): string { + const date = new Date(dateString); + + return ( + date.toLocaleDateString('id-ID', { + day: '2-digit', + month: 'short', + year: 'numeric', + }) + + ' ' + + date.toLocaleTimeString('id-ID', { + hour: '2-digit', + minute: '2-digit', + }) + ); +} + +export function formatShortDate(dateString: string): string { + const date = new Date(dateString); + + return date.toLocaleDateString('id-ID', { + day: '2-digit', + month: 'short', + year: 'numeric', + }); +} + +export function formatDateTime(dateString: string): string { + const date = new Date(dateString); + + return date.toLocaleDateString('id-ID', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} diff --git a/resources/js/lib/product-draft.ts b/resources/js/lib/product-draft.ts index e32f590..7e800c6 100644 --- a/resources/js/lib/product-draft.ts +++ b/resources/js/lib/product-draft.ts @@ -1,4 +1,4 @@ -const DRAFT_PREFIX = 'product-draft'; +import { createDraftStore } from '@/lib/draft-store'; export type ProductDraftData = { productName: string; @@ -19,17 +19,8 @@ export type ProductDraftData = { }>; }; -function getKey( - type: 'create' | 'edit', - userId?: number, - productId?: number, -): string { - if (type === 'edit' && productId) { - return `${DRAFT_PREFIX}-edit-${userId ?? 'anon'}-${productId}`; - } - - return `${DRAFT_PREFIX}-create-${userId ?? 'anon'}`; -} +export const productDraftStore = + createDraftStore('product-draft'); export function saveProductDraft( type: 'create' | 'edit', @@ -37,18 +28,7 @@ export function saveProductDraft( userId?: number, productId?: number, ): boolean { - if (type !== 'create') { - return false; - } - - try { - const key = getKey(type, userId, productId); - localStorage.setItem(key, JSON.stringify(data)); - - return true; - } catch { - return false; - } + return productDraftStore.save(type, data, userId, productId); } export function loadProductDraft( @@ -56,18 +36,7 @@ export function loadProductDraft( userId?: number, productId?: number, ): ProductDraftData | null { - try { - const key = getKey(type, userId, productId); - const raw = localStorage.getItem(key); - - if (!raw) { -return null; -} - - return JSON.parse(raw) as ProductDraftData; - } catch { - return null; - } + return productDraftStore.load(type, userId, productId); } export function clearProductDraft( @@ -75,10 +44,5 @@ export function clearProductDraft( userId?: number, productId?: number, ): void { - try { - const key = getKey(type, userId, productId); - localStorage.removeItem(key); - } catch { - // ignore - } + productDraftStore.clear(type, userId, productId); } diff --git a/resources/js/lib/purchase-draft.ts b/resources/js/lib/purchase-draft.ts index e739172..69ef56c 100644 --- a/resources/js/lib/purchase-draft.ts +++ b/resources/js/lib/purchase-draft.ts @@ -1,4 +1,4 @@ -const DRAFT_PREFIX = 'purchase-draft'; +import { createDraftStore } from '@/lib/draft-store'; export type PurchaseDraftData = { name: string; @@ -19,60 +19,27 @@ export type PurchaseDraftData = { photo?: string; }; -function getKey(type: 'create' | 'edit', userId?: number): string { - if (type === 'edit') { - return `${DRAFT_PREFIX}-edit-${userId ?? 'anon'}`; - } - - return `${DRAFT_PREFIX}-create-${userId ?? 'anon'}`; -} +export const purchaseDraftStore = + createDraftStore('purchase-draft'); export function savePurchaseDraft( type: 'create' | 'edit', data: PurchaseDraftData, userId?: number, ): boolean { - if (type !== 'create') { - return false; - } - - try { - const key = getKey(type, userId); - localStorage.setItem(key, JSON.stringify(data)); - - return true; - } catch { - return false; - } + return purchaseDraftStore.save(type, data, userId); } export function loadPurchaseDraft( type: 'create' | 'edit', userId?: number, ): PurchaseDraftData | null { - try { - const key = getKey(type, userId); - const raw = localStorage.getItem(key); - - if (!raw) { - return null; - } - - return JSON.parse(raw) as PurchaseDraftData; - - } catch { - return null; - } + return purchaseDraftStore.load(type, userId); } export function clearPurchaseDraft( type: 'create' | 'edit', userId?: number, ): void { - try { - const key = getKey(type, userId); - localStorage.removeItem(key); - } catch { - // ignore - } + purchaseDraftStore.clear(type, userId); } diff --git a/resources/js/lib/raw-material-draft.ts b/resources/js/lib/raw-material-draft.ts index adbdf7e..99fe469 100644 --- a/resources/js/lib/raw-material-draft.ts +++ b/resources/js/lib/raw-material-draft.ts @@ -1,4 +1,4 @@ -const DRAFT_PREFIX = 'raw-material-draft'; +import { createDraftStore } from '@/lib/draft-store'; export type RawMaterialDraftData = { name: string; @@ -13,17 +13,8 @@ export type RawMaterialDraftData = { }>; }; -function getKey( - type: 'create' | 'edit', - userId?: number, - rawMaterialId?: number, -): string { - if (type === 'edit' && rawMaterialId) { - return `${DRAFT_PREFIX}-edit-${userId ?? 'anon'}-${rawMaterialId}`; - } - - return `${DRAFT_PREFIX}-create-${userId ?? 'anon'}`; -} +export const rawMaterialDraftStore = + createDraftStore('raw-material-draft'); export function saveRawMaterialDraft( type: 'create' | 'edit', @@ -31,18 +22,7 @@ export function saveRawMaterialDraft( userId?: number, rawMaterialId?: number, ): boolean { - if (type !== 'create') { - return false; - } - - try { - const key = getKey(type, userId, rawMaterialId); - localStorage.setItem(key, JSON.stringify(data)); - - return true; - } catch { - return false; - } + return rawMaterialDraftStore.save(type, data, userId, rawMaterialId); } export function loadRawMaterialDraft( @@ -50,18 +30,7 @@ export function loadRawMaterialDraft( userId?: number, rawMaterialId?: number, ): RawMaterialDraftData | null { - try { - const key = getKey(type, userId, rawMaterialId); - const raw = localStorage.getItem(key); - - if (!raw) { - return null; - } - - return JSON.parse(raw) as RawMaterialDraftData; - } catch { - return null; - } + return rawMaterialDraftStore.load(type, userId, rawMaterialId); } export function clearRawMaterialDraft( @@ -69,10 +38,5 @@ export function clearRawMaterialDraft( userId?: number, rawMaterialId?: number, ): void { - try { - const key = getKey(type, userId, rawMaterialId); - localStorage.removeItem(key); - } catch { - // ignore - } + rawMaterialDraftStore.clear(type, userId, rawMaterialId); } diff --git a/resources/js/lib/restock-draft.ts b/resources/js/lib/restock-draft.ts index 10a7ad3..47d605c 100644 --- a/resources/js/lib/restock-draft.ts +++ b/resources/js/lib/restock-draft.ts @@ -1,4 +1,4 @@ -const DRAFT_PREFIX = 'restock-draft'; +import { createDraftStore } from '@/lib/draft-store'; export type RestockDraftData = { stockType: 'good' | 'reject'; @@ -8,60 +8,27 @@ export type RestockDraftData = { photo?: string; }; -function getKey(type: 'create' | 'edit', userId?: number): string { - if (type === 'edit') { - return `${DRAFT_PREFIX}-edit-${userId ?? 'anon'}`; - } - - return `${DRAFT_PREFIX}-create-${userId ?? 'anon'}`; -} +export const restockDraftStore = + createDraftStore('restock-draft'); export function saveRestockDraft( type: 'create' | 'edit', data: RestockDraftData, userId?: number, ): boolean { - if (type !== 'create') { - return false; - } - - try { - const key = getKey(type, userId); - localStorage.setItem(key, JSON.stringify(data)); - - return true; - } catch { - return false; - } + return restockDraftStore.save(type, data, userId); } export function loadRestockDraft( type: 'create' | 'edit', userId?: number, ): RestockDraftData | null { - try { - const key = getKey(type, userId); - const raw = localStorage.getItem(key); - - if (!raw) { - return null; - } - - return JSON.parse(raw) as RestockDraftData; - - } catch { - return null; - } + return restockDraftStore.load(type, userId); } export function clearRestockDraft( type: 'create' | 'edit', userId?: number, ): void { - try { - const key = getKey(type, userId); - localStorage.removeItem(key); - } catch { - // ignore - } + restockDraftStore.clear(type, userId); } diff --git a/resources/js/pages/admin/finance/cash-account/columns.tsx b/resources/js/pages/admin/finance/cash-account/columns.tsx index 2d04802..d53d881 100644 --- a/resources/js/pages/admin/finance/cash-account/columns.tsx +++ b/resources/js/pages/admin/finance/cash-account/columns.tsx @@ -1,12 +1,6 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { RowActions } from '@/components/row-actions'; import { formatCurrency } from '@/lib/utils'; export type CashAccount = { @@ -55,39 +49,22 @@ export function createCashAccountColumns( const cashAccount = row.original; return ( - -
- - - - - Edit - - - - - - - - Hapus - - -
-
+ , + onClick: () => handleEdit(cashAccount), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(cashAccount), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/finance/cash-account/index.tsx b/resources/js/pages/admin/finance/cash-account/index.tsx index 489ccf0..e8c4ca2 100644 --- a/resources/js/pages/admin/finance/cash-account/index.tsx +++ b/resources/js/pages/admin/finance/cash-account/index.tsx @@ -1,34 +1,19 @@ -import { Form, Head, router } from '@inertiajs/react'; -import { - ArrowDownToLine, - ArrowUpFromLine, - Filter, - Wallet, - X, -} from 'lucide-react'; -import { useCallback, useState } from 'react'; -import { ConfirmDialog } from '@/components/confirm-dialog'; -import { DataTable } from '@/components/data-table'; +import { Head, router } from '@inertiajs/react'; +import { ArrowDownToLine, ArrowUpFromLine, Wallet } from 'lucide-react'; +import { useState } from 'react'; import type { PaginationState } from '@/components/data-table'; +import { DataTable } from '@/components/data-table'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; import { FileUpload } from '@/components/file-upload'; +import { FilterPopover } from '@/components/filter-popover'; +import { FormDialog } from '@/components/form-dialog'; import InputError from '@/components/input-error'; +import { PageHeader } from '@/components/page-header'; import { RupiahInput } from '@/components/rupiah-input'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; import { Select, SelectContent, @@ -36,6 +21,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { useServerTable } from '@/hooks/use-server-table'; import { formatCurrency } from '@/lib/utils'; import { index as cashAccountIndex, @@ -78,7 +64,6 @@ export default function CashAccountIndex({ const [withdrawalOpen, setWithdrawalOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); - const [filterOpen, setFilterOpen] = useState(false); const [depositReceiptKey, setDepositReceiptKey] = useState( null, @@ -105,7 +90,6 @@ export default function CashAccountIndex({ type: string; } | null>(null); - const [search, setSearch] = useState(''); const pagination: PaginationState = { current_page: transactions.current_page, last_page: transactions.last_page, @@ -113,41 +97,20 @@ export default function CashAccountIndex({ total: transactions.total, }; - const hasActiveFilters = filters.type; - - function applyFilter(key: string, value: string) { - const newFilters = { ...filters }; - - if (value === '' || value === 'all') { - delete newFilters[key as keyof typeof newFilters]; - } else { - newFilters[key as keyof typeof newFilters] = value; - } - - router.get( - cashAccountIndex(), - { - ...newFilters, - page: 1, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function clearFilters() { - router.get( - cashAccountIndex(), - { - page: 1, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - setFilterOpen(false); - } + const { + search, + filterOpen, + setFilterOpen, + handlePageChange, + handlePerPageChange, + handleSearchChange, + applyFilter, + clearFilters, + } = useServerTable({ + route: () => cashAccountIndex.url(), + pagination, + filters, + }); function handleDelete() { if (!deleting) { @@ -159,49 +122,6 @@ export default function CashAccountIndex({ }); } - function handlePageChange(page: number) { - router.get( - cashAccountIndex(), - { - ...filters, - page, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - cashAccountIndex(), - { - ...filters, - page: 1, - per_page: perPage, - search, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - cashAccountIndex(), - { - ...filters, - page: 1, - per_page: pagination.per_page, - search: value, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page, filters], - ); - const columns = createTransactionColumns({ handleEdit: (transaction) => { setEditing(transaction); @@ -211,66 +131,34 @@ export default function CashAccountIndex({ }); const filterToolbar = ( - - - - - -
-
- Filter - {hasActiveFilters && ( - - )} -
- -
- - -
-
-
-
+ +
+ + +
+
); return ( @@ -278,29 +166,27 @@ export default function CashAccountIndex({
-
-
-

- Kas Toko -

-
-
- - -
-
+ + + +
+ } + /> @@ -330,7 +216,7 @@ export default function CashAccountIndex({ toolbar={filterToolbar} /> - { setDepositOpen(open); @@ -340,123 +226,72 @@ export default function CashAccountIndex({ setDepositFileMeta(null); } }} + title="Deposit" + action={deposit()} + resetOnSuccess + onSuccess={() => { + setDepositOpen(false); + setDepositReceiptKey(null); + setDepositFileMeta(null); + }} + submitDisabled={depositUploading} + submitLabel={depositUploading ? 'Mengunggah...' : 'Simpan'} > - -
{ - setDepositOpen(false); - setDepositReceiptKey(null); - setDepositFileMeta(null); - }} - > - {({ errors, processing }) => ( - <> - - Deposit - -
-
- - - -
-
- - - -
-
- - - - - - -
-
- - - - - - )} -
-
-
+ {({ errors }) => ( + <> +
+ + + +
+
+ + + +
+
+ + + + + + +
+ + )} + - { setWithdrawalOpen(open); @@ -466,132 +301,74 @@ export default function CashAccountIndex({ setWithdrawalFileMeta(null); } }} + title="Withdrawal" + action={withdrawal()} + resetOnSuccess + onSuccess={() => { + setWithdrawalOpen(false); + setWithdrawalReceiptKey(null); + setWithdrawalFileMeta(null); + }} + submitDisabled={withdrawalUploading} + submitLabel={ + withdrawalUploading ? 'Mengunggah...' : 'Simpan' + } > - -
{ - setWithdrawalOpen(false); - setWithdrawalReceiptKey(null); - setWithdrawalFileMeta(null); - }} - > - {({ errors, processing }) => ( - <> - - Withdrawal - -
-
- - - -
-
- - - -
-
- - - - - - -
-
- - - - - - )} -
-
-
+ {({ errors }) => ( + <> +
+ + + +
+
+ + + +
+
+ + + + + + +
+ + )} + - { if (!open) { @@ -600,143 +377,96 @@ export default function CashAccountIndex({ setEditFileMeta(null); } }} + title="Edit Transaksi" + action={editing ? updateTransaction(editing.id) : ''} + resetOnSuccess + onSuccess={() => { + setEditing(null); + setEditReceiptKey(null); + setEditFileMeta(null); + }} + submitDisabled={editUploading} + submitLabel={editUploading ? 'Mengunggah...' : 'Simpan'} > - - {editing && ( -
{ - setEditing(null); - setEditReceiptKey(null); - setEditFileMeta(null); - }} - > - {({ errors, processing }) => ( - <> - - - Edit Transaksi - - -
-
- - - -
-
- - - -
-
- - - - - - -
-
- - - - - - )} -
- )} -
-
+ {({ errors }) => + editing && ( + <> +
+ + + +
+
+ + + +
+
+ + + + + + +
+ + ) + } + - { if (!open) { setDeleting(null); } }} title="Hapus Transaksi" - description={`Apakah Anda yakin ingin menghapus transaksi "${deleting?.description}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(transaction) => + `Apakah Anda yakin ingin menghapus transaksi "${transaction.description}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDelete} /> diff --git a/resources/js/pages/admin/finance/cash-account/transaction-columns.tsx b/resources/js/pages/admin/finance/cash-account/transaction-columns.tsx index af5db19..6c622ba 100644 --- a/resources/js/pages/admin/finance/cash-account/transaction-columns.tsx +++ b/resources/js/pages/admin/finance/cash-account/transaction-columns.tsx @@ -1,15 +1,9 @@ -import { ImagePreviewModal } from '@/components/image-preview-modal'; -import { Button } from '@/components/ui/button'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { formatCurrency } from '@/lib/utils'; import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; -import { useState } from 'react'; +import { ImagePreviewButton } from '@/components/image-preview-button'; +import { RowActions } from '@/components/row-actions'; +import { formatDate } from '@/lib/format'; +import { formatCurrency } from '@/lib/utils'; export type CashTransaction = { id: number; @@ -30,23 +24,6 @@ export type CashTransaction = { } | null; }; -function formatDate(dateString: string): string { - const date = new Date(dateString); - - return ( - date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - }) + - ' ' + - date.toLocaleTimeString('id-ID', { - hour: '2-digit', - minute: '2-digit', - }) - ); -} - function getTypeLabel(type: string): string { const labels: Record = { deposit: 'Deposit', @@ -69,31 +46,6 @@ function getReferenceLabel(type: string): string { return labels[type] ?? '-'; } -function ReceiptPreview({ url, title }: { url: string; title: string }) { - const [open, setOpen] = useState(false); - - return ( - <> - - - - ); -} - type CreateColumnsParams = { handleEdit: (transaction: CashTransaction) => void; handleDeleteClick: (transaction: CashTransaction) => void; @@ -182,8 +134,8 @@ export function createTransactionColumns( } return ( - ); @@ -216,39 +168,22 @@ export function createTransactionColumns( } return ( - -
- - - - - Edit - - - - - - - - Hapus - - -
-
+ , + onClick: () => handleEdit(transaction), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(transaction), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/finance/employee-advance/columns.tsx b/resources/js/pages/admin/finance/employee-advance/columns.tsx index 557cc04..c4ccc57 100644 --- a/resources/js/pages/admin/finance/employee-advance/columns.tsx +++ b/resources/js/pages/admin/finance/employee-advance/columns.tsx @@ -1,13 +1,8 @@ import type { ColumnDef } from '@tanstack/react-table'; import { CheckCircle, CircleDollarSign, Pencil, Trash2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { formatDate, formatShortDate } from '@/lib/format'; import { formatCurrency } from '@/lib/utils'; export type EmployeeAdvance = { @@ -27,33 +22,6 @@ export type EmployeeAdvance = { }; }; -function formatDate(dateString: string): string { - const date = new Date(dateString); - - return ( - date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - }) + - ' ' + - date.toLocaleTimeString('id-ID', { - hour: '2-digit', - minute: '2-digit', - }) - ); -} - -function formatShortDate(dateString: string): string { - const date = new Date(dateString); - - return date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - }); -} - function getStatusBadge(status: string) { const statusConfig: Record = { pending: { @@ -165,79 +133,39 @@ export function createEmployeeAdvanceColumns( const employeeAdvance = row.original; return ( - -
- {employeeAdvance.status === 'pending' && ( - - - - - - Setujui - - - )} - - {employeeAdvance.status === 'approved' && ( - - - - - - Bayar - - - )} - - - - - - Edit - - - - - - - - Hapus - - -
-
+ + ), + show: employeeAdvance.status === 'pending', + onClick: () => handleApprove(employeeAdvance), + }, + { + label: 'Bayar', + icon: ( + + ), + show: employeeAdvance.status === 'approved', + onClick: () => handlePay(employeeAdvance), + }, + { + label: 'Edit', + icon: , + onClick: () => handleEdit(employeeAdvance), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => + handleDeleteClick(employeeAdvance), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/finance/employee-advance/index.tsx b/resources/js/pages/admin/finance/employee-advance/index.tsx index ec860dd..32d8bed 100644 --- a/resources/js/pages/admin/finance/employee-advance/index.tsx +++ b/resources/js/pages/admin/finance/employee-advance/index.tsx @@ -1,22 +1,18 @@ -import { Form, Head, router } from '@inertiajs/react'; +import { Head, router } from '@inertiajs/react'; import { Plus } from 'lucide-react'; -import { useCallback, useEffect, useState } from 'react'; -import { ConfirmDialog } from '@/components/confirm-dialog'; -import { DataTable } from '@/components/data-table'; +import { useEffect, useState } from 'react'; import type { PaginationState } from '@/components/data-table'; +import { DataTable } from '@/components/data-table'; import { DatePicker } from '@/components/date-picker'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FormDialog } from '@/components/form-dialog'; import InputError from '@/components/input-error'; +import { PageHeader } from '@/components/page-header'; import { RupiahInput } from '@/components/rupiah-input'; import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { useServerTable } from '@/hooks/use-server-table'; import { destroy, index as employeeAdvanceIndex, @@ -48,7 +44,7 @@ export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) { const [editingDueDate, setEditingDueDate] = useState( undefined, ); - const [search, setSearch] = useState(''); + const pagination: PaginationState = { current_page: employeeAdvances.current_page, last_page: employeeAdvances.last_page, @@ -56,6 +52,16 @@ export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) { total: employeeAdvances.total, }; + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => employeeAdvanceIndex.url(), + pagination, + }); + useEffect(() => { if (editing) { setEditingDueDate(new Date(editing.due_date)); @@ -102,46 +108,6 @@ export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) { ); } - function handlePageChange(page: number) { - router.get( - employeeAdvanceIndex.url(), - { - page, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - employeeAdvanceIndex.url(), - { - page: 1, - per_page: perPage, - search, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - employeeAdvanceIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search: value, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page], - ); - const columns = createEmployeeAdvanceColumns({ handleEdit: (employeeAdvance) => setEditing(employeeAdvance), handleDeleteClick: (employeeAdvance) => setDeleting(employeeAdvance), @@ -154,22 +120,9 @@ export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
-
-
-

- Kasbon -

-
- { - setCreateOpen(open); - - if (!open) { - setDueDate(undefined); - } - }} - > + - -
setCreateOpen(false)} - > - {({ errors, processing }) => { - return ( - <> - - - Tambah Kasbon - - -
-
- - - -
-
- - - -
-
- - - - -
-
- - - - - - ); - }} -
-
-
-
+ } + /> + + { + setCreateOpen(open); + + if (!open) { + setDueDate(undefined); + } + }} + title="Tambah Kasbon" + action={store()} + resetOnSuccess + onSuccess={() => setCreateOpen(false)} + > + {({ errors }) => ( + <> +
+ + + +
+
+ + + +
+
+ + + + +
+ + )} +
- { if (!open) { @@ -308,162 +220,117 @@ export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) { setEditingDueDate(undefined); } }} + title="Edit Kasbon" + action={editing ? update(editing.id) : ''} + resetOnSuccess + onSuccess={() => { + setEditing(null); + setEditingDueDate(undefined); + }} > - - {editing && ( -
{ - setEditing(null); - setEditingDueDate(undefined); - }} - > - {({ errors, processing }) => { - return ( - <> - - - Edit Kasbon - - -
-
- - - -
-
- - - -
-
- - - - -
-
- - - - - - ); - }} -
- )} -
-
+ {({ errors }) => + editing && ( + <> +
+ + + +
+
+ + + +
+
+ + + + +
+ + ) + } + - { if (!open) { setDeleting(null); } }} title="Hapus Kasbon" - description={`Apakah Anda yakin ingin menghapus kasbon "${deleting?.description}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(advance) => + `Apakah Anda yakin ingin menghapus kasbon "${advance.description}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDelete} /> - { if (!open) { setApproving(null); } }} title="Setujui Kasbon" - description={`Apakah Anda yakin ingin menyetujui kasbon "${approving?.description}"?`} + description={(advance) => + `Apakah Anda yakin ingin menyetujui kasbon "${advance.description}"?` + } confirmLabel="Setujui" onConfirm={handleApprove} /> - { if (!open) { setPaying(null); } }} title="Bayar Kasbon" - description={`Apakah Anda yakin ingin membayar kasbon "${paying?.description}" sebesar ${paying?.amount}? Saldo kas akan dikembalikan.`} + description={(advance) => + `Apakah Anda yakin ingin membayar kasbon "${advance.description}" sebesar ${advance.amount}? Saldo kas akan dikembalikan.` + } confirmLabel="Bayar" onConfirm={handlePay} /> diff --git a/resources/js/pages/admin/finance/expense/columns.tsx b/resources/js/pages/admin/finance/expense/columns.tsx index bf786e4..cfead1d 100644 --- a/resources/js/pages/admin/finance/expense/columns.tsx +++ b/resources/js/pages/admin/finance/expense/columns.tsx @@ -1,15 +1,9 @@ -import { ImagePreviewModal } from '@/components/image-preview-modal'; -import { Button } from '@/components/ui/button'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { formatCurrency } from '@/lib/utils'; import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; -import { useState } from 'react'; +import { ImagePreviewButton } from '@/components/image-preview-button'; +import { RowActions } from '@/components/row-actions'; +import { formatDate } from '@/lib/format'; +import { formatCurrency } from '@/lib/utils'; export type Expense = { id: number; @@ -25,53 +19,11 @@ export type Expense = { }; }; -function formatDate(dateString: string): string { - const date = new Date(dateString); - - return ( - date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - }) + - ' ' + - date.toLocaleTimeString('id-ID', { - hour: '2-digit', - minute: '2-digit', - }) - ); -} - type CreateColumnsParams = { handleEdit: (expense: Expense) => void; handleDeleteClick: (expense: Expense) => void; }; -function ReceiptPreview({ url, title }: { url: string; title: string }) { - const [open, setOpen] = useState(false); - - return ( - <> - - - - ); -} - export function createExpenseColumns( params: CreateColumnsParams, ): ColumnDef[] { @@ -105,8 +57,8 @@ export function createExpenseColumns( } return ( - ); @@ -141,39 +93,22 @@ export function createExpenseColumns( const expense = row.original; return ( - -
- - - - - Edit - - - - - - - - Hapus - - -
-
+ , + onClick: () => handleEdit(expense), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(expense), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/finance/expense/index.tsx b/resources/js/pages/admin/finance/expense/index.tsx index 037b25b..844830b 100644 --- a/resources/js/pages/admin/finance/expense/index.tsx +++ b/resources/js/pages/admin/finance/expense/index.tsx @@ -1,22 +1,18 @@ -import { Form, Head, router } from '@inertiajs/react'; +import { Head, router } from '@inertiajs/react'; import { Plus } from 'lucide-react'; -import { useCallback, useState } from 'react'; -import { ConfirmDialog } from '@/components/confirm-dialog'; +import { useState } from 'react'; import type { PaginationState } from '@/components/data-table'; import { DataTable } from '@/components/data-table'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; import { FileUpload } from '@/components/file-upload'; +import { FormDialog } from '@/components/form-dialog'; import InputError from '@/components/input-error'; +import { PageHeader } from '@/components/page-header'; import { RupiahInput } from '@/components/rupiah-input'; import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { useServerTable } from '@/hooks/use-server-table'; import { destroy, index as expenseIndex, @@ -54,7 +50,7 @@ export default function ExpenseIndex({ expenses }: Props) { size: number; type: string; } | null>(null); - const [search, setSearch] = useState(''); + const pagination: PaginationState = { current_page: expenses.current_page, last_page: expenses.last_page, @@ -62,45 +58,15 @@ export default function ExpenseIndex({ expenses }: Props) { total: expenses.total, }; - function handlePageChange(page: number) { - router.get( - expenseIndex.url(), - { - page, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - expenseIndex.url(), - { - page: 1, - per_page: perPage, - search, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - expenseIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search: value, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page], - ); + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => expenseIndex.url(), + pagination, + }); function handleDelete() { if (!deleting) { @@ -125,23 +91,9 @@ export default function ExpenseIndex({ expenses }: Props) {
-
-
-

- Pengeluaran -

-
- { - setCreateOpen(open); - - if (!open) { - setCreateReceiptKey(null); - setCreateFileMeta(null); - } - }} - > + - -
{ - setCreateOpen(false); - setCreateReceiptKey(null); - setCreateFileMeta(null); - }} - > - {({ errors, processing }) => { - return ( - <> - - - Tambah Pengeluaran - - -
-
- - - -
-
- - - -
-
- - - - - - -
-
- - - - - - ); - }} -
-
-
-
+ } + /> + + { + setCreateOpen(open); + + if (!open) { + setCreateReceiptKey(null); + setCreateFileMeta(null); + } + }} + title="Tambah Pengeluaran" + action={store()} + resetOnSuccess + onSuccess={() => { + setCreateOpen(false); + setCreateReceiptKey(null); + setCreateFileMeta(null); + }} + submitDisabled={createUploading} + submitLabel={createUploading ? 'Mengunggah...' : 'Simpan'} + > + {({ errors }) => ( + <> +
+ + + +
+
+ + + +
+
+ + + + + + +
+ + )} +
+ + { + if (!open) { + setEditing(null); + setEditReceiptKey(null); + setEditFileMeta(null); + } + }} + title="Edit Pengeluaran" + action={editing ? update(editing.id) : ''} + resetOnSuccess + onSuccess={() => { + setEditing(null); + setEditReceiptKey(null); + setEditFileMeta(null); + }} + submitDisabled={editUploading} + submitLabel={editUploading ? 'Mengunggah...' : 'Simpan'} + > + {({ errors }) => + editing && ( + <> +
+ + + +
+
+ + + +
+
+ + + + + + +
+ + ) + } +
- { - if (!open) { - setEditing(null); - setEditReceiptKey(null); - setEditFileMeta(null); - } - }} - > - - {editing && ( -
{ - setEditing(null); - setEditReceiptKey(null); - setEditFileMeta(null); - }} - > - {({ errors, processing }) => { - return ( - <> - - - Edit Pengeluaran - - -
-
- - - -
-
- - - -
-
- - - - - - -
-
- - - - - - ); - }} -
- )} -
-
- - { if (!open) { setDeleting(null); } }} title="Hapus Pengeluaran" - description={`Apakah Anda yakin ingin menghapus pengeluaran "${deleting?.description}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(expense) => + `Apakah Anda yakin ingin menghapus pengeluaran "${expense.description}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDelete} />
diff --git a/resources/js/pages/admin/finance/payroll-period/columns.tsx b/resources/js/pages/admin/finance/payroll-period/columns.tsx index dc108f1..dc207f5 100644 --- a/resources/js/pages/admin/finance/payroll-period/columns.tsx +++ b/resources/js/pages/admin/finance/payroll-period/columns.tsx @@ -1,15 +1,9 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Eye, Lock, Unlock } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { MONTH_NAMES } from '@/lib/constants'; import { formatCurrency } from '@/lib/utils'; -import { Link } from '@inertiajs/react'; export type PayrollPeriod = { id: number; @@ -26,22 +20,6 @@ export type PayrollPeriod = { payrolls_sum_deduction_amount: number | null; }; -const MONTH_NAMES = [ - '', - 'Januari', - 'Februari', - 'Maret', - 'April', - 'Mei', - 'Juni', - 'Juli', - 'Agustus', - 'September', - 'Oktober', - 'November', - 'Desember', -]; - function formatPeriod(period: PayrollPeriod): string { return `${MONTH_NAMES[period.month]} ${period.year}`; } @@ -201,56 +179,31 @@ export function createPayrollPeriodColumns( const period = row.original; return ( - -
- - - - - - Lihat Detail - - - - {period.status === 'open' && ( - - - - - - Tutup Periode - - - )} - - {period.status === 'closed' && ( - - - - - - Buka Periode - - - )} -
-
+ , + href: showUrl(period.id), + }, + { + label: 'Tutup Periode', + icon: ( + + ), + show: period.status === 'open', + onClick: () => handleClose(period), + }, + { + label: 'Buka Periode', + icon: ( + + ), + show: period.status === 'closed', + onClick: () => handleReopen(period), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/finance/payroll-period/index.tsx b/resources/js/pages/admin/finance/payroll-period/index.tsx index d563a98..1bacb3e 100644 --- a/resources/js/pages/admin/finance/payroll-period/index.tsx +++ b/resources/js/pages/admin/finance/payroll-period/index.tsx @@ -1,19 +1,17 @@ import { Head, router } from '@inertiajs/react'; -import { useCallback, useState } from 'react'; -import { ConfirmDialog } from '@/components/confirm-dialog'; +import { useState } from 'react'; import type { PaginationState } from '@/components/data-table'; import { DataTable } from '@/components/data-table'; -import { Button } from '@/components/ui/button'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { PageHeader } from '@/components/page-header'; +import { useServerTable } from '@/hooks/use-server-table'; +import { MONTH_NAMES } from '@/lib/constants'; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { index as payrollPeriodsIndex } from '@/routes/admin/finance/payroll-periods'; -import { show as payrollPeriodShow } from '@/routes/admin/finance/payroll-periods'; -import { close, reopen } from '@/routes/admin/finance/payroll-periods'; + index as payrollPeriodsIndex, + show as payrollPeriodShow, + close, + reopen, +} from '@/routes/admin/finance/payroll-periods'; import { createPayrollPeriodColumns } from './columns'; import type { PayrollPeriod } from './columns'; @@ -27,26 +25,10 @@ type Props = { }; }; -const MONTH_NAMES = [ - '', - 'Januari', - 'Februari', - 'Maret', - 'April', - 'Mei', - 'Juni', - 'Juli', - 'Agustus', - 'September', - 'Oktober', - 'November', - 'Desember', -]; - export default function PayrollPeriodIndex({ payrollPeriods }: Props) { const [closing, setClosing] = useState(null); const [reopening, setReopening] = useState(null); - const [search, setSearch] = useState(''); + const pagination: PaginationState = { current_page: payrollPeriods.current_page, last_page: payrollPeriods.last_page, @@ -54,6 +36,16 @@ export default function PayrollPeriodIndex({ payrollPeriods }: Props) { total: payrollPeriods.total, }; + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => payrollPeriodsIndex.url(), + pagination, + }); + function handleClose() { if (!closing) { return; @@ -82,46 +74,6 @@ export default function PayrollPeriodIndex({ payrollPeriods }: Props) { ); } - function handlePageChange(page: number) { - router.get( - payrollPeriodsIndex(), - { - page, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - payrollPeriodsIndex(), - { - page: 1, - per_page: perPage, - search, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - payrollPeriodsIndex(), - { - page: 1, - per_page: pagination.per_page, - search: value, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page], - ); - const columns = createPayrollPeriodColumns({ showUrl: (id) => payrollPeriodShow(id).url, handleClose: (period) => setClosing(period), @@ -133,13 +85,7 @@ export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
-
-
-

- Gaji -

-
-
+ - { if (!open) { setClosing(null); } }} title="Tutup Periode Gaji" - description={`Apakah Anda yakin ingin menutup periode gaji ${closing ? `${MONTH_NAMES[closing.month]} ${closing.year}` : ''}? Semua gaji harus sudah dibayar sebelum periode ditutup.`} + description={(period) => + `Apakah Anda yakin ingin menutup periode gaji ${MONTH_NAMES[period.month]} ${period.year}? Semua gaji harus sudah dibayar sebelum periode ditutup.` + } confirmLabel="Tutup" onConfirm={handleClose} /> - { if (!open) { setReopening(null); } }} title="Buka Periode Gaji" - description={`Apakah Anda yakin ingin membuka kembali periode gaji ${reopening ? `${MONTH_NAMES[reopening.month]} ${reopening.year}` : ''}?`} + description={(period) => + `Apakah Anda yakin ingin membuka kembali periode gaji ${MONTH_NAMES[period.month]} ${period.year}?` + } confirmLabel="Buka" onConfirm={handleReopen} /> diff --git a/resources/js/pages/admin/finance/payroll-period/show-columns.tsx b/resources/js/pages/admin/finance/payroll-period/show-columns.tsx index 17c4c36..c07ab34 100644 --- a/resources/js/pages/admin/finance/payroll-period/show-columns.tsx +++ b/resources/js/pages/admin/finance/payroll-period/show-columns.tsx @@ -1,13 +1,7 @@ import type { ColumnDef } from '@tanstack/react-table'; import { CircleDollarSign, Pencil, Trash2, XCircle } from 'lucide-react'; -import { Button } from '@/components/ui/button'; +import { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; import { formatCurrency } from '@/lib/utils'; export type Payroll = { @@ -215,64 +209,32 @@ export function createPayrollColumns( const payroll = row.original; return ( - -
- {payroll.status === 'unpaid' && ( - <> - - - - - - Tambah Adjustment - - - - - - - - - Tandai Dibayar - - - - - - - - - Batalkan - - - - )} -
-
+ , + show: payroll.status === 'unpaid', + onClick: () => handleAddAdjustment(payroll), + }, + { + label: 'Tandai Dibayar', + icon: ( + + ), + show: payroll.status === 'unpaid', + onClick: () => handlePay(payroll), + }, + { + label: 'Batalkan', + icon: ( + + ), + show: payroll.status === 'unpaid', + onClick: () => handleCancel(payroll), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/finance/payroll-period/show.tsx b/resources/js/pages/admin/finance/payroll-period/show.tsx index 0e22b54..c41aff5 100644 --- a/resources/js/pages/admin/finance/payroll-period/show.tsx +++ b/resources/js/pages/admin/finance/payroll-period/show.tsx @@ -1,3 +1,6 @@ +import { Form, Head, router } from '@inertiajs/react'; +import { ArrowLeft } from 'lucide-react'; +import { useState } from 'react'; import { ConfirmDialog } from '@/components/confirm-dialog'; import { DataTable } from '@/components/data-table'; import InputError from '@/components/input-error'; @@ -13,6 +16,7 @@ import { import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { MONTH_NAMES } from '@/lib/constants'; import { formatCurrency } from '@/lib/utils'; import { destroy as adjustmentDestroy } from '@/routes/admin/finance/payroll-adjustments'; import { index as payrollPeriodsIndex } from '@/routes/admin/finance/payroll-periods'; @@ -21,9 +25,6 @@ import { pay as payrollPay, } from '@/routes/admin/finance/payrolls'; import { store as adjustmentStore } from '@/routes/admin/finance/payrolls/adjustments'; -import { Form, Head, router } from '@inertiajs/react'; -import { ArrowLeft } from 'lucide-react'; -import { useState } from 'react'; import type { Payroll, PayrollAdjustment } from './show-columns'; import { createPayrollColumns } from './show-columns'; @@ -37,22 +38,6 @@ type Props = { }; }; -const MONTH_NAMES = [ - '', - 'Januari', - 'Februari', - 'Maret', - 'April', - 'Mei', - 'Juni', - 'Juli', - 'Agustus', - 'September', - 'Oktober', - 'November', - 'Desember', -]; - export default function PayrollPeriodShow({ payrollPeriod }: Props) { const [paying, setPaying] = useState(null); const [cancelling, setCancelling] = useState(null); @@ -66,7 +51,9 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) { const [adjustmentType, setAdjustmentType] = useState('bonus'); function handlePay() { - if (!paying) return; + if (!paying) { + return; + } router.post( payrollPay(paying.id), @@ -78,7 +65,9 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) { } function handleCancel() { - if (!cancelling) return; + if (!cancelling) { + return; + } router.post( payrollCancel(cancelling.id), @@ -90,7 +79,9 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) { } function handleDeleteAdjustment() { - if (!deletingAdjustment) return; + if (!deletingAdjustment) { + return; + } router.delete(adjustmentDestroy(deletingAdjustment.adjustment.id), { onSuccess: () => setDeletingAdjustment(null), @@ -336,7 +327,9 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) { { - if (!open) setPaying(null); + if (!open) { + setPaying(null); + } }} title="Tandai Dibayar" description={`Apakah Anda yakin ingin menandai gaji "${paying?.employee?.user?.user_profile?.full_name}" sebesar ${formatCurrency(paying?.total_amount ?? 0)} sebagai sudah dibayar? Penyesuaian tidak dapat ditambahkan setelah dibayar.`} @@ -348,7 +341,9 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) { { - if (!open) setCancelling(null); + if (!open) { + setCancelling(null); + } }} title="Batalkan Gaji" description={`Apakah Anda yakin ingin membatalkan gaji "${cancelling?.employee?.user?.user_profile?.full_name}"?`} @@ -360,7 +355,9 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) { { - if (!open) setDeletingAdjustment(null); + if (!open) { + setDeletingAdjustment(null); + } }} title="Hapus Penyesuaian" description={`Apakah Anda yakin ingin menghapus penyesuaian "${deletingAdjustment?.adjustment.description}" sebesar ${formatCurrency(deletingAdjustment?.adjustment.amount ?? 0)}?`} diff --git a/resources/js/pages/admin/hr/employee/columns.tsx b/resources/js/pages/admin/hr/employee/columns.tsx index 202479e..b6712a3 100644 --- a/resources/js/pages/admin/hr/employee/columns.tsx +++ b/resources/js/pages/admin/hr/employee/columns.tsx @@ -1,14 +1,8 @@ import type { ColumnDef } from '@tanstack/react-table'; -import { router } from '@inertiajs/react'; import { KeyRound, Pencil, Trash2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Switch } from '@/components/ui/switch'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { RowActions } from '@/components/row-actions'; +import { ToggleStatus } from '@/components/toggle-status'; +import { formatCurrency } from '@/lib/utils'; export type Employee = { id: number; @@ -38,14 +32,6 @@ function getEmploymentStatusLabel(status: string): string { return labels[status] ?? status; } -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(amount); -} - type CreateColumnsParams = { handleEdit: (employee: Employee) => void; handleDeleteClick: (employee: Employee) => void; @@ -132,18 +118,10 @@ export function createEmployeeColumns( return (
- { - router.post( - toggleActiveUrl(employee.id), - {}, - { - preserveScroll: true, - }, - ); - }} + wrapperClassName="flex items-center justify-center gap-1" />
); @@ -160,56 +138,29 @@ export function createEmployeeColumns( const employee = row.original; return ( - -
- - - - - Edit - - - - - - - - Reset Kata Sandi - - - - - - - - - Hapus - - -
-
+ , + onClick: () => handleEdit(employee), + }, + { + label: 'Reset Kata Sandi', + icon: ( + + ), + onClick: () => handleResetPassword(employee), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(employee), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/hr/employee/index.tsx b/resources/js/pages/admin/hr/employee/index.tsx index a9ea57b..0b0eab6 100644 --- a/resources/js/pages/admin/hr/employee/index.tsx +++ b/resources/js/pages/admin/hr/employee/index.tsx @@ -1,15 +1,12 @@ import { Head, router } from '@inertiajs/react'; -import { Filter, Plus, X } from 'lucide-react'; -import { useCallback, useState } from 'react'; -import { ConfirmDialog } from '@/components/confirm-dialog'; -import { DataTable } from '@/components/data-table'; +import { Plus } from 'lucide-react'; +import { useState } from 'react'; import type { PaginationState } from '@/components/data-table'; +import { DataTable } from '@/components/data-table'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FilterPopover } from '@/components/filter-popover'; +import { PageHeader } from '@/components/page-header'; import { Button } from '@/components/ui/button'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; import { Select, SelectContent, @@ -17,6 +14,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { useServerTable } from '@/hooks/use-server-table'; import { destroy, create as employeeCreate, @@ -47,8 +45,7 @@ export default function EmployeeIndex({ employees, filters }: Props) { const [deleting, setDeleting] = useState(null); const [resetPasswordTarget, setResetPasswordTarget] = useState(null); - const [filterOpen, setFilterOpen] = useState(false); - const [search, setSearch] = useState(''); + const pagination: PaginationState = { current_page: employees.current_page, last_page: employees.last_page, @@ -56,84 +53,20 @@ export default function EmployeeIndex({ employees, filters }: Props) { total: employees.total, }; - const hasActiveFilters = filters.employment_status || filters.is_active; - - function applyFilter(key: string, value: string) { - const newFilters = { ...filters }; - - if (value === '' || value === 'all') { - delete newFilters[key as keyof typeof newFilters]; - } else { - newFilters[key as keyof typeof newFilters] = value; - } - - router.get( - employeeIndex.url(), - { - ...newFilters, - page: 1, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function clearFilters() { - router.get( - employeeIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - setFilterOpen(false); - } - - function handlePageChange(page: number) { - router.get( - employeeIndex.url(), - { - ...filters, - page, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - employeeIndex.url(), - { - ...filters, - page: 1, - per_page: perPage, - search, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - employeeIndex.url(), - { - ...filters, - page: 1, - per_page: pagination.per_page, - search: value, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page, filters], - ); + const { + search, + filterOpen, + setFilterOpen, + handlePageChange, + handlePerPageChange, + handleSearchChange, + applyFilter, + clearFilters, + } = useServerTable({ + route: () => employeeIndex.url(), + pagination, + filters, + }); function handleDelete() { if (!deleting) { @@ -169,115 +102,77 @@ export default function EmployeeIndex({ employees, filters }: Props) { }); const filterToolbar = ( - - - - - -
-
- Filter - {hasActiveFilters && ( - - )} -
+ +
+ + +
-
- - -
+
+ + +
-
- - -
- -
- - -
-
-
-
+
+ + +
+ ); return ( @@ -285,19 +180,17 @@ export default function EmployeeIndex({ employees, filters }: Props) {
-
-
-

- Pegawai -

-
- -
+ + + + Tambah + + + } + /> - { if (!open) { setDeleting(null); } }} title="Hapus Pegawai" - description={`Apakah Anda yakin ingin menghapus pegawai "${deleting?.user_profile?.full_name}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(employee) => + `Apakah Anda yakin ingin menghapus pegawai "${employee.user_profile?.full_name}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDelete} /> - { if (!open) { setResetPasswordTarget(null); } }} title="Reset Kata Sandi" - description={`Apakah Anda yakin ingin mereset kata sandi pegawai "${resetPasswordTarget?.user_profile?.full_name}" ke kata sandi default?`} + description={(employee) => + `Apakah Anda yakin ingin mereset kata sandi pegawai "${employee.user_profile?.full_name}" ke kata sandi default?` + } confirmLabel="Reset" variant="default" onConfirm={handleResetPassword} diff --git a/resources/js/pages/admin/hr/leave-request/columns.tsx b/resources/js/pages/admin/hr/leave-request/columns.tsx index 4530347..dd0826c 100644 --- a/resources/js/pages/admin/hr/leave-request/columns.tsx +++ b/resources/js/pages/admin/hr/leave-request/columns.tsx @@ -1,13 +1,8 @@ -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; import type { ColumnDef } from '@tanstack/react-table'; import { CheckCircle, Pencil, Trash2, XCircle } from 'lucide-react'; +import { RowActions } from '@/components/row-actions'; +import { Badge } from '@/components/ui/badge'; +import { formatShortDate } from '@/lib/format'; export type LeaveRequest = { id: number; @@ -25,16 +20,6 @@ export type LeaveRequest = { }; }; -function formatShortDate(dateString: string): string { - const date = new Date(dateString); - - return date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - }); -} - function getStatusBadge(status: string) { const statusConfig: Record = { pending: { @@ -136,77 +121,38 @@ export function createLeaveRequestColumns( const leaveRequest = row.original; return ( - -
- {leaveRequest.status === 'pending' && ( - <> - - - - - - Setujui - - - - - - - - - Tolak - - - - )} - - - - - - Edit - - - - - - - - Hapus - - -
-
+ + ), + show: leaveRequest.status === 'pending', + onClick: () => handleApprove(leaveRequest), + }, + { + label: 'Tolak', + icon: ( + + ), + show: leaveRequest.status === 'pending', + onClick: () => handleReject(leaveRequest), + }, + { + label: 'Edit', + icon: , + onClick: () => handleEdit(leaveRequest), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(leaveRequest), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/hr/leave-request/index.tsx b/resources/js/pages/admin/hr/leave-request/index.tsx index b6600aa..4ceb88c 100644 --- a/resources/js/pages/admin/hr/leave-request/index.tsx +++ b/resources/js/pages/admin/hr/leave-request/index.tsx @@ -1,25 +1,16 @@ -import { Form, Head, router } from '@inertiajs/react'; -import { Filter, Plus, X } from 'lucide-react'; -import { useCallback, useEffect, useState } from 'react'; -import { ConfirmDialog } from '@/components/confirm-dialog'; -import { DataTable } from '@/components/data-table'; +import { Head, router } from '@inertiajs/react'; +import { Plus } from 'lucide-react'; +import { useEffect, useState } from 'react'; import type { PaginationState } from '@/components/data-table'; +import { DataTable } from '@/components/data-table'; import { DatePicker } from '@/components/date-picker'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FilterPopover } from '@/components/filter-popover'; +import { FormDialog } from '@/components/form-dialog'; import InputError from '@/components/input-error'; +import { PageHeader } from '@/components/page-header'; import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; import { Select, SelectContent, @@ -27,6 +18,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { useServerTable } from '@/hooks/use-server-table'; import { approve, destroy, @@ -65,8 +57,7 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { const [editingEndDate, setEditingEndDate] = useState( undefined, ); - const [filterOpen, setFilterOpen] = useState(false); - const [search, setSearch] = useState(''); + const pagination: PaginationState = { current_page: leaveRequests.current_page, last_page: leaveRequests.last_page, @@ -74,7 +65,20 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { total: leaveRequests.total, }; - const hasActiveFilters = filters.status; + const { + search, + filterOpen, + setFilterOpen, + handlePageChange, + handlePerPageChange, + handleSearchChange, + applyFilter, + clearFilters, + } = useServerTable({ + route: () => leaveRequestIndex.url(), + pagination, + filters, + }); useEffect(() => { if (editing) { @@ -86,40 +90,6 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { } }, [editing]); - function applyFilter(key: string, value: string) { - const newFilters = { ...filters }; - - if (value === '' || value === 'all') { - delete newFilters[key as keyof typeof newFilters]; - } else { - newFilters[key as keyof typeof newFilters] = value; - } - - router.get( - leaveRequestIndex(), - { - ...newFilters, - page: 1, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function clearFilters() { - router.get( - leaveRequestIndex(), - { - page: 1, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - setFilterOpen(false); - } - function handleDelete() { if (!deleting) { return; @@ -158,49 +128,6 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { ); } - function handlePageChange(page: number) { - router.get( - leaveRequestIndex(), - { - ...filters, - page, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - leaveRequestIndex(), - { - ...filters, - page: 1, - per_page: perPage, - search, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - leaveRequestIndex(), - { - ...filters, - page: 1, - per_page: pagination.per_page, - search: value, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page, filters], - ); - const columns = createLeaveRequestColumns({ handleEdit: (leaveRequest) => setEditing(leaveRequest), handleDeleteClick: (leaveRequest) => setDeleting(leaveRequest), @@ -209,70 +136,32 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { }); const filterToolbar = ( - - - - - -
-
- Filter - {hasActiveFilters && ( - - )} -
- -
- - -
-
-
-
+ +
+ + +
+
); return ( @@ -280,23 +169,9 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) {
-
-
-

- Cuti -

-
- { - setCreateOpen(open); - - if (!open) { - setStartDate(undefined); - setEndDate(undefined); - } - }} - > + - -
setCreateOpen(false)} - > - {({ errors, processing }) => { - return ( - <> - - - Tambah Permohonan Cuti - - -
-
- - - - -
-
- - - - -
-
- - - - - - ); - }} -
-
-
-
+ } + /> + + { + setCreateOpen(open); + + if (!open) { + setStartDate(undefined); + setEndDate(undefined); + } + }} + title="Tambah Permohonan Cuti" + action={store()} + resetOnSuccess + onSuccess={() => setCreateOpen(false)} + > + {({ errors }) => ( + <> +
+ + + + +
+
+ + + + +
+ + )} +
- { if (!open) { @@ -436,125 +276,77 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { setEditingEndDate(undefined); } }} + title="Edit Permohonan Cuti" + action={editing ? update(editing.id) : ''} + resetOnSuccess + onSuccess={() => { + setEditing(null); + setEditingStartDate(undefined); + setEditingEndDate(undefined); + }} > - - {editing && ( -
{ - setEditing(null); - setEditingStartDate(undefined); - setEditingEndDate(undefined); - }} - > - {({ errors, processing }) => { - return ( - <> - - - Edit Permohonan Cuti - - -
-
- - - - -
-
- - - - -
-
- - - - - - ); - }} -
- )} -
-
+ {({ errors }) => + editing && ( + <> +
+ + + + +
+
+ + + + +
+ + ) + } + - { if (!open) { setDeleting(null); @@ -562,12 +354,11 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { }} title="Hapus Permohonan Cuti" description={`Apakah Anda yakin ingin menghapus permohonan cuti ini? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" onConfirm={handleDelete} /> - { if (!open) { setApproving(null); @@ -579,8 +370,8 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { onConfirm={handleApprove} /> - { if (!open) { setRejecting(null); diff --git a/resources/js/pages/admin/manage/purchase/create.tsx b/resources/js/pages/admin/manage/purchase/create.tsx index cb7ffe1..ab013a6 100644 --- a/resources/js/pages/admin/manage/purchase/create.tsx +++ b/resources/js/pages/admin/manage/purchase/create.tsx @@ -41,18 +41,14 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; import { usePurchaseDraftSave } from '@/hooks/use-purchase-draft'; +import { UNITS } from '@/lib/constants'; +import { formatNumber } from '@/lib/format'; import { loadPurchaseDraft } from '@/lib/purchase-draft'; import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; import { index as purchaseIndex, store } from '@/routes/admin/manage/purchases'; import type { PurchaseCreateData } from './columns'; -const UNITS = [ - { value: 'kg', label: 'Kilogram' }, - { value: 'meter', label: 'Meter' }, - { value: 'yard', label: 'Yard' }, -]; - type VariantState = { variant: string; price: number; @@ -383,9 +379,7 @@ export default function PurchaseCreate({ data }: Props) { })(); function formatQuantity(value: number): string { - return new Intl.NumberFormat('id-ID', { - maximumFractionDigits: 4, - }).format(value); + return formatNumber(value, { maximumFractionDigits: 4 }); } function getPayload() { @@ -806,9 +800,9 @@ export default function PurchaseCreate({ data }: Props) { - m.name - } + itemToStringLabel={( + m, + ) => m.name} value={selectedMaterial} onValueChange={( value, @@ -834,10 +828,14 @@ export default function PurchaseCreate({ data }: Props) { key={ m.id } - value={m} + value={ + m + } > {m.name}{' '} - ({m.unit}) + ( + {m.unit} + ) )} @@ -932,7 +930,9 @@ export default function PurchaseCreate({ data }: Props) { setSupplierId( value - ? String( - value.id, - ) + ? String(value.id) : '', ) } diff --git a/resources/js/pages/admin/manage/purchase/edit.tsx b/resources/js/pages/admin/manage/purchase/edit.tsx index 08af1f8..abb91e6 100644 --- a/resources/js/pages/admin/manage/purchase/edit.tsx +++ b/resources/js/pages/admin/manage/purchase/edit.tsx @@ -39,6 +39,7 @@ import { } from '@/components/ui/sheet'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; +import { formatNumber } from '@/lib/format'; import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; import { @@ -100,9 +101,7 @@ export default function PurchaseEdit({ purchase, data }: Props) { const [photoUrl, setPhotoUrl] = useState(purchase.photo_url); const [uploading, setUploading] = useState(false); - const [mode, setMode] = useState<'new' | 'existing'>( - purchase.default_mode, - ); + const [mode, setMode] = useState<'new' | 'existing'>(purchase.default_mode); const [selectedMaterialName, setSelectedMaterialName] = useState( purchase.existing_material_name ?? '', ); @@ -321,9 +320,7 @@ export default function PurchaseEdit({ purchase, data }: Props) { })(); function formatQuantity(value: number): string { - return new Intl.NumberFormat('id-ID', { - maximumFractionDigits: 4, - }).format(value); + return formatNumber(value, { maximumFractionDigits: 4 }); } function getPayload() { @@ -704,9 +701,9 @@ export default function PurchaseEdit({ purchase, data }: Props) { - m.name - } + itemToStringLabel={( + m, + ) => m.name} value={selectedMaterial} onValueChange={( value, @@ -732,10 +729,14 @@ export default function PurchaseEdit({ purchase, data }: Props) { key={ m.id } - value={m} + value={ + m + } > {m.name}{' '} - ({m.unit}) + ( + {m.unit} + ) )} @@ -830,7 +831,9 @@ export default function PurchaseEdit({ purchase, data }: Props) { setSupplierId( value - ? String( - value.id, - ) + ? String(value.id) : '', ) } diff --git a/resources/js/pages/admin/manage/purchase/index.tsx b/resources/js/pages/admin/manage/purchase/index.tsx index 1e4bde4..3051f4b 100644 --- a/resources/js/pages/admin/manage/purchase/index.tsx +++ b/resources/js/pages/admin/manage/purchase/index.tsx @@ -1,10 +1,12 @@ import { Head, router } from '@inertiajs/react'; import { Plus } from 'lucide-react'; -import { useCallback, useState } from 'react'; +import { useState } from 'react'; import { CardTable } from '@/components/card-table'; -import { ConfirmDialog } from '@/components/confirm-dialog'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; import { useCardTableExpand } from '@/components/hooks/use-card-table-expand'; +import { PageHeader } from '@/components/page-header'; import { Button } from '@/components/ui/button'; +import { useServerTable } from '@/hooks/use-server-table'; import { destroy, create as purchaseCreate, @@ -27,7 +29,6 @@ type Props = { export default function PurchaseIndex({ purchases }: Props) { const [deleting, setDeleting] = useState(null); - const [search, setSearch] = useState(''); const expand = useCardTableExpand(true); const pagination = { @@ -37,45 +38,15 @@ export default function PurchaseIndex({ purchases }: Props) { total: purchases.total, }; - function handlePageChange(page: number) { - router.get( - purchaseIndex.url(), - { - page, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - purchaseIndex.url(), - { - page: 1, - per_page: perPage, - search, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - purchaseIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search: value, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page], - ); + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => purchaseIndex.url(), + pagination, + }); function handleDelete() { if (!deleting) { @@ -92,19 +63,17 @@ export default function PurchaseIndex({ purchases }: Props) {
-
-
-

- Belanja -

-
- -
+ + + + Tambah + + + } + /> - { if (!open) { setDeleting(null); } }} title="Hapus Belanja" - description={`Apakah Anda yakin ingin menghapus belanja dari "${deleting?.supplier?.name}"? Stok akan dikembalikan. Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(purchase) => + `Apakah Anda yakin ingin menghapus belanja dari "${purchase.supplier?.name}"? Stok akan dikembalikan. Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDelete} />
diff --git a/resources/js/pages/admin/manage/purchase/purchase-card.tsx b/resources/js/pages/admin/manage/purchase/purchase-card.tsx index fad9db5..b459e57 100644 --- a/resources/js/pages/admin/manage/purchase/purchase-card.tsx +++ b/resources/js/pages/admin/manage/purchase/purchase-card.tsx @@ -1,41 +1,12 @@ import { ChevronDown, Pencil, Trash2 } from 'lucide-react'; -import { useState } from 'react'; +import { ImagePreviewButton } from '@/components/image-preview-button'; +import { RowActions } from '@/components/row-actions'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; -import { ImagePreviewModal } from '@/components/image-preview-modal'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { formatDateTime, formatNumber } from '@/lib/format'; +import { formatCurrency } from '@/lib/utils'; import type { Purchase } from './columns'; -function formatDateTime(dateString: string): string { - const date = new Date(dateString); - - return date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); -} -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(amount); -} - -function formatNumber(num: number): string { - return new Intl.NumberFormat('id-ID', { - maximumFractionDigits: 4, - }).format(num); -} - export type PurchaseCardRowParams = { purchase: Purchase; index: number; @@ -57,10 +28,11 @@ export function PurchaseCardRow({ const variantCount = items.length; const rawMaterialName = items[0]?.raw_material_price?.raw_material?.name ?? '-'; - const unit = - items[0]?.raw_material_price?.raw_material?.unit ?? ''; - const totalQty = items.reduce((sum, item) => sum + Number(item.quantity), 0); - const [photoPreviewOpen, setPhotoPreviewOpen] = useState(false); + const unit = items[0]?.raw_material_price?.raw_material?.unit ?? ''; + const totalQty = items.reduce( + (sum, item) => sum + Number(item.quantity), + 0, + ); return ( <> @@ -110,86 +82,68 @@ export function PurchaseCardRow({
- Qty: + + Qty:{' '} + {formatNumber(totalQty)} {unit} - Sub: + + Sub:{' '} + {formatCurrency(purchase.subtotal)} - Disc: + + Disc:{' '} + {formatCurrency(purchase.discount)} - Ongkir: + + Ongkir:{' '} + {formatCurrency(purchase.shipping_cost)} - Total: + + Total:{' '} + {formatCurrency(purchase.total)}
{purchase.photo_url && (
- +
)}
- -
- - - - - Edit - - - - - - - Hapus - - -
-
+ , + onClick: () => onEdit(purchase), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => onDelete(purchase), + }, + ]} + wrapperClassName="flex shrink-0 items-center gap-1" + />
- - {purchase.photo_url && ( - - )} ); } diff --git a/resources/js/pages/admin/manage/purchase/purchase-sub-row.tsx b/resources/js/pages/admin/manage/purchase/purchase-sub-row.tsx index 7e9bb63..fc0089c 100644 --- a/resources/js/pages/admin/manage/purchase/purchase-sub-row.tsx +++ b/resources/js/pages/admin/manage/purchase/purchase-sub-row.tsx @@ -1,5 +1,4 @@ -import { useState } from 'react'; -import { ImagePreviewModal } from '@/components/image-preview-modal'; +import { ImagePreviewButton } from '@/components/image-preview-button'; import { Table, TableBody, @@ -8,67 +7,13 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; +import { formatNumber } from '@/lib/format'; +import { formatCurrency } from '@/lib/utils'; import type { Purchase } from './columns'; -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(amount); -} - -function formatNumber(num: number): string { - return new Intl.NumberFormat('id-ID', { - maximumFractionDigits: 4, - }).format(num); -} - -function formatDateTime(dateString: string): string { - const date = new Date(dateString); - return date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); -} - -function VariantPhotoPreview({ url, title }: { url: string; title: string }) { - const [open, setOpen] = useState(false); - - return ( - <> - - - - ); -} - -export function PurchaseItemSubRow({ - purchase, -}: { - purchase: Purchase; -}) { +export function PurchaseItemSubRow({ purchase }: { purchase: Purchase }) { const items = purchase.purchase_items ?? []; - const unit = - items[0]?.raw_material_price?.raw_material?.unit ?? ''; + const unit = items[0]?.raw_material_price?.raw_material?.unit ?? ''; return (
@@ -103,9 +48,14 @@ export function PurchaseItemSubRow({ {item.raw_material_price?.photo_url ? ( - ) : (
diff --git a/resources/js/pages/admin/manage/restock/create.tsx b/resources/js/pages/admin/manage/restock/create.tsx index 6f569df..cc30731 100644 --- a/resources/js/pages/admin/manage/restock/create.tsx +++ b/resources/js/pages/admin/manage/restock/create.tsx @@ -29,6 +29,7 @@ import { } from '@/components/ui/sheet'; import { Textarea } from '@/components/ui/textarea'; import { useRestockDraftSave } from '@/hooks/use-restock-draft'; +import { formatNumber } from '@/lib/format'; import { loadRestockDraft } from '@/lib/restock-draft'; import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; @@ -107,8 +108,7 @@ export default function RestockCreate({ data }: Props) { }, [quantities]); const selectedProduct = useMemo( - () => - products.find((p) => String(p.id) === selectedProductId) ?? null, + () => products.find((p) => String(p.id) === selectedProductId) ?? null, [products, selectedProductId], ); @@ -178,9 +178,7 @@ export default function RestockCreate({ data }: Props) { })(); function formatQuantity(value: number): string { - return new Intl.NumberFormat('id-ID', { - maximumFractionDigits: 4, - }).format(value); + return formatNumber(value, { maximumFractionDigits: 4 }); } function getPayload() { @@ -292,8 +290,7 @@ export default function RestockCreate({ data }: Props) { (quantities[ variant .id - ] ?? - 0) > 0 + ] ?? 0) > 0 ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3' } @@ -329,11 +326,9 @@ export default function RestockCreate({ data }: Props) { )}{' '} pcs ·{' '} - { - formatCurrency( - variant.capital_price, - ) - } + {formatCurrency( + variant.capital_price, + )}

@@ -416,8 +411,7 @@ export default function RestockCreate({ data }: Props) { onValueChange={(value) => setStockType( value as - | 'good' - | 'reject', + 'good' | 'reject', ) } className="flex flex-wrap gap-4" @@ -507,9 +501,7 @@ export default function RestockCreate({ data }: Props) { }} folder="restock" existingUrl={photoUrl} - onUploadingChange={ - setUploading - } + onUploadingChange={setUploading} /> q <= 0) + Object.values(quantities).every( + (q) => q <= 0, + ) } > {processing @@ -648,8 +640,7 @@ export default function RestockCreate({ data }: Props) {
{formatCurrency( - item.price * - item.quantity, + item.price * item.quantity, )}
diff --git a/resources/js/pages/admin/manage/restock/edit.tsx b/resources/js/pages/admin/manage/restock/edit.tsx index c8508ce..3331754 100644 --- a/resources/js/pages/admin/manage/restock/edit.tsx +++ b/resources/js/pages/admin/manage/restock/edit.tsx @@ -20,12 +20,10 @@ import { SheetTitle, } from '@/components/ui/sheet'; import { Textarea } from '@/components/ui/textarea'; +import { formatNumber } from '@/lib/format'; import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; -import { - index as restockIndex, - update, -} from '@/routes/admin/manage/restocks'; +import { index as restockIndex, update } from '@/routes/admin/manage/restocks'; import type { RestockCreateData, RestockForEdit } from './columns'; type CartLine = { @@ -71,9 +69,7 @@ export default function RestockEdit({ restock, data }: Props) { ); const [notes, setNotes] = useState(restock.notes ?? ''); const [photo, setPhoto] = useState(restock.photo_key); - const [photoUrl, setPhotoUrl] = useState( - restock.photo_url, - ); + const [photoUrl, setPhotoUrl] = useState(restock.photo_url); const [uploading, setUploading] = useState(false); const [cartOpen, setCartOpen] = useState(false); const [previewKey, setPreviewKey] = useState(null); @@ -86,8 +82,7 @@ export default function RestockEdit({ restock, data }: Props) { }, [quantities]); const selectedProduct = useMemo( - () => - products.find((p) => String(p.id) === selectedProductId) ?? null, + () => products.find((p) => String(p.id) === selectedProductId) ?? null, [products, selectedProductId], ); @@ -157,9 +152,7 @@ export default function RestockEdit({ restock, data }: Props) { })(); function formatQuantity(value: number): string { - return new Intl.NumberFormat('id-ID', { - maximumFractionDigits: 4, - }).format(value); + return formatNumber(value, { maximumFractionDigits: 4 }); } function getPayload() { @@ -225,8 +218,7 @@ export default function RestockEdit({ restock, data }: Props) { (quantities[ variant .id - ] ?? - 0) > 0 + ] ?? 0) > 0 ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3' } @@ -262,11 +254,9 @@ export default function RestockEdit({ restock, data }: Props) { )}{' '} pcs ·{' '} - { - formatCurrency( - variant.capital_price, - ) - } + {formatCurrency( + variant.capital_price, + )}

@@ -354,8 +344,7 @@ export default function RestockEdit({ restock, data }: Props) { onValueChange={(value) => setStockType( value as - | 'good' - | 'reject', + 'good' | 'reject', ) } className="flex flex-wrap gap-4" @@ -445,9 +434,7 @@ export default function RestockEdit({ restock, data }: Props) { }} folder="restock" existingUrl={photoUrl} - onUploadingChange={ - setUploading - } + onUploadingChange={setUploading} /> q <= 0) + Object.values(quantities).every( + (q) => q <= 0, + ) } > {processing @@ -585,8 +572,7 @@ export default function RestockEdit({ restock, data }: Props) { {formatCurrency( - item.price * - item.quantity, + item.price * item.quantity, )} diff --git a/resources/js/pages/admin/manage/restock/index.tsx b/resources/js/pages/admin/manage/restock/index.tsx index a896950..9a3d846 100644 --- a/resources/js/pages/admin/manage/restock/index.tsx +++ b/resources/js/pages/admin/manage/restock/index.tsx @@ -1,10 +1,12 @@ import { Head, router } from '@inertiajs/react'; import { Plus } from 'lucide-react'; -import { useCallback, useState } from 'react'; +import { useState } from 'react'; import { CardTable } from '@/components/card-table'; -import { ConfirmDialog } from '@/components/confirm-dialog'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; import { useCardTableExpand } from '@/components/hooks/use-card-table-expand'; +import { PageHeader } from '@/components/page-header'; import { Button } from '@/components/ui/button'; +import { useServerTable } from '@/hooks/use-server-table'; import { destroy, create as restockCreate, @@ -27,7 +29,6 @@ type Props = { export default function RestockIndex({ restocks }: Props) { const [deleting, setDeleting] = useState(null); - const [search, setSearch] = useState(''); const expand = useCardTableExpand(true); const pagination = { @@ -37,45 +38,15 @@ export default function RestockIndex({ restocks }: Props) { total: restocks.total, }; - function handlePageChange(page: number) { - router.get( - restockIndex.url(), - { - page, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - restockIndex.url(), - { - page: 1, - per_page: perPage, - search, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - restockIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search: value, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page], - ); + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => restockIndex.url(), + pagination, + }); function handleDelete() { if (!deleting) { @@ -92,19 +63,17 @@ export default function RestockIndex({ restocks }: Props) {
-
-
-

- Restock -

-
- -
+ + + + Tambah + + + } + /> - { if (!open) { setDeleting(null); @@ -153,7 +122,6 @@ export default function RestockIndex({ restocks }: Props) { }} title="Hapus Restock" description="Apakah Anda yakin ingin menghapus restock ini? Stok akan dikembalikan. Tindakan ini tidak dapat dibatalkan." - confirmLabel="Hapus" onConfirm={handleDelete} />
diff --git a/resources/js/pages/admin/manage/restock/restock-card.tsx b/resources/js/pages/admin/manage/restock/restock-card.tsx index 7ae141f..cc69fd4 100644 --- a/resources/js/pages/admin/manage/restock/restock-card.tsx +++ b/resources/js/pages/admin/manage/restock/restock-card.tsx @@ -1,41 +1,12 @@ import { ChevronDown, Pencil, Trash2 } from 'lucide-react'; +import { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { formatDateTime, formatNumber } from '@/lib/format'; +import { formatCurrency } from '@/lib/utils'; import type { Restock, RestockStockType } from './columns'; -function formatDateTime(dateString: string): string { - const date = new Date(dateString); - - return date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); -} - -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(amount); -} - -function formatNumber(num: number): string { - return new Intl.NumberFormat('id-ID', { - maximumFractionDigits: 4, - }).format(num); -} - const STOCK_TYPE_CONFIG: Record< RestockStockType, { label: string; className: string } @@ -71,7 +42,9 @@ export function RestockCardRow({ const variantCount = items.length; const productNames = [ ...new Set( - items.map((item) => item.product_variant?.product?.name).filter(Boolean), + items + .map((item) => item.product_variant?.product?.name) + .filter(Boolean), ), ]; const totalQty = items.reduce( @@ -152,7 +125,7 @@ export function RestockCardRow({ {formatCurrency(restock.subtotal)} - + Total:{' '} {formatCurrency(restock.total)} @@ -160,36 +133,23 @@ export function RestockCardRow({ - -
- - - - - Edit - - - - - - - Hapus - - -
-
+ , + onClick: () => onEdit(restock), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => onDelete(restock), + }, + ]} + wrapperClassName="flex shrink-0 items-center gap-1" + />
diff --git a/resources/js/pages/admin/manage/restock/restock-sub-row.tsx b/resources/js/pages/admin/manage/restock/restock-sub-row.tsx index 45b8964..790d1ce 100644 --- a/resources/js/pages/admin/manage/restock/restock-sub-row.tsx +++ b/resources/js/pages/admin/manage/restock/restock-sub-row.tsx @@ -1,5 +1,4 @@ -import { useState } from 'react'; -import { ImagePreviewModal } from '@/components/image-preview-modal'; +import { ImagePreviewButton } from '@/components/image-preview-button'; import { Table, TableBody, @@ -8,48 +7,10 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; +import { formatNumber } from '@/lib/format'; +import { formatCurrency } from '@/lib/utils'; import type { Restock } from './columns'; -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(amount); -} - -function formatNumber(num: number): string { - return new Intl.NumberFormat('id-ID', { - maximumFractionDigits: 4, - }).format(num); -} - -function VariantPhotoPreview({ url, title }: { url: string; title: string }) { - const [open, setOpen] = useState(false); - - return ( - <> - - - - ); -} - export function RestockItemSubRow({ restock }: { restock: Restock }) { const items = restock.restock_items ?? []; @@ -89,8 +50,10 @@ export function RestockItemSubRow({ restock }: { restock: Restock }) { {item.product_variant?.photo_url ? ( - ) : ( diff --git a/resources/js/pages/admin/master/category/columns.tsx b/resources/js/pages/admin/master/category/columns.tsx index 4277fa0..f4ddccf 100644 --- a/resources/js/pages/admin/master/category/columns.tsx +++ b/resources/js/pages/admin/master/category/columns.tsx @@ -1,12 +1,6 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { RowActions } from '@/components/row-actions'; export type Category = { id: number; @@ -44,39 +38,22 @@ export function createCategoryColumns( const category = row.original; return ( - -
- - - - - Edit - - - - - - - - Hapus - - -
-
+ , + onClick: () => handleEdit(category), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(category), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/master/category/index.tsx b/resources/js/pages/admin/master/category/index.tsx index bcd7700..d2c0298 100644 --- a/resources/js/pages/admin/master/category/index.tsx +++ b/resources/js/pages/admin/master/category/index.tsx @@ -1,20 +1,16 @@ import { Form, Head, router } from '@inertiajs/react'; import { Plus } from 'lucide-react'; -import { useCallback, useState } from 'react'; -import { ConfirmDialog } from '@/components/confirm-dialog'; -import { DataTable } from '@/components/data-table'; +import { useState } from 'react'; import type { PaginationState } from '@/components/data-table'; +import { DataTable } from '@/components/data-table'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FormDialog } from '@/components/form-dialog'; import InputError from '@/components/input-error'; +import { PageHeader } from '@/components/page-header'; import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { useServerTable } from '@/hooks/use-server-table'; import { destroy, index as categoryIndex, @@ -39,7 +35,6 @@ export default function CategoryIndex({ categories, highlight }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); - const [search, setSearch] = useState(''); const pagination: PaginationState = { current_page: categories.current_page, @@ -48,54 +43,15 @@ export default function CategoryIndex({ categories, highlight }: Props) { total: categories.total, }; - function handlePageChange(page: number) { - router.get( - categoryIndex.url(), - { - page, - per_page: pagination.per_page, - search, - }, - { - preserveState: true, - replace: true, - }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - categoryIndex.url(), - { - page: 1, - per_page: perPage, - search, - }, - { - preserveState: true, - replace: true, - }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - categoryIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search: value, - }, - { - preserveState: true, - replace: true, - }, - ); - }, - [pagination.per_page], - ); + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => categoryIndex.url(), + pagination, + }); function handleDelete() { if (!deleting) { @@ -117,12 +73,10 @@ export default function CategoryIndex({ categories, highlight }: Props) {
-
-
-

- Kategori -

- {highlight && ( + Menampilkan kategori dari notifikasi.
- + ) + } + actions={ - -
setCreateOpen(false)} - > - {({ errors, processing }) => { - return ( - <> - - - Tambah Kategori - - -
-
- - - -
-
- - - - - - ); - }} -
-
-
-
+ } + /> + + setCreateOpen(false)} + > + {({ errors }) => ( +
+ + + +
+ )} +
+ + { + if (!open) { + setEditing(null); + } + }} + title="Edit Kategori" + action={editing ? update(editing.id) : ''} + resetOnSuccess + onSuccess={() => setEditing(null)} + > + {({ errors }) => + editing && ( +
+ + + +
+ ) + } +
- { - if (!open) { - setEditing(null); - } - }} - > - - {editing && ( -
setEditing(null)} - > - {({ errors, processing }) => { - return ( - <> - - - Edit Kategori - - -
-
- - - -
-
- - - - - - ); - }} -
- )} -
-
- - { if (!open) { setDeleting(null); } }} title="Hapus Kategori" - description={`Apakah Anda yakin ingin menghapus kategori "${deleting?.name}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(category) => + `Apakah Anda yakin ingin menghapus kategori "${category.name}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDelete} />
diff --git a/resources/js/pages/admin/master/customer/columns.tsx b/resources/js/pages/admin/master/customer/columns.tsx index 37e85a9..3a1f7fe 100644 --- a/resources/js/pages/admin/master/customer/columns.tsx +++ b/resources/js/pages/admin/master/customer/columns.tsx @@ -1,12 +1,6 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { RowActions } from '@/components/row-actions'; export type Customer = { id: number; @@ -62,39 +56,22 @@ export function createCustomerColumns( const customer = row.original; return ( - -
- - - - - Edit - - - - - - - - Hapus - - -
-
+ , + onClick: () => handleEdit(customer), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(customer), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/master/customer/index.tsx b/resources/js/pages/admin/master/customer/index.tsx index 245417b..4ff5447 100644 --- a/resources/js/pages/admin/master/customer/index.tsx +++ b/resources/js/pages/admin/master/customer/index.tsx @@ -1,27 +1,23 @@ -import { ConfirmDialog } from '@/components/confirm-dialog'; +import { Head, router } from '@inertiajs/react'; +import { Plus } from 'lucide-react'; +import { useState } from 'react'; import type { PaginationState } from '@/components/data-table'; import { DataTable } from '@/components/data-table'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FormDialog } from '@/components/form-dialog'; import InputError from '@/components/input-error'; +import { PageHeader } from '@/components/page-header'; import { PhoneNumberInput } from '@/components/phone-number-input'; import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { useServerTable } from '@/hooks/use-server-table'; import { - index as customerIndex, destroy, + index as customerIndex, store, update, } from '@/routes/admin/master/customers'; -import { Form, Head, router } from '@inertiajs/react'; -import { Plus } from 'lucide-react'; -import { useCallback, useState } from 'react'; import type { Customer } from './columns'; import { createCustomerColumns } from './columns'; @@ -39,7 +35,7 @@ export default function CustomerIndex({ customers }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); - const [search, setSearch] = useState(''); + const pagination: PaginationState = { current_page: customers.current_page, last_page: customers.last_page, @@ -47,54 +43,15 @@ export default function CustomerIndex({ customers }: Props) { total: customers.total, }; - function handlePageChange(page: number) { - router.get( - customerIndex.url(), - { - page, - per_page: pagination.per_page, - search, - }, - { - preserveState: true, - replace: true, - }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - customerIndex.url(), - { - page: 1, - per_page: perPage, - search, - }, - { - preserveState: true, - replace: true, - }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - customerIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search: value, - }, - { - preserveState: true, - replace: true, - }, - ); - }, - [pagination.per_page], - ); + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => customerIndex.url(), + pagination, + }); function handleDelete() { if (!deleting) { @@ -116,13 +73,9 @@ export default function CustomerIndex({ customers }: Props) {
-
-
-

- Customer -

-
- + - -
setCreateOpen(false)} - > - {({ errors, processing }) => { - return ( - <> - - - Tambah Customer - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - - - - ); - }} -
-
-
-
+ } + /> + + setCreateOpen(false)} + > + {({ errors }) => ( +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ )} +
+ + { + if (!open) { + setEditing(null); + } + }} + title="Edit Customer" + action={editing ? update(editing.id) : ''} + resetOnSuccess + onSuccess={() => setEditing(null)} + > + {({ errors }) => + editing && ( +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ ) + } +
- { - if (!open) { - setEditing(null); - } - }} - > - - {editing && ( -
setEditing(null)} - > - {({ errors, processing }) => { - return ( - <> - - - Edit Customer - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - - - - ); - }} -
- )} -
-
- - { if (!open) { setDeleting(null); } }} title="Hapus Customer" - description={`Apakah Anda yakin ingin menghapus customer "${deleting?.name}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(customer) => + `Apakah Anda yakin ingin menghapus customer "${customer.name}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDelete} />
diff --git a/resources/js/pages/admin/master/product/columns.tsx b/resources/js/pages/admin/master/product/columns.tsx index 130629d..ef21bb0 100644 --- a/resources/js/pages/admin/master/product/columns.tsx +++ b/resources/js/pages/admin/master/product/columns.tsx @@ -1,14 +1,11 @@ -import type { ColumnDef } from '@tanstack/react-table'; import { router } from '@inertiajs/react'; +import type { ColumnDef } from '@tanstack/react-table'; import { ChevronRight, Pencil, Trash2 } from 'lucide-react'; +import { RowActions } from '@/components/row-actions'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { formatNumber } from '@/lib/format'; +import { formatCurrency } from '@/lib/utils'; export type ProductVariant = { id: number; @@ -58,23 +55,12 @@ function getStatusVariant(status: string): string { return variants[status] ?? 'bg-gray-100 text-gray-800'; } -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(amount); -} - -function formatNumber(num: number): string { - return new Intl.NumberFormat('id-ID').format(num); -} - function getFilteredVariants( allVariants: ProductVariant[], searchValue: string, ): ProductVariant[] { const query = searchValue.toLowerCase().trim(); + return query ? allVariants.filter((v) => v.name.toLowerCase().includes(query)) : allVariants; @@ -361,39 +347,22 @@ export function createProductColumns( const product = row.original; return ( - -
- - - - - Edit - - - - - - - - Hapus - - -
-
+ , + onClick: () => handleEdit(product), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(product), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/master/product/index.tsx b/resources/js/pages/admin/master/product/index.tsx index e544fd2..986cd67 100644 --- a/resources/js/pages/admin/master/product/index.tsx +++ b/resources/js/pages/admin/master/product/index.tsx @@ -1,9 +1,11 @@ import { Head, router } from '@inertiajs/react'; -import { Filter, Plus, X } from 'lucide-react'; -import { useCallback, useMemo, useState } from 'react'; +import { Plus } from 'lucide-react'; +import { useMemo, useState } from 'react'; import { CardTable } from '@/components/card-table'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FilterPopover } from '@/components/filter-popover'; import { useCardTableExpand } from '@/components/hooks/use-card-table-expand'; -import { ConfirmDialog } from '@/components/confirm-dialog'; +import { PageHeader } from '@/components/page-header'; import { Button } from '@/components/ui/button'; import { Combobox, @@ -13,11 +15,6 @@ import { ComboboxItem, ComboboxList, } from '@/components/ui/combobox'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; import { Select, SelectContent, @@ -25,6 +22,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { useServerTable } from '@/hooks/use-server-table'; import { destroy, create as productCreate, @@ -66,11 +64,7 @@ export default function ProductIndex({ products, categories, filters }: Props) { product: Product; variant: ProductVariant; } | null>(null); - const [filterOpen, setFilterOpen] = useState(false); - const [search, setSearch] = useState(''); const expand = useCardTableExpand(true); - const hasActiveFilters = - filters.status || filters.name || filters.stock || filters.category; const pagination = { current_page: products.current_page, @@ -79,87 +73,33 @@ export default function ProductIndex({ products, categories, filters }: Props) { total: products.total, }; + const { + search, + filterOpen, + setFilterOpen, + handlePageChange, + handlePerPageChange, + handleSearchChange, + applyFilter, + clearFilters, + } = useServerTable({ + route: () => productIndex.url(), + pagination, + filters, + filterWithParams: false, + }); + const productNames = useMemo(() => { const names = products.data.map((p) => p.name); + return [...new Set(names)].sort(); }, [products.data]); const selectedCategory = useMemo( - () => - categories.find((c) => String(c.id) === filters.category) ?? null, + () => categories.find((c) => String(c.id) === filters.category) ?? null, [categories, filters.category], ); - function applyFilter(key: string, value: string) { - const newFilters = { ...filters }; - - if (value === '' || value === 'all') { - delete newFilters[key as keyof typeof newFilters]; - } else { - newFilters[key as keyof typeof newFilters] = value; - } - - router.get(productIndex(), newFilters, { - preserveState: true, - replace: true, - }); - } - - function clearFilters() { - router.get( - productIndex(), - {}, - { - preserveState: true, - replace: true, - }, - ); - setFilterOpen(false); - } - - function handlePageChange(page: number) { - router.get( - productIndex.url(), - { - page, - per_page: pagination.per_page, - search, - ...filters, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - productIndex.url(), - { - page: 1, - per_page: perPage, - search, - ...filters, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - productIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search: value, - ...filters, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page, filters], - ); - function handleDelete() { if (!deleting) { return; @@ -187,153 +127,117 @@ export default function ProductIndex({ products, categories, filters }: Props) { } const filterToolbar = ( - - - - - -
-
- Filter - {hasActiveFilters && ( - - )} -
+ +
+ + + applyFilter('name', (value as string) ?? '') + } + > + + + + Tidak ada produk ditemukan. + + + {(name) => ( + {name} + )} + + + +
-
- - - applyFilter('name', (value as string) ?? '') - } - > - - - - Tidak ada produk ditemukan. - - - {(name) => ( - - {name} - - )} - - - -
+
+ + +
-
- - -
+
+ + + Berdasarkan stok bagus + + +
-
- - - Berdasarkan stok bagus - - -
- -
- - cat.name} - value={selectedCategory} - onValueChange={(value) => - applyFilter( - 'category', - value ? String(value.id) : '', - ) - } - > - - - - Tidak ada kategori ditemukan. - - - {(cat) => ( - - {cat.name} - - )} - - - -
-
-
-
+
+ + cat.name} + value={selectedCategory} + onValueChange={(value) => + applyFilter('category', value ? String(value.id) : '') + } + > + + + + Tidak ada kategori ditemukan. + + + {(cat) => ( + + {cat.name} + + )} + + + +
+ ); return ( @@ -341,19 +245,17 @@ export default function ProductIndex({ products, categories, filters }: Props) {
-
-
-

- Produk -

-
- -
+ + + + Tambah + + + } + /> - { if (!open) { setDeleting(null); } }} title="Hapus Produk" - description={`Apakah Anda yakin ingin menghapus produk "${deleting?.name}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(product) => + `Apakah Anda yakin ingin menghapus produk "${product.name}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDelete} /> - { if (!open) { setDeletingVariant(null); } }} title="Hapus Varian" - description={`Apakah Anda yakin ingin menghapus varian "${deletingVariant?.variant.name}" dari produk "${deletingVariant?.product.name}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(target) => + `Apakah Anda yakin ingin menghapus varian "${target.variant.name}" dari produk "${target.product.name}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDeleteVariant} />
diff --git a/resources/js/pages/admin/master/product/product-card.tsx b/resources/js/pages/admin/master/product/product-card.tsx index 6276895..da47fb2 100644 --- a/resources/js/pages/admin/master/product/product-card.tsx +++ b/resources/js/pages/admin/master/product/product-card.tsx @@ -1,26 +1,18 @@ -import { router } from '@inertiajs/react'; import { ChevronDown, Pencil, Trash2 } from 'lucide-react'; +import { RowActions } from '@/components/row-actions'; +import { ToggleStatus } from '@/components/toggle-status'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; -import { Switch } from '@/components/ui/switch'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { formatNumber } from '@/lib/format'; import type { Product } from './columns'; -function formatNumber(num: number): string { - return new Intl.NumberFormat('id-ID').format(num); -} - function getStatusLabel(status: string): string { const labels: Record = { active: 'Aktif', inactive: 'Non Aktif', draft: 'Draft', }; + return labels[status] ?? status; } @@ -30,6 +22,7 @@ function getStatusVariant(status: string): string { inactive: 'bg-red-100 text-red-800', draft: 'bg-yellow-100 text-yellow-800', }; + return variants[status] ?? 'bg-gray-100 text-gray-800'; } @@ -68,10 +61,6 @@ export function ProductCardRow({ product.status === 'active' || product.status === 'inactive'; const isChecked = product.status === 'active'; - function handleToggle() { - router.post(toggleStatusUrl(product.id), {}, { preserveScroll: true }); - } - return ( @@ -138,18 +127,11 @@ export function ProductCardRow({
{isToggleable ? ( -
- - - {getStatusLabel(product.status)} - -
+ ) : (
- -
- - - - - Edit - - - - - - - Hapus - - -
-
+ , + onClick: () => onEdit(product), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => onDelete(product), + }, + ]} + wrapperClassName="flex shrink-0 items-center gap-1" + />
diff --git a/resources/js/pages/admin/master/product/variant/stock-mutations.tsx b/resources/js/pages/admin/master/product/variant/stock-mutations.tsx index a81c605..4a6b0f1 100644 --- a/resources/js/pages/admin/master/product/variant/stock-mutations.tsx +++ b/resources/js/pages/admin/master/product/variant/stock-mutations.tsx @@ -1,7 +1,3 @@ -import { Button } from '@/components/ui/button'; -import { Card, CardContent } from '@/components/ui/card'; -import { useInfiniteScroll } from '@/hooks/use-infinite-scroll'; -import { stockMutations } from '@/routes/admin/master/products/variants'; import { Head } from '@inertiajs/react'; import { ArrowDown, @@ -13,8 +9,12 @@ import { Pencil, ScrollText, } from 'lucide-react'; - +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { useInfiniteScroll } from '@/hooks/use-infinite-scroll'; +import { formatNumber } from '@/lib/format'; import { index as productIndex } from '@/routes/admin/master/products'; +import { stockMutations } from '@/routes/admin/master/products/variants'; type Mutation = { id: number; @@ -47,10 +47,6 @@ type Props = { mutations: PaginatedMutations; }; -function formatNumber(num: number): string { - return new Intl.NumberFormat('id-ID').format(num); -} - function formatDate(dateStr: string): string { return new Intl.DateTimeFormat('id-ID', { day: 'numeric', @@ -67,6 +63,7 @@ function getQualityLabel(quality: string): string { reject: 'Reject', retail: 'Ecer', }; + return labels[quality] ?? quality; } @@ -76,6 +73,7 @@ function getQualityColor(quality: string): string { reject: 'bg-red-100 text-red-800', retail: 'bg-blue-100 text-blue-800', }; + return colors[quality] ?? 'bg-gray-100 text-gray-800'; } @@ -83,14 +81,27 @@ function getTypeLabel(type: string, quantity: number): string { if (type === 'in') { return quantity >= 0 ? 'Penambahan' : 'Pengurangan'; } + return quantity < 0 ? 'Pengurangan' : 'Penambahan'; } function getMutationTitle(description: string | null): string { - if (!description) return 'Perubahan Stok'; - if (description.includes('Transfer stok')) return 'Transfer Stok'; - if (description.includes('Stok awal')) return 'Stok Awal'; - if (description.includes('Penyesuaian stok')) return 'Edit Varian'; + if (!description) { + return 'Perubahan Stok'; + } + + if (description.includes('Transfer stok')) { + return 'Transfer Stok'; + } + + if (description.includes('Stok awal')) { + return 'Stok Awal'; + } + + if (description.includes('Penyesuaian stok')) { + return 'Edit Varian'; + } + return 'Perubahan Stok'; } @@ -98,12 +109,15 @@ function getMutationIcon(description: string | null): React.ReactNode { if (description?.includes('Transfer stok')) { return ; } + if (description?.includes('Stok awal')) { return ; } + if (description?.includes('Penyesuaian stok')) { return ; } + return ; } @@ -114,7 +128,10 @@ export default function StockMutationsPage({ }: Props) { const { items, loading, hasNextPage, sentinelRef } = useInfiniteScroll({ initialData: mutations, - fetchUrl: stockMutations.url({ product: product.id, variant: variant.id }), + fetchUrl: stockMutations.url({ + product: product.id, + variant: variant.id, + }), }); return ( @@ -152,13 +169,17 @@ export default function StockMutationsPage({ ) : ( items.map((mutation) => { const isPositive = mutation.quantity > 0; - const qualityColor = getQualityColor(mutation.stock_quality); + const qualityColor = getQualityColor( + mutation.stock_quality, + ); return (
-
+
{isPositive ? ( ) : ( @@ -169,28 +190,54 @@ export default function StockMutationsPage({
- {getMutationTitle(mutation.description)} + {getMutationTitle( + mutation.description, + )} - - {getQualityLabel(mutation.stock_quality)} + + {getQualityLabel( + mutation.stock_quality, + )}
- {formatDate(mutation.created_at)} + {formatDate( + mutation.created_at, + )} - {mutation.user?.full_name ?? mutation.user?.username} + {mutation.user + ?.full_name ?? + mutation.user + ?.username}
- - {isPositive ? '+' : ''}{formatNumber(mutation.quantity)} + + {isPositive ? '+' : ''} + {formatNumber( + mutation.quantity, + )} - {formatNumber(mutation.stock_before)} → {formatNumber(mutation.stock_after)} + {formatNumber( + mutation.stock_before, + )}{' '} + →{' '} + {formatNumber( + mutation.stock_after, + )}
@@ -208,7 +255,10 @@ export default function StockMutationsPage({ )} {hasNextPage && ( -
+
{loading && (
diff --git a/resources/js/pages/admin/master/product/variant/sub-row.tsx b/resources/js/pages/admin/master/product/variant/sub-row.tsx index b394060..f6f44e4 100644 --- a/resources/js/pages/admin/master/product/variant/sub-row.tsx +++ b/resources/js/pages/admin/master/product/variant/sub-row.tsx @@ -1,7 +1,7 @@ import { ArrowRightLeft, Pencil, ScrollText, Trash2 } from 'lucide-react'; import { useState } from 'react'; -import { ImagePreviewModal } from '@/components/image-preview-modal'; -import { Button } from '@/components/ui/button'; +import { ImagePreviewButton } from '@/components/image-preview-button'; +import { RowActions } from '@/components/row-actions'; import { Table, TableBody, @@ -10,59 +10,12 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import type { Product, ProductVariant } from '../columns'; +import { formatNumber } from '@/lib/format'; +import { formatCurrency } from '@/lib/utils'; import { stockMutations } from '@/routes/admin/master/products/variants'; +import type { Product, ProductVariant } from '../columns'; import { TransferStockDialog } from './transfer-stock-dialog'; -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(amount); -} - -function formatNumber(num: number): string { - return new Intl.NumberFormat('id-ID').format(num); -} - -function VariantPhotoPreview({ urls, title }: { urls: string[]; title: string }) { - const [open, setOpen] = useState(false); - - return ( - <> - - - - ); -} - export function VariantSubRow({ product, onEditVariant, @@ -119,8 +72,8 @@ export function VariantSubRow({ {variant.photo_urls?.length > 0 ? ( - ) : ( @@ -161,86 +114,56 @@ export function VariantSubRow({ )} - -
- - - - - - Transfer Stok - - - - - - - - Mutasi Stok - - - - - - - - Edit - - - - - - - - Hapus - - -
-
+ + ), + onClick: () => + setTransferVariant({ + product, + variant, + }), + }, + { + label: 'Mutasi Stok', + icon: ( + + ), + onClick: () => { + window.location.href = + stockMutations.url({ + product: product.id, + variant: variant.id, + }); + }, + }, + { + label: 'Edit', + icon: ( + + ), + onClick: () => + onEditVariant( + product, + variant, + ), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => + onDeleteVariantClick( + product, + variant, + ), + }, + ]} + />
)) diff --git a/resources/js/pages/admin/master/raw-material/create.tsx b/resources/js/pages/admin/master/raw-material/create.tsx index e5d70a2..d471189 100644 --- a/resources/js/pages/admin/master/raw-material/create.tsx +++ b/resources/js/pages/admin/master/raw-material/create.tsx @@ -1,3 +1,13 @@ +import { Form, Head, usePage } from '@inertiajs/react'; +import { + ArrowLeft, + Check, + ClipboardPaste, + Copy, + Plus, + Trash2, +} from 'lucide-react'; +import { useCallback, useRef, useState } from 'react'; import { ConfirmDialog } from '@/components/confirm-dialog'; import { FileUpload } from '@/components/file-upload'; import InputError from '@/components/input-error'; @@ -9,25 +19,13 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { useRawMaterialDraftSave } from '@/hooks/use-raw-material-draft'; +import { UNITS } from '@/lib/constants'; import { loadRawMaterialDraft } from '@/lib/raw-material-draft'; import { getTemporaryUrl } from '@/lib/upload'; -import { index as rawMaterialIndex, store } from '@/routes/admin/master/raw-materials'; -import { Form, Head, usePage } from '@inertiajs/react'; import { - ArrowLeft, - Check, - ClipboardPaste, - Copy, - Plus, - Trash2, -} from 'lucide-react'; -import { useCallback, useRef, useState } from 'react'; - -const UNITS = [ - { value: 'kg', label: 'Kilogram' }, - { value: 'meter', label: 'Meter' }, - { value: 'yard', label: 'Yard' }, -]; + index as rawMaterialIndex, + store, +} from '@/routes/admin/master/raw-materials'; type VariantState = { variant: string; @@ -110,6 +108,7 @@ export default function RawMaterialCreate() { setVariants((prev) => { const updated = [...prev]; (updated[index] as Record)[field] = value; + return updated; }); }, @@ -118,7 +117,9 @@ export default function RawMaterialCreate() { const [copiedIndex, setCopiedIndex] = useState(null); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); - const [deleteVariantIndex, setDeleteVariantIndex] = useState(null); + const [deleteVariantIndex, setDeleteVariantIndex] = useState( + null, + ); const confirmRemoveVariant = useCallback((index: number) => { setDeleteVariantIndex(index); @@ -131,6 +132,7 @@ export default function RawMaterialCreate() { navigator.clipboard.writeText(String(price)); setCopiedIndex(variantIndex); setTimeout(() => setCopiedIndex(null), 1500); + return prev; }); }, []); @@ -139,10 +141,15 @@ export default function RawMaterialCreate() { navigator.clipboard.readText().then((text) => { try { const price = Number(text); + if (!isNaN(price)) { setVariants((prev) => { const updated = [...prev]; - updated[variantIndex] = { ...updated[variantIndex], price }; + updated[variantIndex] = { + ...updated[variantIndex], + price, + }; + return updated; }); } @@ -155,6 +162,7 @@ export default function RawMaterialCreate() { const applyToAll = useCallback((variantIndex: number) => { setVariants((prev) => { const sourcePrice = prev[variantIndex].price; + return prev.map((v, i) => i === variantIndex ? v : { ...v, price: sourcePrice }, ); @@ -204,26 +212,35 @@ export default function RawMaterialCreate() {
- Informasi Bahan Baku + + Informasi Bahan Baku +
setName(e.target.value)} + onChange={(e) => + setName(e.target.value) + } placeholder="Masukkan nama bahan baku" />
{UNITS.map((u) => ( -
- -
- @@ -251,136 +276,224 @@ export default function RawMaterialCreate() { Varian Bahan Baku - {variants.map((variant, variantIndex) => ( -
-
-

- Varian {variantIndex + 1} -

-
- - - - {variantIndex > 0 && ( + {variants.map( + (variant, variantIndex) => ( +
+
+

+ Varian{' '} + {variantIndex + 1} +

+
- )} + + + {variantIndex > + 0 && ( + + )} +
+
+
+
+ + + updateVariant( + variantIndex, + 'variant', + e.target + .value, + ) + } + placeholder="Contoh: Ukuran L, Warna Merah" + /> + +
+
+ + + updateVariant( + variantIndex, + 'price', + val, + ) + } + /> + +
+
+ + + updateVariant( + variantIndex, + 'stock', + val, + ) + } + /> + +
-
-
- - updateVariant(variantIndex, 'variant', e.target.value) + - -
-
- - - updateVariant(variantIndex, 'price', val) - } - /> - -
-
- - + onChange={(key) => { updateVariant( variantIndex, - 'stock', - val, + 'photo', + key, + ); + updateVariant( + variantIndex, + 'photoUrl', + key + ? getTemporaryUrl( + key, + ) + : null, + ); + }} + folder="raw-material-variant" + existingUrl={ + variant.photoUrl + } + onUploadingChange={( + uploading, + ) => + updateVariant( + variantIndex, + 'uploading', + uploading, ) } />
-
- - { - updateVariant(variantIndex, 'photo', key); - updateVariant( - variantIndex, - 'photoUrl', - key ? getTemporaryUrl(key) : null, - ); - }} - folder="raw-material-variant" - existingUrl={variant.photoUrl} - onUploadingChange={(uploading) => - updateVariant(variantIndex, 'uploading', uploading) - } - /> - -
-
- ))} + ), + )} @@ -421,6 +537,7 @@ export default function RawMaterialCreate() { if (deleteVariantIndex !== null) { removeVariant(deleteVariantIndex); } + setDeleteConfirmOpen(false); setDeleteVariantIndex(null); }} diff --git a/resources/js/pages/admin/master/raw-material/edit.tsx b/resources/js/pages/admin/master/raw-material/edit.tsx index 05782bf..cce2573 100644 --- a/resources/js/pages/admin/master/raw-material/edit.tsx +++ b/resources/js/pages/admin/master/raw-material/edit.tsx @@ -1,3 +1,13 @@ +import { Form, Head, usePage } from '@inertiajs/react'; +import { + ArrowLeft, + Check, + ClipboardPaste, + Copy, + Plus, + Trash2, +} from 'lucide-react'; +import { useCallback, useRef, useState } from 'react'; import { ConfirmDialog } from '@/components/confirm-dialog'; import { FileUpload } from '@/components/file-upload'; import InputError from '@/components/input-error'; @@ -9,27 +19,15 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { useRawMaterialDraftSave } from '@/hooks/use-raw-material-draft'; +import { UNITS } from '@/lib/constants'; import { clearRawMaterialDraft } from '@/lib/raw-material-draft'; import { getTemporaryUrl } from '@/lib/upload'; -import { index as rawMaterialIndex, update } from '@/routes/admin/master/raw-materials'; -import { Form, Head, usePage } from '@inertiajs/react'; import { - ArrowLeft, - Check, - ClipboardPaste, - Copy, - Plus, - Trash2, -} from 'lucide-react'; -import { useCallback, useRef, useState } from 'react'; + index as rawMaterialIndex, + update, +} from '@/routes/admin/master/raw-materials'; import type { RawMaterialForEdit, RawMaterialVariantForEdit } from './columns'; -const UNITS = [ - { value: 'kg', label: 'Kilogram' }, - { value: 'meter', label: 'Meter' }, - { value: 'yard', label: 'Yard' }, -]; - type Props = { rawMaterial: RawMaterialForEdit; }; @@ -124,6 +122,7 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { setVariants((prev) => { const updated = [...prev]; (updated[index] as Record)[field] = value; + return updated; }); }, @@ -132,7 +131,9 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { const [copiedIndex, setCopiedIndex] = useState(null); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); - const [deleteVariantIndex, setDeleteVariantIndex] = useState(null); + const [deleteVariantIndex, setDeleteVariantIndex] = useState( + null, + ); const confirmRemoveVariant = useCallback((index: number) => { setDeleteVariantIndex(index); @@ -145,6 +146,7 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { navigator.clipboard.writeText(String(price)); setCopiedIndex(variantIndex); setTimeout(() => setCopiedIndex(null), 1500); + return prev; }); }, []); @@ -153,10 +155,15 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { navigator.clipboard.readText().then((text) => { try { const price = Number(text); + if (!isNaN(price)) { setVariants((prev) => { const updated = [...prev]; - updated[variantIndex] = { ...updated[variantIndex], price }; + updated[variantIndex] = { + ...updated[variantIndex], + price, + }; + return updated; }); } @@ -169,6 +176,7 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { const applyToAll = useCallback((variantIndex: number) => { setVariants((prev) => { const sourcePrice = prev[variantIndex].price; + return prev.map((v, i) => i === variantIndex ? v : { ...v, price: sourcePrice }, ); @@ -220,26 +228,35 @@ export default function RawMaterialEdit({ rawMaterial }: Props) {
- Informasi Bahan Baku + + Informasi Bahan Baku +
setName(e.target.value)} + onChange={(e) => + setName(e.target.value) + } placeholder="Masukkan nama bahan baku" />
{UNITS.map((u) => ( -
- -
- @@ -267,141 +292,226 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { Varian Bahan Baku - {variants.map((variant, variantIndex) => ( -
-
-

- Varian {variantIndex + 1} -

-
- - - - {variantIndex > 0 && ( + {variants.map( + (variant, variantIndex) => ( +
+
+

+ Varian{' '} + {variantIndex + 1} +

+
- )} + + + {variantIndex > + 0 && ( + + )} +
-
-
-
- - - updateVariant(variantIndex, 'variant', e.target.value) - } - placeholder="Contoh: Ukuran L, Warna Merah" - /> - +
+
+ + + updateVariant( + variantIndex, + 'variant', + e.target + .value, + ) + } + placeholder="Contoh: Ukuran L, Warna Merah" + /> + +
+
+ + + updateVariant( + variantIndex, + 'price', + val, + ) + } + /> + +
+
+ + + updateVariant( + variantIndex, + 'stock', + val, + ) + } + /> + +
- - updateVariant(variantIndex, 'price', val) - } - /> - -
-
- - + onChange={(key) => { updateVariant( variantIndex, - 'stock', - val, + 'photo', + key, + ); + updateVariant( + variantIndex, + 'photoUrl', + key + ? getTemporaryUrl( + key, + ) + : null, + ); + }} + folder="raw-material-variant" + existingUrl={ + variant.photoUrl + } + onUploadingChange={( + uploading, + ) => + updateVariant( + variantIndex, + 'uploading', + uploading, ) } />
-
- - { - updateVariant(variantIndex, 'photo', key); - updateVariant( - variantIndex, - 'photoUrl', - key ? getTemporaryUrl(key) : null, - ); - }} - folder="raw-material-variant" - existingUrl={variant.photoUrl} - onUploadingChange={(uploading) => - updateVariant(variantIndex, 'uploading', uploading) - } - /> - -
-
- ))} + ), + )} @@ -442,6 +555,7 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { if (deleteVariantIndex !== null) { removeVariant(deleteVariantIndex); } + setDeleteConfirmOpen(false); setDeleteVariantIndex(null); }} diff --git a/resources/js/pages/admin/master/raw-material/index.tsx b/resources/js/pages/admin/master/raw-material/index.tsx index f3d3e29..a0f4f67 100644 --- a/resources/js/pages/admin/master/raw-material/index.tsx +++ b/resources/js/pages/admin/master/raw-material/index.tsx @@ -1,15 +1,12 @@ import { Head, router } from '@inertiajs/react'; -import { Filter, Plus, X } from 'lucide-react'; -import { useCallback, useState } from 'react'; +import { Plus } from 'lucide-react'; +import { useState } from 'react'; import { CardTable } from '@/components/card-table'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FilterPopover } from '@/components/filter-popover'; import { useCardTableExpand } from '@/components/hooks/use-card-table-expand'; -import { ConfirmDialog } from '@/components/confirm-dialog'; +import { PageHeader } from '@/components/page-header'; import { Button } from '@/components/ui/button'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; import { Select, SelectContent, @@ -17,6 +14,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { useServerTable } from '@/hooks/use-server-table'; import { destroy, create as rawMaterialCreate, @@ -52,10 +50,7 @@ export default function RawMaterialIndex({ rawMaterials, filters }: Props) { rawMaterial: RawMaterial; variant: RawMaterialVariant; } | null>(null); - const [filterOpen, setFilterOpen] = useState(false); - const [search, setSearch] = useState(''); const expand = useCardTableExpand(true); - const hasActiveFilters = filters.is_active || filters.stock; const pagination = { current_page: rawMaterials.current_page, @@ -64,78 +59,26 @@ export default function RawMaterialIndex({ rawMaterials, filters }: Props) { total: rawMaterials.total, }; - function applyFilter(key: string, value: string) { - const newFilters = { ...filters }; - - if (value === '' || value === 'all') { - delete newFilters[key as keyof typeof newFilters]; - } else { - newFilters[key as keyof typeof newFilters] = value; - } - - router.get(rawMaterialIndex(), newFilters, { - preserveState: true, - replace: true, - }); - } - - function clearFilters() { - router.get( - rawMaterialIndex(), - {}, - { - preserveState: true, - replace: true, - }, - ); - setFilterOpen(false); - } - - function handlePageChange(page: number) { - router.get( - rawMaterialIndex.url(), - { - page, - per_page: pagination.per_page, - search, - ...filters, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - rawMaterialIndex.url(), - { - page: 1, - per_page: perPage, - search, - ...filters, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - rawMaterialIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search: value, - ...filters, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page, filters], - ); + const { + search, + filterOpen, + setFilterOpen, + handlePageChange, + handlePerPageChange, + handleSearchChange, + applyFilter, + clearFilters, + } = useServerTable({ + route: () => rawMaterialIndex.url(), + pagination, + filters, + filterWithParams: false, + }); function handleDelete() { - if (!deleting) return; + if (!deleting) { + return; + } router.delete(destroy.url(deleting.id), { onSuccess: () => setDeleting(null), @@ -143,7 +86,9 @@ export default function RawMaterialIndex({ rawMaterials, filters }: Props) { } function handleDeleteVariant() { - if (!deletingVariant) return; + if (!deletingVariant) { + return; + } router.delete( variantDestroy.url({ @@ -157,75 +102,49 @@ export default function RawMaterialIndex({ rawMaterials, filters }: Props) { } const filterToolbar = ( - - - - - -
-
- Filter - {hasActiveFilters && ( - - )} -
+ +
+ + +
-
- - -
- -
- - -
-
-
-
+
+ + +
+ ); return ( @@ -233,19 +152,17 @@ export default function RawMaterialIndex({ rawMaterials, filters }: Props) {
-
-
-

- Bahan Baku -

-
- -
+ + + + Tambah + + + } + /> { - window.location.href = rawMaterialEdit.url(rm.id); + window.location.href = rawMaterialEdit.url( + rm.id, + ); }} onDelete={(rm) => setDeleting(rm)} toggleStatusUrl={(id) => toggleStatus.url(id)} @@ -287,29 +206,31 @@ export default function RawMaterialIndex({ rawMaterials, filters }: Props) { )} /> - { if (!open) { setDeleting(null); } }} title="Hapus Bahan Baku" - description={`Apakah Anda yakin ingin menghapus bahan baku "${deleting?.name}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(rawMaterial) => + `Apakah Anda yakin ingin menghapus bahan baku "${rawMaterial.name}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDelete} /> - { if (!open) { setDeletingVariant(null); } }} title="Hapus Varian" - description={`Apakah Anda yakin ingin menghapus varian "${deletingVariant?.variant.variant}" dari bahan baku "${deletingVariant?.rawMaterial.name}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(target) => + `Apakah Anda yakin ingin menghapus varian "${target.variant.variant}" dari bahan baku "${target.rawMaterial.name}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDeleteVariant} />
diff --git a/resources/js/pages/admin/master/raw-material/raw-material-card.tsx b/resources/js/pages/admin/master/raw-material/raw-material-card.tsx index 0e24f94..826e28f 100644 --- a/resources/js/pages/admin/master/raw-material/raw-material-card.tsx +++ b/resources/js/pages/admin/master/raw-material/raw-material-card.tsx @@ -1,28 +1,12 @@ +import { ChevronDown, Pencil, Trash2 } from 'lucide-react'; +import { RowActions } from '@/components/row-actions'; +import { ToggleStatus } from '@/components/toggle-status'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; -import { Switch } from '@/components/ui/switch'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { router } from '@inertiajs/react'; -import { ChevronDown, Pencil, Trash2 } from 'lucide-react'; +import { formatNumber } from '@/lib/format'; +import { formatCurrency } from '@/lib/utils'; import type { RawMaterial } from './columns'; -function formatNumber(num: number): string { - return new Intl.NumberFormat('id-ID', { maximumFractionDigits: 4 }).format(num); -} - -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(amount); -} - export type RawMaterialCardRowParams = { rawMaterial: RawMaterial; index: number; @@ -43,12 +27,14 @@ export function RawMaterialCardRow({ toggleStatusUrl, }: RawMaterialCardRowParams) { const variants = rawMaterial.raw_material_prices ?? []; - const totalStock = variants.reduce((sum, v) => sum + (Number(v.stock) || 0), 0); - const totalValue = variants.reduce((sum, v) => sum + (Number(v.price) || 0) * (Number(v.stock) || 0), 0); - - function handleToggle() { - router.post(toggleStatusUrl(rawMaterial.id), {}, { preserveScroll: true }); - } + const totalStock = variants.reduce( + (sum, v) => sum + (Number(v.stock) || 0), + 0, + ); + const totalValue = variants.reduce( + (sum, v) => sum + (Number(v.price) || 0) * (Number(v.stock) || 0), + 0, + ); return ( @@ -94,51 +80,35 @@ export function RawMaterialCardRow({
-
- - - {rawMaterial.is_active ? 'Aktif' : 'Non Aktif'} - -
+
- -
- - - - - Edit - - - - - - - Hapus - - -
-
+ , + onClick: () => onEdit(rawMaterial), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => onDelete(rawMaterial), + }, + ]} + wrapperClassName="flex shrink-0 items-center gap-1" + />
diff --git a/resources/js/pages/admin/master/raw-material/variant/sub-row.tsx b/resources/js/pages/admin/master/raw-material/variant/sub-row.tsx index 59b1697..d23ebe0 100644 --- a/resources/js/pages/admin/master/raw-material/variant/sub-row.tsx +++ b/resources/js/pages/admin/master/raw-material/variant/sub-row.tsx @@ -2,8 +2,8 @@ import { router } from '@inertiajs/react'; import { Pencil, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { ConfirmDialog } from '@/components/confirm-dialog'; -import { ImagePreviewModal } from '@/components/image-preview-modal'; -import { Button } from '@/components/ui/button'; +import { ImagePreviewButton } from '@/components/image-preview-button'; +import { RowActions } from '@/components/row-actions'; import { Table, TableBody, @@ -12,52 +12,13 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; +import { formatNumber } from '@/lib/format'; +import { formatCurrency } from '@/lib/utils'; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; + edit as variantEdit, + destroy as variantDestroy, +} from '@/routes/admin/master/raw-materials/variants'; import type { RawMaterial, RawMaterialVariant } from '../columns'; -import { edit as variantEdit, destroy as variantDestroy } from '@/routes/admin/master/raw-materials/variants'; - -function formatCurrency(amount: number): string { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(amount); -} - -function formatNumber(num: number): string { - return new Intl.NumberFormat('id-ID', { maximumFractionDigits: 4 }).format(num); -} - -function VariantPhotoPreview({ url, title }: { url: string; title: string }) { - const [open, setOpen] = useState(false); - - return ( - <> - - - - ); -} export function RawMaterialVariantSubRow({ rawMaterial, @@ -65,10 +26,13 @@ export function RawMaterialVariantSubRow({ rawMaterial: RawMaterial; }) { const variants = rawMaterial.raw_material_prices ?? []; - const [deletingVariant, setDeletingVariant] = useState(null); + const [deletingVariant, setDeletingVariant] = + useState(null); function handleDeleteVariant() { - if (!deletingVariant) return; + if (!deletingVariant) { + return; + } router.delete( variantDestroy.url({ @@ -116,8 +80,8 @@ export function RawMaterialVariantSubRow({ {variant.photo_url ? ( - ) : ( @@ -136,45 +100,32 @@ export function RawMaterialVariantSubRow({ {formatNumber(variant.stock)} - -
- - - - - - Edit - - - - - - - - Hapus - - -
-
+ + ), + onClick: () => { + window.location.href = + variantEdit.url({ + rawMaterial: + rawMaterial.id, + variant: variant.id, + }); + }, + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => + setDeletingVariant(variant), + }, + ]} + />
)) diff --git a/resources/js/pages/admin/master/supplier/columns.tsx b/resources/js/pages/admin/master/supplier/columns.tsx index ddb16e7..8031874 100644 --- a/resources/js/pages/admin/master/supplier/columns.tsx +++ b/resources/js/pages/admin/master/supplier/columns.tsx @@ -1,12 +1,6 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { RowActions } from '@/components/row-actions'; export type Supplier = { id: number; @@ -62,39 +56,22 @@ export function createSupplierColumns( const supplier = row.original; return ( - -
- - - - - Edit - - - - - - - - Hapus - - -
-
+ , + onClick: () => handleEdit(supplier), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(supplier), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/master/supplier/index.tsx b/resources/js/pages/admin/master/supplier/index.tsx index 247e7ba..ae12027 100644 --- a/resources/js/pages/admin/master/supplier/index.tsx +++ b/resources/js/pages/admin/master/supplier/index.tsx @@ -1,27 +1,23 @@ -import { ConfirmDialog } from '@/components/confirm-dialog'; +import { Head, router } from '@inertiajs/react'; +import { Plus } from 'lucide-react'; +import { useState } from 'react'; import type { PaginationState } from '@/components/data-table'; import { DataTable } from '@/components/data-table'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { FormDialog } from '@/components/form-dialog'; import InputError from '@/components/input-error'; +import { PageHeader } from '@/components/page-header'; import { PhoneNumberInput } from '@/components/phone-number-input'; import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { useServerTable } from '@/hooks/use-server-table'; import { destroy, - store, index as supplierIndex, + store, update, } from '@/routes/admin/master/suppliers'; -import { Form, Head, router } from '@inertiajs/react'; -import { Plus } from 'lucide-react'; -import { useCallback, useState } from 'react'; import type { Supplier } from './columns'; import { createSupplierColumns } from './columns'; @@ -39,7 +35,7 @@ export default function SupplierIndex({ suppliers }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); - const [search, setSearch] = useState(''); + const pagination: PaginationState = { current_page: suppliers.current_page, last_page: suppliers.last_page, @@ -47,54 +43,15 @@ export default function SupplierIndex({ suppliers }: Props) { total: suppliers.total, }; - function handlePageChange(page: number) { - router.get( - supplierIndex.url(), - { - page, - per_page: pagination.per_page, - search, - }, - { - preserveState: true, - replace: true, - }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - supplierIndex.url(), - { - page: 1, - per_page: perPage, - search, - }, - { - preserveState: true, - replace: true, - }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - supplierIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search: value, - }, - { - preserveState: true, - replace: true, - }, - ); - }, - [pagination.per_page], - ); + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => supplierIndex.url(), + pagination, + }); function handleDelete() { if (!deleting) { @@ -116,13 +73,9 @@ export default function SupplierIndex({ suppliers }: Props) {
-
-
-

- Supplier -

-
- + - -
setCreateOpen(false)} - > - {({ errors, processing }) => { - return ( - <> - - - Tambah Supplier - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - - - - ); - }} -
-
-
-
+ } + /> + + setCreateOpen(false)} + > + {({ errors }) => ( +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ )} +
+ + { + if (!open) { + setEditing(null); + } + }} + title="Edit Supplier" + action={editing ? update(editing.id) : ''} + resetOnSuccess + onSuccess={() => setEditing(null)} + > + {({ errors }) => + editing && ( +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ ) + } +
- { - if (!open) { - setEditing(null); - } - }} - > - - {editing && ( -
setEditing(null)} - > - {({ errors, processing }) => { - return ( - <> - - - Edit Supplier - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - - - - ); - }} -
- )} -
-
- - { if (!open) { setDeleting(null); } }} title="Hapus Supplier" - description={`Apakah Anda yakin ingin menghapus supplier "${deleting?.name}"? Tindakan ini tidak dapat dibatalkan.`} - confirmLabel="Hapus" + description={(supplier) => + `Apakah Anda yakin ingin menghapus supplier "${supplier.name}"? Tindakan ini tidak dapat dibatalkan.` + } onConfirm={handleDelete} />
diff --git a/resources/js/pages/admin/roles/columns.tsx b/resources/js/pages/admin/roles/columns.tsx index a2d3d86..5d93b51 100644 --- a/resources/js/pages/admin/roles/columns.tsx +++ b/resources/js/pages/admin/roles/columns.tsx @@ -1,12 +1,6 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; +import { RowActions } from '@/components/row-actions'; export type Role = { id: number; @@ -60,37 +54,22 @@ export function createRoleColumns( const role = row.original; return ( - -
- - - - - Edit - - - - - - - - Hapus - - -
-
+ , + onClick: () => handleEdit(role), + }, + { + label: 'Hapus', + icon: ( + + ), + onClick: () => handleDeleteClick(role), + }, + ]} + /> ); }, }, diff --git a/resources/js/pages/admin/roles/index.tsx b/resources/js/pages/admin/roles/index.tsx index c2e0fec..7d3f884 100644 --- a/resources/js/pages/admin/roles/index.tsx +++ b/resources/js/pages/admin/roles/index.tsx @@ -1,10 +1,12 @@ import { Head, router } from '@inertiajs/react'; import { Plus } from 'lucide-react'; -import { useCallback, useState } from 'react'; -import { ConfirmDialog } from '@/components/confirm-dialog'; -import { DataTable } from '@/components/data-table'; +import { useState } from 'react'; import type { PaginationState } from '@/components/data-table'; +import { DataTable } from '@/components/data-table'; +import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog'; +import { PageHeader } from '@/components/page-header'; import { Button } from '@/components/ui/button'; +import { useServerTable } from '@/hooks/use-server-table'; import { index as rolesIndex, create as roleCreate, @@ -26,7 +28,7 @@ type Props = { export default function RoleIndex({ roles }: Props) { const [deleting, setDeleting] = useState(null); - const [search, setSearch] = useState(''); + const pagination: PaginationState = { current_page: roles.current_page, last_page: roles.last_page, @@ -34,45 +36,15 @@ export default function RoleIndex({ roles }: Props) { total: roles.total, }; - function handlePageChange(page: number) { - router.get( - rolesIndex.url(), - { - page, - per_page: pagination.per_page, - search, - }, - { preserveState: true, replace: true }, - ); - } - - function handlePerPageChange(perPage: number) { - router.get( - rolesIndex.url(), - { - page: 1, - per_page: perPage, - search, - }, - { preserveState: true, replace: true }, - ); - } - - const handleSearchChange = useCallback( - (value: string) => { - setSearch(value); - router.get( - rolesIndex.url(), - { - page: 1, - per_page: pagination.per_page, - search: value, - }, - { preserveState: true, replace: true }, - ); - }, - [pagination.per_page], - ); + const { + search, + handlePageChange, + handlePerPageChange, + handleSearchChange, + } = useServerTable({ + route: () => rolesIndex.url(), + pagination, + }); function handleDelete() { if (!deleting) { @@ -96,19 +68,17 @@ export default function RoleIndex({ roles }: Props) {
-
-
-

- Role & Permission -

-
- -
+ + + + Tambah + + + } + /> - { if (!open) { setDeleting(null); } }} title="Hapus Role" - description={`Apakah Anda yakin ingin menghapus role "${deleting?.name}"? Semua user dengan role ini akan kehilangan permission terkait.`} - confirmLabel="Hapus" + description={(role) => + `Apakah Anda yakin ingin menghapus role "${role.name}"? Semua user dengan role ini akan kehilangan permission terkait.` + } onConfirm={handleDelete} />