197 lines
5.5 KiB
TypeScript
197 lines
5.5 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
|
import { toast } from "sonner"
|
|
import { confirm } from "@/shared/components/confirm-dialog"
|
|
import type { DataViewChangeEvent, DataViewPaginationState, DataViewSorting } from "@/shared/data-view/table-view"
|
|
|
|
// ── Types ──
|
|
|
|
export type CrudDialogClient = {
|
|
list(query?: any): Promise<any>
|
|
create(payload: any): Promise<any>
|
|
update(id: string, payload: any): Promise<any>
|
|
destroy(id: string): Promise<any>
|
|
}
|
|
|
|
export type UseCrudDialogOptions<TClient extends CrudDialogClient> = {
|
|
queryKey: string[]
|
|
getClient: () => TClient
|
|
resourceLabel?: string
|
|
}
|
|
|
|
type CrudListShape = {
|
|
data?: unknown
|
|
meta?: {
|
|
last_page?: number
|
|
total?: number
|
|
}
|
|
pagination?: {
|
|
last_page?: number
|
|
total?: number
|
|
}
|
|
}
|
|
|
|
function normalizeCrudListResponse(response: unknown): {
|
|
items: any[]
|
|
meta?: { last_page?: number; total?: number }
|
|
} {
|
|
const root = (response ?? {}) as CrudListShape
|
|
const directData = root.data
|
|
|
|
if (Array.isArray(directData)) {
|
|
return { items: directData, meta: root.meta ?? root.pagination }
|
|
}
|
|
|
|
if (directData && typeof directData === "object") {
|
|
const nested = directData as CrudListShape
|
|
if (Array.isArray(nested.data)) {
|
|
return {
|
|
items: nested.data,
|
|
meta: nested.meta ?? nested.pagination ?? root.meta ?? root.pagination,
|
|
}
|
|
}
|
|
}
|
|
|
|
return { items: [], meta: root.meta ?? root.pagination }
|
|
}
|
|
|
|
// ── Hook ──
|
|
|
|
export function useCrudDialog<TClient extends CrudDialogClient>({
|
|
queryKey,
|
|
getClient,
|
|
resourceLabel = "item",
|
|
}: UseCrudDialogOptions<TClient>) {
|
|
const client = getClient()
|
|
const queryClient = useQueryClient()
|
|
|
|
// ── Local pagination state (no URL pollution) ──
|
|
const [page, setPage] = useState(1)
|
|
const [pageSize, setPageSize] = useState(10)
|
|
const [sortBy, setSortBy] = useState<string | null>(null)
|
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc" | null>(null)
|
|
|
|
// ── Form dialog state ──
|
|
const [isFormOpen, setIsFormOpen] = useState(false)
|
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
const [editingItem, setEditingItem] = useState<any>(null)
|
|
|
|
const fullQueryKey = [...queryKey, { page, pageSize, sortBy, sortOrder }]
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: fullQueryKey,
|
|
queryFn: async () => {
|
|
const params: Record<string, unknown> = { page, per_page: pageSize }
|
|
if (sortBy) params.sort_by = sortBy
|
|
if (sortOrder) params.sort_order = sortOrder
|
|
|
|
try {
|
|
return await client.list(params)
|
|
} catch {
|
|
// Some endpoints ignore/reject pagination params; retry without params.
|
|
return client.list()
|
|
}
|
|
},
|
|
})
|
|
|
|
const normalized = normalizeCrudListResponse(data)
|
|
const items = normalized.items
|
|
const meta = normalized.meta
|
|
|
|
const pagination: DataViewPaginationState = {
|
|
page,
|
|
pageSize,
|
|
pageCount: meta?.last_page ?? 1,
|
|
total: meta?.total ?? 0,
|
|
}
|
|
|
|
const sorting: DataViewSorting = sortBy
|
|
? [{ id: sortBy, desc: sortOrder === "desc" }]
|
|
: []
|
|
|
|
const handleChange = (event: DataViewChangeEvent) => {
|
|
switch (event.type) {
|
|
case "pagination":
|
|
setPage(event.pagination.page)
|
|
setPageSize(event.pagination.pageSize)
|
|
break
|
|
case "sorting": {
|
|
const sort = event.sorting[0]
|
|
setSortBy(sort?.id ?? null)
|
|
setSortOrder(sort ? (sort.desc ? "desc" : "asc") : null)
|
|
setPage(1)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
const invalidateQuery = () => {
|
|
queryClient.invalidateQueries({ queryKey })
|
|
}
|
|
|
|
const { mutateAsync: deleteItem } = useMutation({
|
|
mutationFn: (id: string) => {
|
|
const promise = client.destroy(id)
|
|
toast.promise(promise, {
|
|
loading: `Deleting ${resourceLabel}...`,
|
|
success: `${resourceLabel} deleted`,
|
|
error: `Failed to delete ${resourceLabel}`,
|
|
})
|
|
return promise
|
|
},
|
|
onSuccess: invalidateQuery,
|
|
})
|
|
|
|
const openCreate = () => {
|
|
setEditingId(null)
|
|
setEditingItem(null)
|
|
setIsFormOpen(true)
|
|
}
|
|
|
|
const openEdit = (row: any) => {
|
|
setEditingId(String(row.id))
|
|
setEditingItem(row)
|
|
setIsFormOpen(true)
|
|
}
|
|
|
|
const closeForm = () => {
|
|
setIsFormOpen(false)
|
|
setEditingId(null)
|
|
setEditingItem(null)
|
|
}
|
|
|
|
const handleDelete = async (row: any) => {
|
|
const confirmed = await confirm({
|
|
title: `Delete this ${resourceLabel}?`,
|
|
description: "This action cannot be undone.",
|
|
confirmLabel: "Delete",
|
|
variant: "destructive",
|
|
})
|
|
if (confirmed) await deleteItem(String(row.id))
|
|
}
|
|
|
|
const handleFormSuccess = () => {
|
|
invalidateQuery()
|
|
closeForm()
|
|
}
|
|
|
|
return {
|
|
items,
|
|
isLoading,
|
|
pagination,
|
|
sorting,
|
|
handleChange,
|
|
isFormOpen,
|
|
editingId,
|
|
editingItem,
|
|
openCreate,
|
|
openEdit,
|
|
closeForm,
|
|
handleDelete,
|
|
handleFormSuccess,
|
|
invalidateQuery,
|
|
}
|
|
}
|