- Implemented CuttingIndex component for listing and managing cuttings. - Added routes for cutting management in web.php. - Created CuttingTest for testing cutting-related features including authorization, validation, and stock management. - Updated roles create and edit pages to include necessary imports. - Refactored settings and profile pages to streamline imports. - Enhanced permissions checks for cutting management actions.
241 lines
8.0 KiB
TypeScript
241 lines
8.0 KiB
TypeScript
import {
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
ChevronsLeft,
|
|
ChevronsRight,
|
|
Search,
|
|
} from 'lucide-react';
|
|
import * as React from 'react';
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
|
|
export interface PaginationState {
|
|
current_page: number;
|
|
last_page: number;
|
|
per_page: number;
|
|
total: number;
|
|
}
|
|
|
|
interface CardRenderContext<TData> {
|
|
item: TData;
|
|
index: number;
|
|
isExpanded: boolean;
|
|
onToggleExpand: () => void;
|
|
}
|
|
|
|
interface CardTableProps<TData> {
|
|
data: TData[];
|
|
getItemKey: (item: TData) => number | string;
|
|
|
|
renderCard: (ctx: CardRenderContext<TData>) => React.ReactNode;
|
|
renderSubContent: (item: TData) => React.ReactNode;
|
|
|
|
expandedKeys: Set<number | string> | 'all';
|
|
onToggleExpand: (key: number | string) => void;
|
|
|
|
searchPlaceholder?: string;
|
|
searchValue?: string;
|
|
onSearchChange?: (value: string) => void;
|
|
|
|
toolbar?: React.ReactNode;
|
|
|
|
pagination?: PaginationState;
|
|
onPageChange?: (page: number) => void;
|
|
onPerPageChange?: (perPage: number) => void;
|
|
|
|
emptyText?: string;
|
|
}
|
|
|
|
function useDebounce(callback: (value: string) => void, delay: number) {
|
|
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
return React.useCallback(
|
|
(value: string) => {
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
|
|
timeoutRef.current = setTimeout(() => {
|
|
callback(value);
|
|
}, delay);
|
|
},
|
|
[callback, delay],
|
|
);
|
|
}
|
|
|
|
export function CardTable<TData>({
|
|
data,
|
|
getItemKey,
|
|
renderCard,
|
|
renderSubContent,
|
|
expandedKeys,
|
|
onToggleExpand,
|
|
searchPlaceholder = 'Cari...',
|
|
searchValue,
|
|
onSearchChange,
|
|
toolbar,
|
|
pagination,
|
|
onPageChange,
|
|
onPerPageChange,
|
|
emptyText = 'Tidak ada data.',
|
|
}: CardTableProps<TData>) {
|
|
const [localSearch, setLocalSearch] = React.useState(searchValue ?? '');
|
|
|
|
React.useEffect(() => {
|
|
setLocalSearch(searchValue ?? '');
|
|
}, [searchValue]);
|
|
|
|
const isServerMode = !!pagination && !!onPageChange;
|
|
|
|
const handleSearchDebounced = useDebounce(
|
|
(value: string) => onSearchChange?.(value),
|
|
300,
|
|
);
|
|
|
|
function handleSearchChange(value: string) {
|
|
setLocalSearch(value);
|
|
|
|
if (isServerMode) {
|
|
handleSearchDebounced(value);
|
|
}
|
|
}
|
|
|
|
function isItemExpanded(key: number | string): boolean {
|
|
if (expandedKeys === 'all') {
|
|
return true;
|
|
}
|
|
|
|
return expandedKeys.has(key);
|
|
}
|
|
|
|
const totalPages = pagination?.last_page ?? 1;
|
|
const currentPage = pagination?.current_page ?? 1;
|
|
|
|
return (
|
|
<>
|
|
{(onSearchChange || toolbar || isServerMode) && (
|
|
<div className="flex items-center gap-2">
|
|
{onSearchChange && (
|
|
<div className="relative max-w-sm flex-1">
|
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
placeholder={searchPlaceholder}
|
|
value={localSearch}
|
|
onChange={(e) =>
|
|
handleSearchChange(e.target.value)
|
|
}
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
)}
|
|
{toolbar}
|
|
<div className="ml-auto flex items-center gap-2">
|
|
{isServerMode && onPerPageChange && (
|
|
<Select
|
|
value={String(pagination?.per_page ?? 25)}
|
|
onValueChange={(value) =>
|
|
onPerPageChange(Number(value))
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 w-[70px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="25">25</SelectItem>
|
|
<SelectItem value="50">50</SelectItem>
|
|
<SelectItem value="100">100</SelectItem>
|
|
<SelectItem value="999999">
|
|
Semua
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col gap-3">
|
|
{data.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="flex h-24 items-center justify-center text-muted-foreground">
|
|
{emptyText}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
data.map((item, index) => {
|
|
const key = getItemKey(item);
|
|
const isExpanded = isItemExpanded(key);
|
|
|
|
return (
|
|
<div key={key} className="flex flex-col">
|
|
{renderCard({
|
|
item,
|
|
index,
|
|
isExpanded,
|
|
onToggleExpand: () => onToggleExpand(key),
|
|
})}
|
|
{isExpanded && (
|
|
<div className="rounded-b-lg border border-t-0 bg-muted/30 p-4">
|
|
{renderSubContent(item)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{isServerMode && (
|
|
<div className="flex items-center justify-end gap-2">
|
|
<span className="text-sm text-muted-foreground">
|
|
Halaman {currentPage} dari {totalPages}
|
|
</span>
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange(1)}
|
|
disabled={currentPage <= 1}
|
|
>
|
|
<ChevronsLeft className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange(currentPage - 1)}
|
|
disabled={currentPage <= 1}
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange(currentPage + 1)}
|
|
disabled={currentPage >= totalPages}
|
|
>
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => onPageChange(totalPages)}
|
|
disabled={currentPage >= totalPages}
|
|
>
|
|
<ChevronsRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|