diff --git a/resources/js/pages/admin/finance/expense/hooks/use-expense-index.ts b/resources/js/pages/admin/finance/expense/hooks/use-expense-index.ts new file mode 100644 index 0000000..0c4e77d --- /dev/null +++ b/resources/js/pages/admin/finance/expense/hooks/use-expense-index.ts @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { Expense } from '@/types'; +import { router } from '@inertiajs/react'; +import expenseRoutes from '@/routes/expense'; +import { toast } from 'sonner'; + +export function useExpenseIndex() { + const [isFormOpen, setIsFormOpen] = useState(false); + const [selectedExpense, setSelectedExpense] = useState(null); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); + const [expenseToDelete, setExpenseToDelete] = useState(null); + const [rowsToDelete, setRowsToDelete] = useState([]); + const [rowSelection, setRowSelection] = useState({}); + const [selectedProof, setSelectedProof] = useState(null); + + const onAdd = () => { + setSelectedExpense(null); + setIsFormOpen(true); + }; + + const onEdit = (expense: Expense) => { + setSelectedExpense(expense); + setIsFormOpen(true); + }; + + const onDelete = (expense: Expense) => { + setExpenseToDelete(expense); + setIsDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + if (expenseToDelete) { + router.delete(expenseRoutes.destroy(expenseToDelete.id).url, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + setIsDeleteDialogOpen(false); + setExpenseToDelete(null); + setRowSelection({}); + }, + }); + } + }; + + const confirmBulkDelete = () => { + router.post(expenseRoutes.bulkDestroy().url, { + ids: rowsToDelete.map((row: any) => row.id), + _method: 'DELETE' + }, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + setIsBulkDeleteDialogOpen(false); + setRowsToDelete([]); + setRowSelection({}); + }, + }); + }; + + const closeForm = () => { + setIsFormOpen(false); + setTimeout(() => setSelectedExpense(null), 200); + }; + + const onPreviewImage = (url: string) => { + setSelectedProof(url); + }; + + return { + isFormOpen, + selectedExpense, + isDeleteDialogOpen, + isBulkDeleteDialogOpen, + expenseToDelete, + rowsToDelete, + rowSelection, + selectedProof, + setRowSelection, + setRowsToDelete, + setIsDeleteDialogOpen, + setIsBulkDeleteDialogOpen, + setSelectedProof, + onAdd, + onEdit, + onDelete, + confirmDelete, + confirmBulkDelete, + closeForm, + onPreviewImage, + }; +} diff --git a/resources/js/pages/admin/finance/expense/index.tsx b/resources/js/pages/admin/finance/expense/index.tsx index e5a1020..138cff4 100644 --- a/resources/js/pages/admin/finance/expense/index.tsx +++ b/resources/js/pages/admin/finance/expense/index.tsx @@ -1,26 +1,9 @@ -import { Head, useForm, router } from '@inertiajs/react'; +import { Head } from '@inertiajs/react'; import type { Expense } from '@/types'; -import { ColumnDef } from '@tanstack/react-table'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Trash2, Pencil, ImagePlus, X } from 'lucide-react'; +import { Trash2, X } from 'lucide-react'; import { DataTable } from '@/components/data-table'; -import { DataTableColumnHeader } from '@/components/data-table-column-header'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { Field, FieldGroup } from "@/components/ui/field" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { useState, useRef } from 'react'; -import { toast } from 'sonner'; -import { TooltipContent, TooltipTrigger } from '@radix-ui/react-tooltip'; -import { Tooltip } from '@/components/ui/tooltip'; -import expenseRoutes from '@/routes/expense'; import { AlertDialog, AlertDialogAction, @@ -32,232 +15,37 @@ import { AlertDialogMedia, AlertDialogTitle, } from "@/components/ui/alert-dialog" -import { NumericFormat } from 'react-number-format'; +import { Dialog, DialogContent } from "@/components/ui/dialog" + +import { useExpenseIndex } from './hooks/use-expense-index'; +import { getColumns } from './partials/columns'; +import { ExpenseFormModal } from './partials/expense-form-modal'; export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) { - const [isOpen, setIsOpen] = useState(false); - const [isEditing, setIsEditing] = useState(false); - const [selectedExpense, setSelectedExpense] = useState(null); + const { + isFormOpen, + selectedExpense, + isDeleteDialogOpen, + isBulkDeleteDialogOpen, + expenseToDelete, + rowsToDelete, + rowSelection, + selectedProof, + setRowSelection, + setRowsToDelete, + setIsDeleteDialogOpen, + setIsBulkDeleteDialogOpen, + setSelectedProof, + onAdd, + onEdit, + onDelete, + confirmDelete, + confirmBulkDelete, + closeForm, + onPreviewImage, + } = useExpenseIndex(); - const { data, setData, post, patch, processing, errors, reset, clearErrors } = useForm<{ - name: string; - amount: string; - image: File | null; - }>({ - name: '', - amount: '', - image: null, - }); - - const [imagePreview, setImagePreview] = useState(null); - const [selectedProof, setSelectedProof] = useState(null); - const fileInputRef = useRef(null); - - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); - const [expenseToDelete, setExpenseToDelete] = useState(null); - const [rowsToDelete, setRowsToDelete] = useState([]); - const [rowSelection, setRowSelection] = useState({}); - - const onEdit = (expense: Expense) => { - setIsEditing(true); - setSelectedExpense(expense); - setData({ - name: expense.name, - amount: expense.amount.toString(), - image: null, - }); - setImagePreview(expense.proof_url || null); - setIsOpen(true); - }; - - const onDelete = (expense: Expense) => { - setExpenseToDelete(expense); - setIsDeleteDialogOpen(true); - }; - - const confirmDelete = () => { - if (expenseToDelete) { - router.delete(expenseRoutes.destroy(expenseToDelete.id).url, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - setIsDeleteDialogOpen(false); - setExpenseToDelete(null); - setRowSelection({}); - }, - }); - } - }; - - const confirmBulkDelete = () => { - router.post(expenseRoutes.bulkDestroy().url, { - ids: rowsToDelete.map((row: any) => row.id), - _method: 'DELETE' - }, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - setIsBulkDeleteDialogOpen(false); - setRowsToDelete([]); - setRowSelection({}); - }, - }); - }; - - const closeModal = () => { - setIsOpen(false); - setTimeout(() => { - setIsEditing(false); - setSelectedExpense(null); - setImagePreview(null); - reset(); - clearErrors(); - }, 200); - }; - - const onImageChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - setData('image', file); - const reader = new FileReader(); - reader.onloadend = () => { - setImagePreview(reader.result as string); - }; - reader.readAsDataURL(file); - } - }; - - const removeImage = () => { - setData('image', null); - setImagePreview(null); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - }; - - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (isEditing && selectedExpense) { - router.post(expenseRoutes.update(selectedExpense.id).url, { - ...data, - _method: 'PATCH', - }, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - closeModal(); - }, - }); - } else { - post(expenseRoutes.store().url, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - closeModal(); - }, - }); - } - }; - - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(amount); - }; - - const columns: ColumnDef[] = [ - { - accessorKey: "proof_url", - header: "Bukti", - cell: ({ row }) => { - const url = row.original.proof_url; - return url ? ( - - ) : ( -
- -
- ); - }, - meta: { title: "Bukti" }, - }, - { - accessorKey: "user.name", - header: ({ column }) => { - return ( - - ) - }, - cell: ({ row }) => row.original.user?.name, - meta: { title: "Pegawai" }, - }, - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ) - }, - meta: { title: "Nama" }, - }, - { - accessorKey: "amount", - header: ({ column }) => { - return ( - - ) - }, - cell: ({ row }) => formatCurrency(row.original.amount), - meta: { title: "Nominal" }, - }, - { - accessorKey: "created_at", - header: ({ column }) => { - return ( - - ) - }, - cell: ({ row }) => new Date(row.original.created_at).toLocaleDateString('id-ID', { - day: '2-digit', - month: 'long', - year: 'numeric' - }), - meta: { title: "Tanggal" }, - }, - { - id: "actions", - header: "Aksi", - cell: ({ row }) => { - const expense = row.original; - return ( -
- - - - - -

Ubah

-
-
- - - - - -

Hapus

-
-
-
- ); - }, - meta: { title: "Aksi" }, - }, - ]; + const columns = getColumns({ onEdit, onDelete, onPreviewImage }); return (
@@ -267,98 +55,16 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) {

Pengeluaran

-
- !open && closeModal()}> - - - {isEditing ? 'Ubah Pengeluaran' : 'Tambah Pengeluaran'} - -
- - - - setData('name', e.target.value)} - autoComplete='off' - placeholder='Contoh: Bayar Listrik' - maxLength={100} - /> - {errors.name &&

{errors.name}

} -
- - - - { - setData('amount', values.value) - }} - placeholder="Rp 0" - autoComplete='off' - /> - {errors.amount &&

{errors.amount}

} -
- - - -
- {imagePreview ? ( -
- Preview - -
- ) : ( -
fileInputRef.current?.click()} - className="h-40 w-full flex flex-col items-center justify-center border-2 border-dashed rounded-lg cursor-pointer hover:bg-accent/50 transition-colors" - > - - Klik untuk upload bukti - PNG, JPG up to 5MB -
- )} - -
- {errors.image &&

{errors.image}

} -
-
- - - - -
-
-
+ setSelectedProof(null)}> @@ -400,6 +106,7 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) { + {/* Single Delete Confirmation */} @@ -418,6 +125,7 @@ export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) { + {/* Bulk Delete Confirmation */} diff --git a/resources/js/pages/admin/finance/expense/partials/columns.tsx b/resources/js/pages/admin/finance/expense/partials/columns.tsx new file mode 100644 index 0000000..ca73fa7 --- /dev/null +++ b/resources/js/pages/admin/finance/expense/partials/columns.tsx @@ -0,0 +1,107 @@ +import { ColumnDef } from '@tanstack/react-table'; +import { Expense } from '@/types'; +import { DataTableColumnHeader } from '@/components/data-table-column-header'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Button } from '@/components/ui/button'; +import { Pencil, Trash2, ImagePlus } from 'lucide-react'; + +interface ColumnProps { + onEdit: (expense: Expense) => void; + onDelete: (expense: Expense) => void; + onPreviewImage: (url: string) => void; +} + +const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(amount); +}; + +export const getColumns = ({ onEdit, onDelete, onPreviewImage }: ColumnProps): ColumnDef[] => [ + { + accessorKey: "proof_url", + header: "Bukti", + cell: ({ row }) => { + const url = row.original.proof_url; + return url ? ( + + ) : ( +
+ +
+ ); + }, + meta: { title: "Bukti" }, + }, + { + accessorKey: "user.name", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.user?.name, + meta: { title: "Pegawai" }, + }, + { + accessorKey: "name", + header: ({ column }) => ( + + ), + meta: { title: "Nama" }, + }, + { + accessorKey: "amount", + header: ({ column }) => ( + + ), + cell: ({ row }) => formatCurrency(row.original.amount), + meta: { title: "Nominal" }, + }, + { + accessorKey: "created_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => new Date(row.original.created_at).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric' + }), + meta: { title: "Tanggal" }, + }, + { + id: "actions", + header: "Aksi", + cell: ({ row }) => { + const expense = row.original; + return ( +
+ + + + + +

Ubah

+
+
+ + + + + +

Hapus

+
+
+
+ ); + }, + meta: { title: "Aksi" }, + }, +]; diff --git a/resources/js/pages/admin/finance/expense/partials/expense-form-modal.tsx b/resources/js/pages/admin/finance/expense/partials/expense-form-modal.tsx new file mode 100644 index 0000000..a2641f9 --- /dev/null +++ b/resources/js/pages/admin/finance/expense/partials/expense-form-modal.tsx @@ -0,0 +1,195 @@ +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Field, FieldGroup } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Expense } from '@/types'; +import { useForm, router } from '@inertiajs/react'; +import { useEffect, useState, useRef } from 'react'; +import expenseRoutes from '@/routes/expense'; +import { toast } from 'sonner'; +import { NumericFormat } from 'react-number-format'; +import { ImagePlus, X } from 'lucide-react'; + +interface ExpenseFormModalProps { + isOpen: boolean; + onClose: () => void; + expense: Expense | null; +} + +export function ExpenseFormModal({ isOpen, onClose, expense }: ExpenseFormModalProps) { + const isEditing = !!expense; + const fileInputRef = useRef(null); + const [imagePreview, setImagePreview] = useState(null); + + const { data, setData, post, processing, errors, reset, clearErrors } = useForm<{ + name: string; + amount: string; + image: File | null; + }>({ + name: '', + amount: '', + image: null, + }); + + useEffect(() => { + if (expense) { + setData({ + name: expense.name, + amount: expense.amount.toString(), + image: null, + }); + setImagePreview(expense.proof_url || null); + } else { + reset(); + setImagePreview(null); + } + }, [expense]); + + const handleClose = () => { + onClose(); + setTimeout(() => { + reset(); + setImagePreview(null); + clearErrors(); + }, 200); + }; + + const onImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + setData('image', file); + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + const removeImage = () => { + setData('image', null); + setImagePreview(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (isEditing && expense) { + router.post(expenseRoutes.update(expense.id).url, { + ...data, + _method: 'PATCH', + }, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + handleClose(); + }, + }); + } else { + post(expenseRoutes.store().url, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + handleClose(); + }, + }); + } + }; + + return ( + !open && handleClose()}> + + + {isEditing ? 'Ubah Pengeluaran' : 'Tambah Pengeluaran'} + +
+ + + + setData('name', e.target.value)} + autoComplete='off' + placeholder='Contoh: Bayar Listrik' + maxLength={100} + /> + {errors.name &&

{errors.name}

} +
+ + + + { + setData('amount', values.value) + }} + placeholder="Rp 0" + autoComplete='off' + /> + {errors.amount &&

{errors.amount}

} +
+ + + +
+ {imagePreview ? ( +
+ Preview + +
+ ) : ( +
fileInputRef.current?.click()} + className="h-40 w-full flex flex-col items-center justify-center border-2 border-dashed rounded-lg cursor-pointer hover:bg-accent/50 transition-colors" + > + + Klik untuk upload bukti + PNG, JPG up to 5MB +
+ )} + +
+ {errors.image &&

{errors.image}

} +
+
+ + + + +
+
+
+ ); +} diff --git a/resources/js/pages/admin/master/category/hooks/use-category-index.ts b/resources/js/pages/admin/master/category/hooks/use-category-index.ts new file mode 100644 index 0000000..1c44bbb --- /dev/null +++ b/resources/js/pages/admin/master/category/hooks/use-category-index.ts @@ -0,0 +1,89 @@ +import { useState } from 'react'; +import { Category } from '@/types'; +import { router } from '@inertiajs/react'; +import categoryRoutes from '@/routes/category'; +import { toast } from 'sonner'; + +export function useCategoryIndex() { + const [isFormOpen, setIsFormOpen] = useState(false); + const [selectedCategory, setSelectedCategory] = useState(null); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); + const [categoryToDelete, setCategoryToDelete] = useState(null); + const [rowsToDelete, setRowsToDelete] = useState([]); + const [rowSelection, setRowSelection] = useState({}); + + const onAdd = () => { + setSelectedCategory(null); + setIsFormOpen(true); + }; + + const onEdit = (category: Category) => { + setSelectedCategory(category); + setIsFormOpen(true); + }; + + const onDelete = (category: Category) => { + setCategoryToDelete(category); + setIsDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + if (categoryToDelete) { + router.delete(categoryRoutes.destroy(categoryToDelete.id).url, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + setIsDeleteDialogOpen(false); + setCategoryToDelete(null); + setRowSelection({}); + }, + }); + } + }; + + const confirmBulkDelete = () => { + router.post(categoryRoutes.bulkDestroy().url, { + ids: rowsToDelete.map((row: any) => row.id), + _method: 'DELETE' + }, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + setIsBulkDeleteDialogOpen(false); + setRowsToDelete([]); + setRowSelection({}); + }, + }); + }; + + const onToggleStatus = (id: number) => { + router.patch(categoryRoutes.toggleStatus(id).url, {}, { + onSuccess: (response: any) => toast.success(response.props.flash.success), + }); + }; + + const closeForm = () => { + setIsFormOpen(false); + setTimeout(() => setSelectedCategory(null), 200); + }; + + return { + isFormOpen, + selectedCategory, + isDeleteDialogOpen, + isBulkDeleteDialogOpen, + categoryToDelete, + rowsToDelete, + rowSelection, + setRowSelection, + setRowsToDelete, + setIsDeleteDialogOpen, + setIsBulkDeleteDialogOpen, + onAdd, + onEdit, + onDelete, + confirmDelete, + confirmBulkDelete, + onToggleStatus, + closeForm, + }; +} diff --git a/resources/js/pages/admin/master/category/index.tsx b/resources/js/pages/admin/master/category/index.tsx index 00b9600..bc9fee4 100644 --- a/resources/js/pages/admin/master/category/index.tsx +++ b/resources/js/pages/admin/master/category/index.tsx @@ -1,28 +1,9 @@ -import { Head, useForm, router, usePage } from '@inertiajs/react'; +import { Head } from '@inertiajs/react'; import type { Category } from '@/types'; -import { ColumnDef } from '@tanstack/react-table'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Trash2, Pencil } from 'lucide-react'; +import { Trash2 } from 'lucide-react'; import { DataTable } from '@/components/data-table'; -import { DataTableColumnHeader } from '@/components/data-table-column-header'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { Field, FieldGroup } from "@/components/ui/field" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { useState } from 'react'; -import { toast } from 'sonner'; -import { TooltipContent, TooltipTrigger } from '@radix-ui/react-tooltip'; -import { Tooltip } from '@/components/ui/tooltip'; -import { Switch } from '@/components/ui/switch'; -import categoryRoutes from '@/routes/category'; import { AlertDialog, AlertDialogAction, @@ -35,154 +16,33 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog" +import { useCategoryIndex } from './hooks/use-category-index'; +import { getColumns } from './partials/columns'; +import { CategoryFormModal } from './partials/category-form-modal'; + export default function CategoryIndex({ categories }: { categories: Category[] }) { - const [isOpen, setIsOpen] = useState(false); - const [isEditing, setIsEditing] = useState(false); - const [selectedCategory, setSelectedCategory] = useState(null); + const { + isFormOpen, + selectedCategory, + isDeleteDialogOpen, + isBulkDeleteDialogOpen, + categoryToDelete, + rowsToDelete, + rowSelection, + setRowSelection, + setRowsToDelete, + setIsDeleteDialogOpen, + setIsBulkDeleteDialogOpen, + onAdd, + onEdit, + onDelete, + confirmDelete, + confirmBulkDelete, + onToggleStatus, + closeForm, + } = useCategoryIndex(); - const { data, setData, post, patch, processing, errors, reset, clearErrors } = useForm({ - name: '', - }); - - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); - const [categoryToDelete, setCategoryToDelete] = useState(null); - const [rowsToDelete, setRowsToDelete] = useState([]); - const [rowSelection, setRowSelection] = useState({}); - - const onEdit = (category: Category) => { - setIsEditing(true); - setSelectedCategory(category); - setData({ - name: category.name, - }); - setIsOpen(true); - }; - - const onDelete = (category: Category) => { - setCategoryToDelete(category); - setIsDeleteDialogOpen(true); - }; - - const confirmDelete = () => { - if (categoryToDelete) { - router.delete(categoryRoutes.destroy(categoryToDelete.id).url, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - setIsDeleteDialogOpen(false); - setCategoryToDelete(null); - setRowSelection({}); - }, - }); - } - }; - - const confirmBulkDelete = () => { - router.post(categoryRoutes.bulkDestroy().url, { - ids: rowsToDelete.map((row: any) => row.id), - _method: 'DELETE' - }, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - setIsBulkDeleteDialogOpen(false); - setRowsToDelete([]); - setRowSelection({}); - }, - }); - }; - - const onToggleStatus = (id: number) => { - router.patch(categoryRoutes.toggleStatus(id).url, {}, { - onSuccess: (response: any) => toast.success(response.props.flash.success), - }); - }; - - const closeModal = () => { - setIsOpen(false); - setTimeout(() => { - setIsEditing(false); - setSelectedCategory(null); - reset(); - clearErrors(); - }, 200); - }; - - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (isEditing && selectedCategory) { - patch(categoryRoutes.update(selectedCategory.id).url, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - closeModal(); - }, - }); - } else { - post(categoryRoutes.store().url, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - closeModal(); - }, - }); - } - }; - - const columns: ColumnDef[] = [ - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ) - }, - meta: { title: "Nama" }, - }, - { - accessorKey: "is_active", - header: "Status", - meta: { title: "Status" }, - cell: ({ row }) => { - const category = row.original; - return ( - onToggleStatus(category.id)} - /> - ); - } - }, - { - id: "actions", - header: "Aksi", - cell: ({ row }) => { - const category = row.original; - return ( -
- - - - - -

Ubah

-
-
- - - - - -

Hapus

-
-
-
- ); - }, - meta: { title: "Aksi" }, - }, - ]; + const columns = getColumns({ onEdit, onDelete, onToggleStatus }); return (
@@ -192,41 +52,16 @@ export default function CategoryIndex({ categories }: { categories: Category[] }

Kategori

-
- !open && closeModal()}> - - - {isEditing ? 'Ubah Kategori' : 'Tambah Kategori'} - -
- - - - setData('name', e.target.value)} - autoComplete='off' - placeholder='Contoh: Gamis' - maxLength={50} - /> - {errors.name &&

{errors.name}

} -
-
- - - - -
-
-
+ @@ -260,6 +95,7 @@ export default function CategoryIndex({ categories }: { categories: Category[] } + {/* Single Delete Confirmation */} @@ -278,6 +114,7 @@ export default function CategoryIndex({ categories }: { categories: Category[] } + {/* Bulk Delete Confirmation */} diff --git a/resources/js/pages/admin/master/category/partials/category-form-modal.tsx b/resources/js/pages/admin/master/category/partials/category-form-modal.tsx new file mode 100644 index 0000000..02af08a --- /dev/null +++ b/resources/js/pages/admin/master/category/partials/category-form-modal.tsx @@ -0,0 +1,100 @@ +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Field, FieldGroup } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Category } from '@/types'; +import { useForm } from '@inertiajs/react'; +import { useEffect } from 'react'; +import categoryRoutes from '@/routes/category'; +import { toast } from 'sonner'; + +interface CategoryFormModalProps { + isOpen: boolean; + onClose: () => void; + category: Category | null; +} + +export function CategoryFormModal({ isOpen, onClose, category }: CategoryFormModalProps) { + const isEditing = !!category; + + const { data, setData, post, patch, processing, errors, reset, clearErrors } = useForm({ + name: '', + }); + + useEffect(() => { + if (category) { + setData({ + name: category.name, + }); + } else { + reset(); + } + }, [category]); + + const handleClose = () => { + onClose(); + setTimeout(() => { + reset(); + clearErrors(); + }, 200); + }; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (isEditing && category) { + patch(categoryRoutes.update(category.id).url, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + handleClose(); + }, + }); + } else { + post(categoryRoutes.store().url, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + handleClose(); + }, + }); + } + }; + + return ( + !open && handleClose()}> + + + {isEditing ? 'Ubah Kategori' : 'Tambah Kategori'} + +
+ + + + setData('name', e.target.value)} + autoComplete='off' + placeholder='Contoh: Gamis' + maxLength={50} + /> + {errors.name &&

{errors.name}

} +
+
+ + + + +
+
+
+ ); +} diff --git a/resources/js/pages/admin/master/category/partials/columns.tsx b/resources/js/pages/admin/master/category/partials/columns.tsx new file mode 100644 index 0000000..1deff58 --- /dev/null +++ b/resources/js/pages/admin/master/category/partials/columns.tsx @@ -0,0 +1,79 @@ +import { ColumnDef } from '@tanstack/react-table'; +import { Category } from '@/types'; +import { DataTableColumnHeader } from '@/components/data-table-column-header'; +import { Switch } from '@/components/ui/switch'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Button } from '@/components/ui/button'; +import { Pencil, Trash2 } from 'lucide-react'; + +interface ColumnProps { + onEdit: (category: Category) => void; + onDelete: (category: Category) => void; + onToggleStatus: (id: number) => void; +} + +export const getColumns = ({ onEdit, onDelete, onToggleStatus }: ColumnProps): ColumnDef[] => [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + meta: { title: "Nama" }, + }, + { + accessorKey: "is_active", + header: "Status", + meta: { title: "Status" }, + cell: ({ row }) => { + const category = row.original; + return ( + onToggleStatus(category.id)} + /> + ); + } + }, + { + id: "actions", + header: "Aksi", + cell: ({ row }) => { + const category = row.original; + return ( +
+ + + + + +

Ubah

+
+
+ + + + + +

Hapus

+
+
+
+ ); + }, + meta: { title: "Aksi" }, + }, +]; diff --git a/resources/js/pages/admin/master/product/hooks/use-product-index.ts b/resources/js/pages/admin/master/product/hooks/use-product-index.ts new file mode 100644 index 0000000..9d0f69a --- /dev/null +++ b/resources/js/pages/admin/master/product/hooks/use-product-index.ts @@ -0,0 +1,79 @@ +import { useState } from 'react'; +import { Product } from '@/types'; +import { router } from '@inertiajs/react'; +import productRoutes from '@/routes/product'; +import { toast } from 'sonner'; + +export function useProductIndex() { + const [selectedImage, setSelectedImage] = useState(null); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); + const [productToDelete, setProductToDelete] = useState(null); + const [rowsToDelete, setRowsToDelete] = useState([]); + const [rowSelection, setRowSelection] = useState({}); + + const onDelete = (product: Product) => { + setProductToDelete(product); + setIsDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + if (productToDelete) { + router.delete(productRoutes.destroy(productToDelete.id).url, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + setIsDeleteDialogOpen(false); + setProductToDelete(null); + setRowSelection({}); + }, + }); + } + }; + + const confirmBulkDelete = () => { + router.post(productRoutes.bulkDestroy().url, { + ids: rowsToDelete.map((row: any) => row.id), + _method: 'DELETE' + }, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + setIsBulkDeleteDialogOpen(false); + setRowsToDelete([]); + setRowSelection({}); + }, + }); + }; + + const onToggleStatus = (id: number) => { + router.patch(productRoutes.toggleStatus(id).url, {}, { + onSuccess: (response: any) => toast.success(response.props.flash.success), + }); + }; + + const onPreviewImage = (url: string) => { + setSelectedImage(url); + }; + + const closeImagePreview = () => { + setSelectedImage(null); + }; + + return { + selectedImage, + isDeleteDialogOpen, + isBulkDeleteDialogOpen, + productToDelete, + rowsToDelete, + rowSelection, + setRowSelection, + setRowsToDelete, + setIsDeleteDialogOpen, + setIsBulkDeleteDialogOpen, + onDelete, + confirmDelete, + confirmBulkDelete, + onToggleStatus, + onPreviewImage, + closeImagePreview, + }; +} diff --git a/resources/js/pages/admin/master/product/index.tsx b/resources/js/pages/admin/master/product/index.tsx index 432ae65..e4aa8bc 100644 --- a/resources/js/pages/admin/master/product/index.tsx +++ b/resources/js/pages/admin/master/product/index.tsx @@ -1,18 +1,9 @@ -import { Head, router, Link } from '@inertiajs/react'; +import { Head, Link } from '@inertiajs/react'; import type { Product, Category } from '@/types'; -import { ColumnDef } from '@tanstack/react-table'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Trash2, Pencil, Plus, ImagePlus } from 'lucide-react'; +import { Trash2 } from 'lucide-react'; import { DataTable } from '@/components/data-table'; -import { DataTableColumnHeader } from '@/components/data-table-column-header'; -import { useState } from 'react'; -import { toast } from 'sonner'; -import { TooltipContent, TooltipTrigger } from '@radix-ui/react-tooltip'; -import { Tooltip } from '@/components/ui/tooltip'; -import { Switch } from '@/components/ui/switch'; -import productRoutes from '@/routes/product'; import { AlertDialog, AlertDialogAction, @@ -24,171 +15,33 @@ import { AlertDialogMedia, AlertDialogTitle, } from "@/components/ui/alert-dialog" -import { Dialog, DialogContent } from "@/components/ui/dialog" -import { X } from 'lucide-react'; + +import productRoutes from '@/routes/product'; +import { useProductIndex } from './hooks/use-product-index'; +import { getColumns } from './partials/columns'; +import { ImagePreviewDialog } from './partials/image-preview-dialog'; export default function ProductIndex({ products, categories }: { products: Product[], categories: Category[] }) { - const [selectedImage, setSelectedImage] = useState(null); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); - const [productToDelete, setProductToDelete] = useState(null); - const [rowsToDelete, setRowsToDelete] = useState([]); - const [rowSelection, setRowSelection] = useState({}); + const { + selectedImage, + isDeleteDialogOpen, + isBulkDeleteDialogOpen, + productToDelete, + rowsToDelete, + rowSelection, + setRowSelection, + setRowsToDelete, + setIsDeleteDialogOpen, + setIsBulkDeleteDialogOpen, + onDelete, + confirmDelete, + confirmBulkDelete, + onToggleStatus, + onPreviewImage, + closeImagePreview, + } = useProductIndex(); - const onDelete = (product: Product) => { - setProductToDelete(product); - setIsDeleteDialogOpen(true); - }; - - const confirmDelete = () => { - if (productToDelete) { - router.delete(productRoutes.destroy(productToDelete.id).url, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - setIsDeleteDialogOpen(false); - setProductToDelete(null); - setRowSelection({}); - }, - }); - } - }; - - const confirmBulkDelete = () => { - router.post(productRoutes.bulkDestroy().url, { - ids: rowsToDelete.map((row: any) => row.id), - _method: 'DELETE' - }, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - setIsBulkDeleteDialogOpen(false); - setRowsToDelete([]); - setRowSelection({}); - }, - }); - }; - - const onToggleStatus = (id: number) => { - router.patch(productRoutes.toggleStatus(id).url, {}, { - onSuccess: (response: any) => toast.success(response.props.flash.success), - }); - }; - - const columns: ColumnDef[] = [ - { - accessorKey: "thumbnail_url", - header: "Thumbnail", - meta: { title: "Thumbnail" }, - cell: ({ row }) => { - const product = row.original; - return ( -
- {product.thumbnail_url ? ( - - ) : ( -
- -
- )} -
- ); - } - }, - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ) - }, - meta: { title: "Nama" }, - }, - { - accessorKey: "categories", - header: ({ column }) => { - return ( - - ) - }, - meta: { title: "Kategori" }, - filterFn: (row, id, value) => { - const categories = row.getValue(id) as Category[]; - if (!categories) return false; - return categories.some(cat => String(cat.id) === String(value)); - }, - cell: ({ row }) => { - const product = row.original; - return ( -
- {product.categories?.map((category) => ( - - {category.name} - - ))} - {(!product.categories || product.categories.length === 0) && ( - Tanpa Kategori - )} -
- ); - } - }, - { - accessorKey: "is_active", - header: "Status", - meta: { title: "Status" }, - cell: ({ row }) => { - const product = row.original; - return ( - onToggleStatus(product.id)} - /> - ); - } - }, - { - id: "actions", - header: "Aksi", - cell: ({ row }) => { - const product = row.original; - return ( -
- - - - - - - -

Ubah

-
-
- - - - - -

Hapus

-
-
-
- ); - }, - meta: { title: "Aksi" }, - }, - ]; + const columns = getColumns({ onDelete, onToggleStatus, onPreviewImage }); return (
@@ -245,6 +98,7 @@ export default function ProductIndex({ products, categories }: { products: Produ + {/* Single Delete Confirmation */} @@ -263,6 +117,7 @@ export default function ProductIndex({ products, categories }: { products: Produ + {/* Bulk Delete Confirmation */} @@ -286,23 +141,10 @@ export default function ProductIndex({ products, categories }: { products: Produ - setSelectedImage(null)}> - -
- Preview - -
-
-
+
); } diff --git a/resources/js/pages/admin/master/product/partials/columns.tsx b/resources/js/pages/admin/master/product/partials/columns.tsx new file mode 100644 index 0000000..9f02192 --- /dev/null +++ b/resources/js/pages/admin/master/product/partials/columns.tsx @@ -0,0 +1,130 @@ +import { ColumnDef } from '@tanstack/react-table'; +import { Product, Category } from '@/types'; +import { DataTableColumnHeader } from '@/components/data-table-column-header'; +import { Badge } from '@/components/ui/badge'; +import { Switch } from '@/components/ui/switch'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Button } from '@/components/ui/button'; +import { Pencil, Trash2, ImagePlus } from 'lucide-react'; +import { Link } from '@inertiajs/react'; +import productRoutes from '@/routes/product'; + +interface ColumnProps { + onEdit?: (product: Product) => void; + onDelete: (product: Product) => void; + onToggleStatus: (id: number) => void; + onPreviewImage: (url: string) => void; +} + +export const getColumns = ({ onDelete, onToggleStatus, onPreviewImage }: ColumnProps): ColumnDef[] => [ + { + accessorKey: "thumbnail_url", + header: "Thumbnail", + meta: { title: "Thumbnail" }, + cell: ({ row }) => { + const product = row.original; + return ( +
+ {product.thumbnail_url ? ( + + ) : ( +
+ +
+ )} +
+ ); + } + }, + { + accessorKey: "name", + header: ({ column }) => ( + + ), + meta: { title: "Nama" }, + }, + { + accessorKey: "categories", + header: ({ column }) => ( + + ), + meta: { title: "Kategori" }, + filterFn: (row, id, value) => { + const categories = row.getValue(id) as Category[]; + if (!categories) return false; + return categories.some(cat => String(cat.id) === String(value)); + }, + cell: ({ row }) => { + const product = row.original; + return ( +
+ {product.categories?.map((category) => ( + + {category.name} + + ))} + {(!product.categories || product.categories.length === 0) && ( + Tanpa Kategori + )} +
+ ); + } + }, + { + accessorKey: "is_active", + header: "Status", + meta: { title: "Status" }, + cell: ({ row }) => { + const product = row.original; + return ( + onToggleStatus(product.id)} + /> + ); + } + }, + { + id: "actions", + header: "Aksi", + cell: ({ row }) => { + const product = row.original; + return ( +
+ + + + + + + +

Ubah

+
+
+ + + + + +

Hapus

+
+
+
+ ); + }, + meta: { title: "Aksi" }, + }, +]; diff --git a/resources/js/pages/admin/master/product/partials/image-preview-dialog.tsx b/resources/js/pages/admin/master/product/partials/image-preview-dialog.tsx new file mode 100644 index 0000000..4e612bb --- /dev/null +++ b/resources/js/pages/admin/master/product/partials/image-preview-dialog.tsx @@ -0,0 +1,29 @@ +import { Dialog, DialogContent } from "@/components/ui/dialog" +import { X } from 'lucide-react'; + +interface ImagePreviewDialogProps { + imageUrl: string | null; + onClose: () => void; +} + +export function ImagePreviewDialog({ imageUrl, onClose }: ImagePreviewDialogProps) { + return ( + onClose()}> + +
+ Preview + +
+
+
+ ); +} diff --git a/resources/js/pages/admin/master/user/hooks/use-user-index.ts b/resources/js/pages/admin/master/user/hooks/use-user-index.ts new file mode 100644 index 0000000..dd1b671 --- /dev/null +++ b/resources/js/pages/admin/master/user/hooks/use-user-index.ts @@ -0,0 +1,84 @@ +import { useState } from 'react'; +import { User } from '@/types'; +import { router } from '@inertiajs/react'; +import userRoutes from '@/routes/user'; +import { toast } from 'sonner'; + +export function useUserIndex() { + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); + const [userToDelete, setUserToDelete] = useState(null); + const [isResetPasswordOpen, setIsResetPasswordOpen] = useState(false); + const [userToReset, setUserToReset] = useState(null); + const [rowsToDelete, setRowsToDelete] = useState([]); + const [rowSelection, setRowSelection] = useState({}); + + const onResetPassword = (user: User) => { + setUserToReset(user); + setIsResetPasswordOpen(true); + }; + + const confirmResetPassword = () => { + if (userToReset) { + router.patch(userRoutes.resetPassword(userToReset.id).url, {}, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + setIsResetPasswordOpen(false); + setUserToReset(null); + }, + }); + } + }; + + const onDelete = (user: User) => { + setUserToDelete(user); + setIsDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + if (userToDelete) { + router.delete(userRoutes.destroy(userToDelete.id).url, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + setIsDeleteDialogOpen(false); + setUserToDelete(null); + setRowSelection({}); + }, + }); + } + }; + + const confirmBulkDelete = () => { + router.post(userRoutes.bulkDestroy().url, { + ids: rowsToDelete.map((row: any) => row.id), + _method: 'DELETE' + }, { + onSuccess: (response: any) => { + toast.success(response.props.flash.success); + setIsBulkDeleteDialogOpen(false); + setRowsToDelete([]); + setRowSelection({}); + }, + }); + }; + + return { + isDeleteDialogOpen, + isBulkDeleteDialogOpen, + userToDelete, + isResetPasswordOpen, + userToReset, + rowsToDelete, + rowSelection, + setRowSelection, + setRowsToDelete, + setIsDeleteDialogOpen, + setIsBulkDeleteDialogOpen, + setIsResetPasswordOpen, + onResetPassword, + confirmResetPassword, + onDelete, + confirmDelete, + confirmBulkDelete, + }; +} diff --git a/resources/js/pages/admin/master/user/index.tsx b/resources/js/pages/admin/master/user/index.tsx index c06598e..48e5a1b 100644 --- a/resources/js/pages/admin/master/user/index.tsx +++ b/resources/js/pages/admin/master/user/index.tsx @@ -1,17 +1,9 @@ -import { Head, router, Link } from '@inertiajs/react'; +import { Head, Link } from '@inertiajs/react'; import type { User } from '@/types'; -import { ColumnDef } from '@tanstack/react-table'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Trash2, Pencil, Plus, KeyRound } from 'lucide-react'; +import { Trash2, KeyRound } from 'lucide-react'; import { DataTable } from '@/components/data-table'; -import { DataTableColumnHeader } from '@/components/data-table-column-header'; -import { useState } from 'react'; -import { toast } from 'sonner'; -import { TooltipContent, TooltipTrigger } from '@radix-ui/react-tooltip'; -import { Tooltip } from '@/components/ui/tooltip'; -import userRoutes from '@/routes/user'; -import { UserInfo } from '@/components/user-info'; import { AlertDialog, AlertDialogAction, @@ -24,146 +16,32 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog" +import userRoutes from '@/routes/user'; +import { useUserIndex } from './hooks/use-user-index'; +import { getColumns } from './partials/columns'; + export default function UserIndex({ users, defaultPassword }: { users: User[], defaultPassword: string }) { - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); - const [userToDelete, setUserToDelete] = useState(null); - const [isResetPasswordOpen, setIsResetPasswordOpen] = useState(false); - const [userToReset, setUserToReset] = useState(null); - const [rowsToDelete, setRowsToDelete] = useState([]); - const [rowSelection, setRowSelection] = useState({}); + const { + isDeleteDialogOpen, + isBulkDeleteDialogOpen, + userToDelete, + isResetPasswordOpen, + userToReset, + rowsToDelete, + rowSelection, + setRowSelection, + setRowsToDelete, + setIsDeleteDialogOpen, + setIsBulkDeleteDialogOpen, + setIsResetPasswordOpen, + onResetPassword, + confirmResetPassword, + onDelete, + confirmDelete, + confirmBulkDelete, + } = useUserIndex(); - const onResetPassword = (user: User) => { - setUserToReset(user); - setIsResetPasswordOpen(true); - }; - - const confirmResetPassword = () => { - if (userToReset) { - router.patch(userRoutes.resetPassword(userToReset.id).url, {}, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - setIsResetPasswordOpen(false); - setUserToReset(null); - }, - }); - } - }; - - const onDelete = (user: User) => { - setUserToDelete(user); - setIsDeleteDialogOpen(true); - }; - - const confirmDelete = () => { - if (userToDelete) { - router.delete(userRoutes.destroy(userToDelete.id).url, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - setIsDeleteDialogOpen(false); - setUserToDelete(null); - setRowSelection({}); - }, - }); - } - }; - - const confirmBulkDelete = () => { - router.post(userRoutes.bulkDestroy().url, { - ids: rowsToDelete.map((row: any) => row.id), - _method: 'DELETE' - }, { - onSuccess: (response: any) => { - toast.success(response.props.flash.success); - setIsBulkDeleteDialogOpen(false); - setRowsToDelete([]); - setRowSelection({}); - }, - }); - }; - - const columns: ColumnDef[] = [ - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ) - }, - meta: { title: "User" }, - cell: ({ row }) => { - const user = row.original; - return ( -
- -
- ); - } - }, - { - accessorKey: "username", - header: ({ column }) => { - return ( - - ) - }, - meta: { title: "Nama Pengguna" }, - }, - { - accessorFn: (row) => row.profile?.phone_number, - id: "phone_number", - header: ({ column }) => { - return ( - - ) - }, - meta: { title: "Nomor Telepon" }, - }, - { - id: "actions", - header: "Aksi", - cell: ({ row }) => { - const user = row.original; - return ( -
- - - - - -

Reset Kata Sandi

-
-
- - - - - - - -

Ubah

-
-
- - - - - -

Hapus

-
-
-
- ); - }, - meta: { title: "Aksi" }, - }, - ]; + const columns = getColumns({ onResetPassword, onDelete }); return (
@@ -202,6 +80,7 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d + {/* Single Delete Confirmation */} @@ -220,6 +99,7 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d + {/* Reset Password Confirmation */} @@ -238,6 +118,7 @@ export default function UserIndex({ users, defaultPassword }: { users: User[], d + {/* Bulk Delete Confirmation */} diff --git a/resources/js/pages/admin/master/user/partials/columns.tsx b/resources/js/pages/admin/master/user/partials/columns.tsx new file mode 100644 index 0000000..1607028 --- /dev/null +++ b/resources/js/pages/admin/master/user/partials/columns.tsx @@ -0,0 +1,105 @@ +import { ColumnDef } from '@tanstack/react-table'; +import { User } from '@/types'; +import { DataTableColumnHeader } from '@/components/data-table-column-header'; +import { UserInfo } from '@/components/user-info'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Button } from '@/components/ui/button'; +import { KeyRound, Pencil, Trash2 } from 'lucide-react'; +import { Link } from '@inertiajs/react'; +import userRoutes from '@/routes/user'; + +interface ColumnProps { + onResetPassword: (user: User) => void; + onDelete: (user: User) => void; +} + +export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDef[] => [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + meta: { title: "User" }, + cell: ({ row }) => { + const user = row.original; + return ( +
+ +
+ ); + } + }, + { + accessorKey: "username", + header: ({ column }) => ( + + ), + meta: { title: "Nama Pengguna" }, + }, + { + accessorFn: (row) => row.profile?.phone_number, + id: "phone_number", + header: ({ column }) => ( + + ), + meta: { title: "Nomor Telepon" }, + }, + { + id: "actions", + header: "Aksi", + cell: ({ row }) => { + const user = row.original; + return ( +
+ + + + + +

Reset Kata Sandi

+
+
+ + + + + + + +

Ubah

+
+
+ + + + + +

Hapus

+
+
+
+ ); + }, + meta: { title: "Aksi" }, + }, +];