// components/PageCrudWrapper.tsx import { useState, useEffect, ReactNode } from 'react'; import { PageTemplate, PageAction } from '@/components/page-template'; import { PlusIcon } from 'lucide-react'; import { router, usePage } from '@inertiajs/react'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Filter, Search } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Pagination } from '@/components/ui/pagination'; import { hasPermission } from '@/utils/authorization'; import { CrudTable } from './CrudTable'; import { CrudFormModal } from './CrudFormModal'; import { CrudDeleteModal } from './CrudDeleteModal'; import { toast } from '@/components/custom-toast'; import { CrudConfig } from '@/types/crud'; import { BreadcrumbItem } from '@/types'; import { useTranslation } from 'react-i18next'; export interface CrudButton { label: string; icon?: ReactNode; variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; onClick?: () => void; permission?: string; className?: string; showAddButton?: boolean; } interface PageCrudWrapperProps { config: CrudConfig; title?: string; url: string; buttons?: CrudButton[]; breadcrumbs?: BreadcrumbItem[]; } export function PageCrudWrapper({ config, title, url, buttons = [], breadcrumbs }: PageCrudWrapperProps) { const { t } = useTranslation(); const { entity, table, filters = [], form, hooks } = config; const { auth, globalSettings, ...pageProps } = usePage().props as any; const permissions = auth?.permissions || []; // Get data from page props using entity name const data = pageProps[entity.name] || { data: [], links: [] }; const pageFilters = pageProps.filters || {}; // State const [searchTerm, setSearchTerm] = useState(pageFilters.search || ''); const [filterValues, setFilterValues] = useState>({}); const [showFilters, setShowFilters] = useState(false); const [isFormModalOpen, setIsFormModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [currentItem, setCurrentItem] = useState(null); const [formMode, setFormMode] = useState<'create' | 'edit' | 'view'>('create'); // Initialize filter values from URL useEffect(() => { const initialFilters: Record = {}; filters.forEach(filter => { const filterKey = filter.name || filter.key; initialFilters[filterKey] = pageFilters[filterKey] || ''; }); setFilterValues(initialFilters); }, []); // Check if any filters are active const hasActiveFilters = () => { return Object.entries(filterValues).some(([key, value]) => { return value && value !== ''; }) || searchTerm !== ''; }; // Count active filters const activeFilterCount = () => { return Object.entries(filterValues).filter(([key, value]) => { return value && value !== ''; }).length + (searchTerm ? 1 : 0); }; const handleSearch = (e: React.FormEvent) => { e.preventDefault(); applyFilters(); }; const applyFilters = () => { const params: any = { page: 1 }; if (searchTerm) { params.search = searchTerm; } // Add filter values to params Object.entries(filterValues).forEach(([key, value]) => { if (value && value !== '') { params[key] = value; } }); // Add per_page if it exists if (pageFilters.per_page) { params.per_page = pageFilters.per_page; } router.get(entity.endpoint, params, { preserveState: true, preserveScroll: true }); }; const handleFilterChange = (key: string, value: any) => { setFilterValues(prev => ({ ...prev, [key]: value })); const params: any = { page: 1 }; if (searchTerm) { params.search = searchTerm; } // Add all current filter values const newFilters = { ...filterValues, [key]: value }; Object.entries(newFilters).forEach(([k, v]) => { if (v && v !== '') { params[k] = v; } }); // Add per_page if it exists if (pageFilters.per_page) { params.per_page = pageFilters.per_page; } router.get(entity.endpoint, params, { preserveState: true, preserveScroll: true }); }; const handleSort = (field: string) => { const direction = pageFilters.sort_field === field && pageFilters.sort_direction === 'asc' ? 'desc' : 'asc'; const params: any = { sort_field: field, sort_direction: direction, page: 1 }; // Add search and filters if (searchTerm) { params.search = searchTerm; } Object.entries(filterValues).forEach(([key, value]) => { if (value && value !== '') { params[key] = value; } }); // Add per_page if it exists if (pageFilters.per_page) { params.per_page = pageFilters.per_page; } router.get(entity.endpoint, params, { preserveState: true, preserveScroll: true }); }; const handleAction = (action: string, item: any) => { setCurrentItem(item); switch (action) { case 'view': setFormMode('view'); setIsFormModalOpen(true); break; case 'edit': setFormMode('edit'); setIsFormModalOpen(true); break; case 'delete': setIsDeleteModalOpen(true); break; default: break; } }; const handleAddNew = () => { setCurrentItem(null); setFormMode('create'); setIsFormModalOpen(true); }; const handleFormSubmit = (formData: any) => { // Make a copy of the form data to avoid modifying the original const processedFormData = { ...formData }; // For roles, create a simplified object with only the required fields if (entity.name === 'roles') { // Extract permission names from the permissions array if they're objects if (processedFormData.permissions && Array.isArray(processedFormData.permissions)) { const permissionNames = processedFormData.permissions.map(p => { if (typeof p === 'object' && p !== null && p.name) { return p.name; } return String(p); }); processedFormData.permissions = permissionNames; } // Reset the object with only the fields we need const cleanData = { label: processedFormData.label, description: processedFormData.description || '', permissions: processedFormData.permissions || [] }; // Replace all properties Object.keys(processedFormData).forEach(key => { delete processedFormData[key]; }); Object.assign(processedFormData, cleanData); } // Fix permissions format for other entities else if (processedFormData.permissions && Array.isArray(processedFormData.permissions)) { const permissionsObj = {}; processedFormData.permissions.forEach((id, index) => { permissionsObj[index] = String(id); }); processedFormData.permissions = permissionsObj; } // Ensure we're not sending the name field for permissions as it's auto-generated if (entity.name === 'permissions' && formMode === 'edit') { delete processedFormData.name; } // Check if this entity has file uploads const hasFileFields = form.fields.some(field => field.type === 'file'); if (hasFileFields) { // Get file field names const fileFields = form.fields .filter(field => field.type === 'file') .map(field => field.name); // Use FormData for file uploads const formDataObj = new FormData(); // Add all fields to FormData Object.keys(processedFormData).forEach(key => { // For file fields in edit mode if (fileFields.includes(key) && formMode === 'edit') { // Only include the file if a new one was selected if (processedFormData[key] && typeof processedFormData[key] === 'object') { formDataObj.append(key, processedFormData[key]); } // Otherwise skip this field - don't send empty file fields return; } formDataObj.append(key, processedFormData[key]); }); if (formMode === 'create') { // Show loading toast if (!globalSettings?.is_demo) { toast.loading(t('Creating...')); } router.post(entity.endpoint, formDataObj, { onSuccess: (page) => { setIsFormModalOpen(false); if (!globalSettings?.is_demo) { toast.dismiss(); } if (page.props.flash.success) { toast.success(t(page.props.flash.success)); } else if (page.props.flash.error) { toast.error(t(page.props.flash.error)); } if (hooks?.afterCreate) { hooks.afterCreate(formData, page.props[entity.name]); } }, onError: (errors) => { if (!globalSettings?.is_demo) { toast.dismiss(); } if (typeof errors === 'string') { toast.error(t(errors)); } else { toast.error(t(`Failed to create ${entity.name.slice(0, -1)}: {{errors}}`, { errors: Object.values(errors).join(', ') })); } } }); } else if (formMode === 'edit') { // Show loading toast if (!globalSettings?.is_demo) { toast.loading(t('Updating...')); } router.post(`${entity.endpoint}/${currentItem.id}?_method=PUT`, formDataObj, { onSuccess: (page) => { setIsFormModalOpen(false); if (!globalSettings?.is_demo) { toast.dismiss(); } if (page.props.flash.success) { toast.success(t(page.props.flash.success)); } else if (page.props.flash.error) { toast.error(t(page.props.flash.error)); } if (hooks?.afterUpdate) { hooks.afterUpdate(formData, page.props[entity.name]); } }, onError: (errors) => { if (!globalSettings?.is_demo) { toast.dismiss(); } if (typeof errors === 'string') { toast.error(t(errors)); } else { toast.error(t(`Failed to update ${entity.name.slice(0, -1)}: {{errors}}`, { errors: Object.values(errors).join(', ') })); } } }); } return; } if (formMode === 'create') { // Show loading toast if (!globalSettings?.is_demo) { toast.loading(t('Creating...')); } router.post(entity.endpoint, processedFormData, { onSuccess: (page) => { setIsFormModalOpen(false); if (!globalSettings?.is_demo) { toast.dismiss(); } if (page.props.flash.success) { toast.success(t(page.props.flash.success)); } else if (page.props.flash.error) { toast.error(t(page.props.flash.error)); } if (hooks?.afterCreate) { hooks.afterCreate(formData, page.props[entity.name]); } }, onError: (errors) => { if (!globalSettings?.is_demo) { toast.dismiss(); } if (typeof errors === 'string') { toast.error(t(errors)); } else { toast.error(t(`Failed to create ${entity.name.slice(0, -1)}: {{errors}}`, { errors: Object.values(errors).join(', ') })); } } }); } else if (formMode === 'edit') { // Show loading toast if (!globalSettings?.is_demo) { toast.loading(t('Updating...')); } router.put(`${entity.endpoint}/${currentItem.id}`, processedFormData, { onSuccess: (page) => { setIsFormModalOpen(false); if (!globalSettings?.is_demo) { toast.dismiss(); } if (page.props.flash.success) { toast.success(t(page.props.flash.success)); } else if (page.props.flash.error) { toast.error(t(page.props.flash.error)); } if (hooks?.afterUpdate) { hooks.afterUpdate(formData, page.props[entity.name]); } }, onError: (errors) => { if (!globalSettings?.is_demo) { toast.dismiss(); } if (typeof errors === 'string') { toast.error(t(errors)); } else { toast.error(t(`Failed to update ${entity.name.slice(0, -1)}: {{errors}}`, { errors: Object.values(errors).join(', ') })); } } }); } }; const handleDeleteConfirm = () => { // Show loading toast if (!globalSettings?.is_demo) { toast.loading(t('Deleting...')); } router.delete(`${entity.endpoint}/${currentItem.id}`, { onSuccess: (page) => { setIsDeleteModalOpen(false); if (!globalSettings?.is_demo) { toast.dismiss(); } if (page.props.flash.success) { toast.success(t(page.props.flash.success)); } else if (page.props.flash.error) { toast.error(t(page.props.flash.error)); } if (hooks?.afterDelete) { hooks.afterDelete(currentItem.id); } }, onError: (errors) => { if (!globalSettings?.is_demo) { toast.dismiss(); } if (typeof errors === 'string') { toast.error(t(errors)); } else { toast.error(t(`Failed to delete ${entity.name.slice(0, -1)}: {{errors}}`, { errors: Object.values(errors).join(', ') })); } } }); }; const handleResetFilters = () => { // Reset all filters to default values const resetFilters: Record = {}; filters.forEach(filter => { resetFilters[filter.key] = filter.type === 'select' ? 'all' : ''; }); setFilterValues(resetFilters); setSearchTerm(''); setShowFilters(false); router.get(entity.endpoint, { page: 1, per_page: pageFilters.per_page }, { preserveState: true, preserveScroll: true }); }; // Check if we should show the add button const showAddButton = buttons.every(button => button.showAddButton !== false); // Define page actions const pageActions: PageAction[] = []; // Add custom buttons with permission check buttons.forEach(button => { if (!button.permission || hasPermission(permissions, button.permission)) { pageActions.push({ label: button.label, icon: button.icon, variant: button.variant, onClick: button.onClick }); } }); // Add the default "Add New" button if allowed and user has permission if (showAddButton && hasPermission(permissions, entity.permissions.create)) { pageActions.push({ label: `Add ${entity.name.slice(0, -1).charAt(0).toUpperCase() + entity.name.slice(0, -1).slice(1)}`, icon: , variant: 'default', onClick: () => handleAddNew() }); } const pageTitle = title || entity.name.charAt(0).toUpperCase() + entity.name.slice(1); // Generate default breadcrumbs if not provided const defaultBreadcrumbs: BreadcrumbItem[] = [ { title: t('Dashboard'), href: route('dashboard') }, { title: pageTitle } ]; const pageBreadcrumbs = breadcrumbs || defaultBreadcrumbs; return ( {/* Search and filters section */}
setSearchTerm(e.target.value)} className="w-full pl-9" />
{filters.length > 0 && (
)}
{showFilters && filters.length > 0 && (
{filters.map((filter) => { const filterKey = filter.name || filter.key; return (
{filter.type === 'select' && ( )}
); })}
)}
{/* Table section */}
{/* Pagination section */} router.get(url)} />
setIsFormModalOpen(false)} onSubmit={handleFormSubmit} formConfig={{ ...form, modalSize: config.modalSize || form.modalSize }} initialData={currentItem} title={ formMode === 'create' ? `Add New ${entity.name.slice(0, -1).charAt(0).toUpperCase() + entity.name.slice(0, -1).slice(1)}` : formMode === 'edit' ? `Edit ${entity.name.slice(0, -1).charAt(0).toUpperCase() + entity.name.slice(0, -1).slice(1)}` : `View ${entity.name.slice(0, -1).charAt(0).toUpperCase() + entity.name.slice(0, -1).slice(1)}` } mode={formMode} description={config.description} /> setIsDeleteModalOpen(false)} onConfirm={handleDeleteConfirm} itemName={currentItem?.name || ''} entityName={entity.name.slice(0, -1)} />
); }