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 && (
+
+ {Object.values(filters).filter(Boolean).length}
+
+ )}
+
+
+
+
+
+ Filter
+ {hasActiveFilters && (
+
+
+ Hapus Semua
+
+ )}
+
+ {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 (
+
+
+
+
+
+ );
+}
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 (
+ <>
+ setOpen(true)}
+ className={`relative block overflow-hidden rounded-md border bg-muted transition-opacity hover:opacity-80 ${className}`}
+ >
+
+ {srcs.length > 1 && (
+
+ {srcs.length}
+
+ )}
+
+
+ >
+ );
+}
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.icon}
+
+
+ ) : (
+
+ {action.icon}
+
+ )}
+
+
+ {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 (
-
-
-
-
- handleEdit(cashAccount)}
- >
-
-
-
- Edit
-
-
-
-
-
- handleDeleteClick(cashAccount)
- }
- >
-
-
-
-
- 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 && (
-
- {Object.values(filters).filter(Boolean).length}
-
- )}
-
-
-
-
-
- Filter
- {hasActiveFilters && (
-
-
- Hapus Semua
-
- )}
-
-
-
-
- Tipe Sumber
-
-
- applyFilter('type', value)
- }
- >
-
-
-
-
- Semua Tipe
- Deposit
-
- Withdrawal
-
-
- Pengeluaran
-
-
- Transfer
-
-
-
-
-
-
-
+
+
+
+ Tipe Sumber
+
+ applyFilter('type', value)}
+ >
+
+
+
+
+ Semua Tipe
+ Deposit
+ Withdrawal
+ Pengeluaran
+ Transfer
+
+
+
+
);
return (
@@ -278,29 +166,27 @@ export default function CashAccountIndex({
-
-
-
- Kas Toko
-
-
-
-
setDepositOpen(true)}
- >
-
- Deposit
-
-
setWithdrawalOpen(true)}
- >
-
- Withdrawal
-
-
-
+
+ setDepositOpen(true)}
+ >
+
+ Deposit
+
+ setWithdrawalOpen(true)}
+ >
+
+ Withdrawal
+
+
+ }
+ />
@@ -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'}
>
-
-
-
-
+ {({ errors }) => (
+ <>
+
+
+ Jumlah{' '}
+ *
+
+
+
+
+
+
+ Keterangan{' '}
+ *
+
+
+
+
+
+
+ Bukti{' '}
+ *
+
+
+
+
+
+
+
+ >
+ )}
+
- {
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'
+ }
>
-
-
-
-
+ {({ errors }) => (
+ <>
+
+
+ Jumlah{' '}
+ *
+
+
+
+
+
+
+ Keterangan{' '}
+ *
+
+
+
+
+
+
+ Bukti{' '}
+ *
+
+
+
+
+
+
+
+ >
+ )}
+
- {
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 && (
-
- )}
-
-
+ {({ errors }) =>
+ editing && (
+ <>
+
+
+ Jumlah{' '}
+
+ *
+
+
+
+
+
+
+
+ Keterangan{' '}
+
+ *
+
+
+
+
+
+
+
+ Bukti{' '}
+
+ *
+
+
+
+
+
+
+
+
+ >
+ )
+ }
+
- {
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 (
- <>
- setOpen(true)}
- className="block h-10 w-10 overflow-hidden rounded-md border bg-muted transition-opacity hover:opacity-80"
- >
-
-
-
- >
- );
-}
-
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 (
-
-
-
-
- handleEdit(transaction)}
- >
-
-
-
- Edit
-
-
-
-
-
- handleDeleteClick(transaction)
- }
- >
-
-
-
-
- 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' && (
-
-
-
- handleApprove(employeeAdvance)
- }
- >
-
-
-
-
- Setujui
-
-
- )}
-
- {employeeAdvance.status === 'approved' && (
-
-
-
- handlePay(employeeAdvance)
- }
- >
-
-
-
-
- Bayar
-
-
- )}
-
-
-
-
- handleEdit(employeeAdvance)
- }
- >
-
-
-
- Edit
-
-
-
-
-
- handleDeleteClick(employeeAdvance)
- }
- >
-
-
-
-
- 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(open);
+
+ if (!open) {
+ setDueDate(undefined);
+ }
+ }}
+ title="Tambah Kasbon"
+ action={store()}
+ resetOnSuccess
+ onSuccess={() => setCreateOpen(false)}
+ >
+ {({ errors }) => (
+ <>
+
+
+ Jumlah{' '}
+ *
+
+
+
+
+
+
+ Keterangan{' '}
+ *
+
+
+
+
+
+
+ Jatuh Tempo{' '}
+ *
+
+
+
+
+
+ >
+ )}
+
-
{
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 && (
-
- )}
-
-
+ {({ errors }) =>
+ editing && (
+ <>
+
+
+ Jumlah{' '}
+
+ *
+
+
+
+
+
+
+
+ Keterangan{' '}
+
+ *
+
+
+
+
+
+
+
+ Jatuh Tempo{' '}
+
+ *
+
+
+
+
+
+
+ >
+ )
+ }
+
-
{
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 (
- <>
- setOpen(true)}
- className="block h-10 w-10 overflow-hidden rounded-md border bg-muted transition-opacity hover:opacity-80"
- >
-
-
-
- >
- );
-}
-
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 (
-
-
-
-
- handleEdit(expense)}
- >
-
-
-
- Edit
-
-
-
-
-
- handleDeleteClick(expense)
- }
- >
-
-
-
-
- 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(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 }) => (
+ <>
+
+
+ Jumlah{' '}
+ *
+
+
+
+
+
+
+ Keterangan{' '}
+ *
+
+
+
+
+
+
+ Bukti{' '}
+ *
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
{
+ 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 && (
+ <>
+
+
+ Jumlah{' '}
+
+ *
+
+
+
+
+
+
+
+ Keterangan{' '}
+
+ *
+
+
+
+
+
+
+
+ Bukti{' '}
+
+ *
+
+
+
+
+
+
+
+
+ >
+ )
+ }
+
-
{
- if (!open) {
- setEditing(null);
- setEditReceiptKey(null);
- setEditFileMeta(null);
- }
- }}
- >
-
- {editing && (
-
- )}
-
-
-
-
{
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' && (
-
-
- handleClose(period)}
- >
-
-
-
-
- Tutup Periode
-
-
- )}
-
- {period.status === 'closed' && (
-
-
- handleReopen(period)}
- >
-
-
-
-
- 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) {
-
+
-
{
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' && (
- <>
-
-
-
- handleAddAdjustment(payroll)
- }
- >
-
-
-
-
- Tambah Adjustment
-
-
-
-
-
-
- handlePay(payroll)
- }
- >
-
-
-
-
- Tandai Dibayar
-
-
-
-
-
-
- handleCancel(payroll)
- }
- >
-
-
-
-
- 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 (
-
-
-
-
- handleEdit(employee)}
- >
-
-
-
- Edit
-
-
-
-
-
- handleResetPassword(employee)
- }
- >
-
-
-
-
- Reset Kata Sandi
-
-
-
-
-
-
- handleDeleteClick(employee)
- }
- >
-
-
-
-
- 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 && (
-
- {Object.values(filters).filter(Boolean).length}
-
- )}
-
-
-
-
-
- Filter
- {hasActiveFilters && (
-
-
- Hapus Semua
-
- )}
-
+
+
+
+ Status Karyawan
+
+
+ applyFilter('employment_status', value)
+ }
+ >
+
+
+
+
+ Semua Status
+ Full Time
+ Part Time
+ Kontrak
+ Magang
+ Keluar
+
+
+
-
-
- Status Karyawan
-
-
- applyFilter('employment_status', value)
- }
- >
-
-
-
-
-
- Semua Status
-
-
- Full Time
-
-
- Part Time
-
-
- Kontrak
-
-
- Magang
-
- Keluar
-
-
-
+
+
+ Status Aktif
+
+ applyFilter('is_active', value)}
+ >
+
+
+
+
+ Semua
+ Aktif
+ Tidak Aktif
+
+
+
-
-
- Status Aktif
-
-
- applyFilter('is_active', value)
- }
- >
-
-
-
-
- Semua
- Aktif
- Tidak Aktif
-
-
-
-
-
-
- Jenis Kelamin
-
-
- applyFilter('gender', value)
- }
- >
-
-
-
-
- Semua
- Laki-laki
-
- Perempuan
-
-
-
-
-
-
-
+
+
+ Jenis Kelamin
+
+ applyFilter('gender', value)}
+ >
+
+
+
+
+ Semua
+ Laki-laki
+ Perempuan
+
+
+
+
);
return (
@@ -285,19 +180,17 @@ export default function EmployeeIndex({ employees, filters }: Props) {
-
+
+
+
+ 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' && (
- <>
-
-
-
- handleApprove(leaveRequest)
- }
- >
-
-
-
-
- Setujui
-
-
-
-
-
-
- handleReject(leaveRequest)
- }
- >
-
-
-
-
- Tolak
-
-
- >
- )}
-
-
-
- handleEdit(leaveRequest)}
- >
-
-
-
- Edit
-
-
-
-
-
- handleDeleteClick(leaveRequest)
- }
- >
-
-
-
-
- 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 && (
-
- {Object.values(filters).filter(Boolean).length}
-
- )}
-
-
-
-
-
- Filter
- {hasActiveFilters && (
-
-
- Hapus Semua
-
- )}
-
-
-
-
- Status
-
-
- applyFilter('status', value)
- }
- >
-
-
-
-
-
- Semua Status
-
-
- Menunggu
-
-
- Disetujui
-
-
- Ditolak
-
-
- Dibatalkan
-
-
-
-
-
-
-
+
+
+ Status
+ applyFilter('status', value)}
+ >
+
+
+
+
+ Semua Status
+ Menunggu
+ Disetujui
+ Ditolak
+ Dibatalkan
+
+
+
+
);
return (
@@ -280,23 +169,9 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) {
-
-
-
- Cuti
-
-
-
{
- setCreateOpen(open);
-
- if (!open) {
- setStartDate(undefined);
- setEndDate(undefined);
- }
- }}
- >
+
-
-
-
-
-
+ }
+ />
+
+
{
+ setCreateOpen(open);
+
+ if (!open) {
+ setStartDate(undefined);
+ setEndDate(undefined);
+ }
+ }}
+ title="Tambah Permohonan Cuti"
+ action={store()}
+ resetOnSuccess
+ onSuccess={() => setCreateOpen(false)}
+ >
+ {({ errors }) => (
+ <>
+
+
+ Tanggal Mulai{' '}
+ *
+
+
+
+
+
+
+
+ Tanggal Selesai{' '}
+ *
+
+
+
+
+
+ >
+ )}
+
-
{
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 && (
-
- )}
-
-
+ {({ errors }) =>
+ editing && (
+ <>
+
+
+ Tanggal Mulai{' '}
+
+ *
+
+
+
+
+
+
+
+
+ Tanggal Selesai{' '}
+
+ *
+
+
+
+
+
+
+ >
+ )
+ }
+
-
{
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) {
-
+
+
+
+ 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 && (
-
setPhotoPreviewOpen(true)}
- className="block h-16 w-16 overflow-hidden rounded-md border transition-opacity hover:opacity-80"
- >
-
-
+
)}
-
-
-
-
- onEdit(purchase)}
- >
-
-
-
- Edit
-
-
-
- onDelete(purchase)}
- >
-
-
-
-
- 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 (
- <>
- setOpen(true)}
- className="block h-10 w-10 overflow-hidden rounded-md border transition-opacity hover:opacity-80"
- >
-
-
-
- >
- );
-}
-
-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) {
-
+
+
+
+ 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({
-
-
-
-
- onEdit(restock)}
- >
-
-
-
- Edit
-
-
-
- onDelete(restock)}
- >
-
-
-
-
- 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 (
- <>
- setOpen(true)}
- className="block h-10 w-10 overflow-hidden rounded-md border transition-opacity hover:opacity-80"
- >
-
-
-
- >
- );
-}
-
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 (
-
-
-
-
- handleEdit(category)}
- >
-
-
-
- Edit
-
-
-
-
-
- handleDeleteClick(category)
- }
- >
-
-
-
-
- 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 }) => (
+
+
+ Nama *
+
+
+
+
+ )}
+
+
+
{
+ if (!open) {
+ setEditing(null);
+ }
+ }}
+ title="Edit Kategori"
+ action={editing ? update(editing.id) : ''}
+ resetOnSuccess
+ onSuccess={() => setEditing(null)}
+ >
+ {({ errors }) =>
+ editing && (
+
+
+ Nama{' '}
+ *
+
+
+
+
+ )
+ }
+
-
{
- if (!open) {
- setEditing(null);
- }
- }}
- >
-
- {editing && (
-
- )}
-
-
-
-
{
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 (
-
-
-
-
- handleEdit(customer)}
- >
-
-
-
- Edit
-
-
-
-
-
- handleDeleteClick(customer)
- }
- >
-
-
-
-
- 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 }) => (
+
+
+
+ Nama{' '}
+ *
+
+
+
+
+
+
+ Alamat
+
+
+
+
+ )}
+
+
+
{
+ if (!open) {
+ setEditing(null);
+ }
+ }}
+ title="Edit Customer"
+ action={editing ? update(editing.id) : ''}
+ resetOnSuccess
+ onSuccess={() => setEditing(null)}
+ >
+ {({ errors }) =>
+ editing && (
+
+
+
+ Nama{' '}
+
+ *
+
+
+
+
+
+
+
+ Alamat
+
+
+
+
+ )
+ }
+
-
{
- if (!open) {
- setEditing(null);
- }
- }}
- >
-
- {editing && (
-
- )}
-
-
-
-
{
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 (
-
-
-
-
- handleEdit(product)}
- >
-
-
-
- Edit
-
-
-
-
-
- handleDeleteClick(product)
- }
- >
-
-
-
-
- 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 && (
-
- {Object.values(filters).filter(Boolean).length}
-
- )}
-
-
-
-
-
- Filter
- {hasActiveFilters && (
-
-
- Hapus Semua
-
- )}
-
+
+
+
+ Nama Produk
+
+
+ applyFilter('name', (value as string) ?? '')
+ }
+ >
+
+
+
+ Tidak ada produk ditemukan.
+
+
+ {(name) => (
+ {name}
+ )}
+
+
+
+
-
-
- Nama Produk
-
-
- applyFilter('name', (value as string) ?? '')
- }
- >
-
-
-
- Tidak ada produk ditemukan.
-
-
- {(name) => (
-
- {name}
-
- )}
-
-
-
-
+
+ Status
+ applyFilter('status', value)}
+ >
+
+
+
+
+ Semua Status
+ Aktif
+ Non Aktif
+ Draft
+
+
+
-
-
- Status
-
-
- applyFilter('status', value)
- }
- >
-
-
-
-
-
- Semua Status
-
- Aktif
-
- Non Aktif
-
- Draft
-
-
-
+
+ Stok
+
+ Berdasarkan stok bagus
+
+ applyFilter('stock', value)}
+ >
+
+
+
+
+ Semua Stok
+ Habis
+
+ Menipis (di bawah 10)
+
+
+
+
-
-
- Stok
-
-
- Berdasarkan stok bagus
-
-
- applyFilter('stock', value)
- }
- >
-
-
-
-
- Semua Stok
- Habis
-
- Menipis (di bawah 10)
-
-
-
-
-
-
-
- Kategori
-
- cat.name}
- value={selectedCategory}
- onValueChange={(value) =>
- applyFilter(
- 'category',
- value ? String(value.id) : '',
- )
- }
- >
-
-
-
- Tidak ada kategori ditemukan.
-
-
- {(cat) => (
-
- {cat.name}
-
- )}
-
-
-
-
-
-
-
+
+
+ Kategori
+
+ 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) {
-
+
+
+
+ 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)}
-
-
+
) : (
-
-
-
-
- onEdit(product)}
- >
-
-
-
- Edit
-
-
-
- onDelete(product)}
- >
-
-
-
-
- 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 (
- <>
-
setOpen(true)}
- className="relative block h-10 w-10 overflow-hidden rounded-md border bg-muted transition-opacity hover:opacity-80"
- >
-
- {urls.length > 1 && (
-
- {urls.length}
-
- )}
-
-
- >
- );
-}
-
export function VariantSubRow({
product,
onEditVariant,
@@ -119,8 +72,8 @@ export function VariantSubRow({
{variant.photo_urls?.length > 0 ? (
-
) : (
@@ -161,86 +114,56 @@ export function VariantSubRow({
)}
-
-
-
-
-
- setTransferVariant({
- product,
- variant,
- })
- }
- >
-
-
-
-
- Transfer Stok
-
-
-
-
- {
- window.location.href = stockMutations.url({
- product: product.id,
- variant: variant.id,
- });
- }}
- >
-
-
-
-
- Mutasi Stok
-
-
-
-
-
- onEditVariant(
- product,
- variant,
- )
- }
- >
-
-
-
-
- Edit
-
-
-
-
-
- onDeleteVariantClick(
- product,
- variant,
- )
- }
- >
-
-
-
-
- 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
+
Nama Bahan Baku{' '}
- *
+
+ *
+
setName(e.target.value)}
+ onChange={(e) =>
+ setName(e.target.value)
+ }
placeholder="Masukkan nama bahan baku"
/>
- Satuan *
+ Satuan{' '}
+
+ *
+
{UNITS.map((u) => (
-
-
-
+
+
+
{u.label}
@@ -242,7 +268,6 @@ export default function RawMaterialCreate() {
-
@@ -251,136 +276,224 @@ export default function RawMaterialCreate() {
Varian Bahan Baku
- {variants.map((variant, variantIndex) => (
-
-
-
- Varian {variantIndex + 1}
-
-
-
copyPrice(variantIndex)}
- >
- {copiedIndex === variantIndex ? (
-
- ) : (
-
- )}
- Salin Harga
-
-
pastePrice(variantIndex)}
- >
-
- Tempel Harga
-
-
applyToAll(variantIndex)}
- >
- Terapkan ke Semua
-
- {variantIndex > 0 && (
+ {variants.map(
+ (variant, variantIndex) => (
+
+
+
+ Varian{' '}
+ {variantIndex + 1}
+
+
confirmRemoveVariant(variantIndex)}
+ size="sm"
+ className="whitespace-nowrap"
+ onClick={() =>
+ copyPrice(
+ variantIndex,
+ )
+ }
>
-
+ {copiedIndex ===
+ variantIndex ? (
+
+ ) : (
+
+ )}
+ Salin Harga
- )}
+
+ pastePrice(
+ variantIndex,
+ )
+ }
+ >
+
+ Tempel Harga
+
+
+ applyToAll(
+ variantIndex,
+ )
+ }
+ >
+ Terapkan ke
+ Semua
+
+ {variantIndex >
+ 0 && (
+
+ confirmRemoveVariant(
+ variantIndex,
+ )
+ }
+ >
+
+
+ )}
+
+
+
+
+
+ Nama Varian{' '}
+
+ *
+
+
+
+ updateVariant(
+ variantIndex,
+ 'variant',
+ e.target
+ .value,
+ )
+ }
+ placeholder="Contoh: Ukuran L, Warna Merah"
+ />
+
+
+
+
+ Harga{' '}
+
+ *
+
+
+
+ updateVariant(
+ variantIndex,
+ 'price',
+ val,
+ )
+ }
+ />
+
+
+
+
+ Stok{' '}
+
+ *
+
+
+
+ updateVariant(
+ variantIndex,
+ 'stock',
+ val,
+ )
+ }
+ />
+
+
-
-
- Nama Varian{' '}
- *
+ Foto Varian{' '}
+
+ *
+
-
- updateVariant(variantIndex, 'variant', e.target.value)
+
-
-
-
-
- Harga *
-
-
- updateVariant(variantIndex, 'price', val)
- }
- />
-
-
-
-
- Stok *
-
-
+ 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,
)
}
/>
-
- Foto Varian *
- {
- updateVariant(variantIndex, 'photo', key);
- updateVariant(
- variantIndex,
- 'photoUrl',
- key ? getTemporaryUrl(key) : null,
- );
- }}
- folder="raw-material-variant"
- existingUrl={variant.photoUrl}
- onUploadingChange={(uploading) =>
- updateVariant(variantIndex, 'uploading', uploading)
- }
- />
-
-
-
- ))}
+ ),
+ )}
v.uploading)}
+ disabled={
+ processing ||
+ variants.some((v) => v.uploading)
+ }
>
{processing ? 'Menyimpan...' : 'Simpan'}
@@ -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
+
Nama Bahan Baku{' '}
- *
+
+ *
+
setName(e.target.value)}
+ onChange={(e) =>
+ setName(e.target.value)
+ }
placeholder="Masukkan nama bahan baku"
/>
- Satuan *
+ Satuan{' '}
+
+ *
+
{UNITS.map((u) => (
-
-
-
+
+
+
{u.label}
@@ -258,7 +284,6 @@ export default function RawMaterialEdit({ rawMaterial }: Props) {
-
@@ -267,141 +292,226 @@ export default function RawMaterialEdit({ rawMaterial }: Props) {
Varian Bahan Baku
- {variants.map((variant, variantIndex) => (
-
-
-
- Varian {variantIndex + 1}
-
-
-
copyPrice(variantIndex)}
- >
- {copiedIndex === variantIndex ? (
-
- ) : (
-
- )}
- Salin Harga
-
-
pastePrice(variantIndex)}
- >
-
- Tempel Harga
-
-
applyToAll(variantIndex)}
- >
- Terapkan ke Semua
-
- {variantIndex > 0 && (
+ {variants.map(
+ (variant, variantIndex) => (
+
+
+
+ Varian{' '}
+ {variantIndex + 1}
+
+
confirmRemoveVariant(variantIndex)}
+ size="sm"
+ className="whitespace-nowrap"
+ onClick={() =>
+ copyPrice(
+ variantIndex,
+ )
+ }
>
-
+ {copiedIndex ===
+ variantIndex ? (
+
+ ) : (
+
+ )}
+ Salin Harga
- )}
+
+ pastePrice(
+ variantIndex,
+ )
+ }
+ >
+
+ Tempel Harga
+
+
+ applyToAll(
+ variantIndex,
+ )
+ }
+ >
+ Terapkan ke
+ Semua
+
+ {variantIndex >
+ 0 && (
+
+ confirmRemoveVariant(
+ variantIndex,
+ )
+ }
+ >
+
+
+ )}
+
-
-
-
-
- Nama Varian{' '}
- *
-
-
- updateVariant(variantIndex, 'variant', e.target.value)
- }
- placeholder="Contoh: Ukuran L, Warna Merah"
- />
-
+
+
+
+ Nama Varian{' '}
+
+ *
+
+
+
+ updateVariant(
+ variantIndex,
+ 'variant',
+ e.target
+ .value,
+ )
+ }
+ placeholder="Contoh: Ukuran L, Warna Merah"
+ />
+
+
+
+
+ Harga{' '}
+
+ *
+
+
+
+ updateVariant(
+ variantIndex,
+ 'price',
+ val,
+ )
+ }
+ />
+
+
+
+
+ Stok{' '}
+
+ *
+
+
+
+ updateVariant(
+ variantIndex,
+ 'stock',
+ val,
+ )
+ }
+ />
+
+
- Harga *
+ Foto Varian{' '}
+
+ *
+
-
- updateVariant(variantIndex, 'price', val)
- }
- />
-
-
-
-
- Stok *
-
-
+ 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,
)
}
/>
-
-
- Foto Varian *
-
- {
- updateVariant(variantIndex, 'photo', key);
- updateVariant(
- variantIndex,
- 'photoUrl',
- key ? getTemporaryUrl(key) : null,
- );
- }}
- folder="raw-material-variant"
- existingUrl={variant.photoUrl}
- onUploadingChange={(uploading) =>
- updateVariant(variantIndex, 'uploading', uploading)
- }
- />
-
-
-
- ))}
+ ),
+ )}
v.uploading)}
+ disabled={
+ processing ||
+ variants.some((v) => v.uploading)
+ }
>
{processing ? 'Menyimpan...' : 'Simpan'}
@@ -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 && (
-
- {Object.values(filters).filter(Boolean).length}
-
- )}
-
-
-
-
-
- Filter
- {hasActiveFilters && (
-
-
- Hapus Semua
-
- )}
-
+
+
+ Status
+ applyFilter('is_active', value)}
+ >
+
+
+
+
+ Semua Status
+ Aktif
+ Non Aktif
+
+
+
-
-
- Status
-
- applyFilter('is_active', value)}
- >
-
-
-
-
- Semua Status
- Aktif
- Non Aktif
-
-
-
-
-
-
- Stok
-
- applyFilter('stock', value)}
- >
-
-
-
-
- Semua Stok
- Habis
- Menipis (di bawah 10)
-
-
-
-
-
-
+
+ Stok
+ applyFilter('stock', value)}
+ >
+
+
+
+
+ Semua Stok
+ Habis
+
+ Menipis (di bawah 10)
+
+
+
+
+
);
return (
@@ -233,19 +152,17 @@ export default function RawMaterialIndex({ rawMaterials, filters }: Props) {
-
+
+
+
+ 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'}
-
-
+
-
-
-
-
- onEdit(rawMaterial)}
- >
-
-
-
- Edit
-
-
-
- onDelete(rawMaterial)}
- >
-
-
-
-
- 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 (
- <>
- setOpen(true)}
- className="relative block h-10 w-10 overflow-hidden rounded-md border bg-muted transition-opacity hover:opacity-80"
- >
-
-
-
- >
- );
-}
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)}
-
-
-
-
- {
- window.location.href = variantEdit.url({
- rawMaterial: rawMaterial.id,
- variant: variant.id,
- });
- }}
- >
-
-
-
-
- Edit
-
-
-
-
-
- setDeletingVariant(variant)
- }
- >
-
-
-
-
- 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 (
-
-
-
-
- handleEdit(supplier)}
- >
-
-
-
- Edit
-
-
-
-
-
- handleDeleteClick(supplier)
- }
- >
-
-
-
-
- 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 }) => (
+
+
+
+ Nama{' '}
+ *
+
+
+
+
+
+
+ Alamat
+
+
+
+
+ )}
+
+
+
{
+ if (!open) {
+ setEditing(null);
+ }
+ }}
+ title="Edit Supplier"
+ action={editing ? update(editing.id) : ''}
+ resetOnSuccess
+ onSuccess={() => setEditing(null)}
+ >
+ {({ errors }) =>
+ editing && (
+
+
+
+ Nama{' '}
+
+ *
+
+
+
+
+
+
+
+ Alamat
+
+
+
+
+ )
+ }
+
-
{
- if (!open) {
- setEditing(null);
- }
- }}
- >
-
- {editing && (
-
- )}
-
-
-
-
{
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 (
-
-
-
-
- handleEdit(role)}
- >
-
-
-
- Edit
-
-
-
-
- handleDeleteClick(role)}
- >
-
-
-
-
- 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) {
-
+
+
+
+ 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}
/>