Files
HRM-System/resources/js/config/crud/coupons.ts
2026-04-13 08:16:56 +08:00

305 lines
8.8 KiB
TypeScript
Executable File

// config/crud/coupons.ts
import React from 'react';
import { CrudConfig } from '@/types/crud';
import { columnRenderers } from '@/utils/columnRenderers';
import { Switch } from '@/components/ui/switch';
import { toast } from '@/components/custom-toast';
import { t } from '@/utils/i18n';
import { router } from '@inertiajs/react';
// Separate component for status toggle to properly handle hooks
const StatusToggle = ({ initialValue, rowId }: { initialValue: boolean, rowId: number }) => {
const [isChecked, setIsChecked] = React.useState(initialValue);
const handleToggle = () => {
toast.loading(`${isChecked ? t('Deactivating') : t('Activating')} coupon...`);
// Use Inertia router for consistent error handling
router.put(route('coupons.toggle-status', rowId), {}, {
onSuccess: (page: any) => {
toast.dismiss();
setIsChecked(!isChecked);
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));
}
},
onError: (errors: any) => {
toast.dismiss();
if (typeof errors === 'string') {
toast.error(t(errors));
} else {
toast.error(t('Failed to update coupon status: {{errors}}', { errors: Object.values(errors).join(', ') }));
}
}
});
};
return React.createElement('div', { className: 'flex items-center justify-center' }, [
React.createElement(Switch, {
key: 'status-switch',
checked: isChecked,
onCheckedChange: handleToggle
})
]);
};
export const couponsConfig: CrudConfig = {
entity: {
name: 'coupons',
endpoint: route('coupons.index'),
permissions: {
view: 'view-coupons',
create: 'create-coupons',
edit: 'create-coupons',
delete: 'delete-coupons'
}
},
modalSize: '4xl',
table: {
columns: [
{ key: 'name', label: t('Name'), sortable: true },
{
key: 'type',
label: t('Type'),
sortable: true,
render: (value) => {
const className = value === 'percentage'
? 'bg-blue-100 text-blue-800'
: 'bg-green-100 text-green-800';
return value === 'percentage' ? t('Percentage') : t('Flat Amount');
}
},
{
key: 'minimum_spend',
label: t('Min Spend'),
render: (value) => value ? (window.appSettings?.formatCurrency(value) || `$${parseFloat(value).toFixed(2)}`) : '-'
},
{
key: 'maximum_spend',
label: t('Max Spend'),
render: (value) => value ? (window.appSettings?.formatCurrency(value) || `$${parseFloat(value).toFixed(2)}`) : '-'
},
{
key: 'discount_amount',
label: t('Discount'),
render: (value, row) => {
const amount = parseFloat(value);
return row.type === 'percentage'
? `${amount}%`
: (window.appSettings?.formatCurrency(amount) || `$${amount.toFixed(2)}`);
}
},
{ key: 'use_limit_per_coupon', label: t('Coupon Limit'), render: (value) => value || t('Unlimited') },
{ key: 'use_limit_per_user', label: t('User Limit'), render: (value) => value || t('Unlimited') },
{
key: 'expiry_date',
label: t('Expiry Date'),
sortable: true,
render: (value) => `${window.appSettings.formatDateTime(value, false)}`
},
{ key: 'code', label: t('Code'), sortable: true },
{
key: 'status',
label: t('Status'),
render: (value, row) => {
// Use a component to properly handle hooks
return React.createElement(StatusToggle, { initialValue: !!value, rowId: row.id });
}
}
],
actions: [
{
label: t('View Details'),
icon: 'Eye',
action: 'view-details',
href: (row: any) => route('coupons.show', row.id),
className: 'text-blue-500'
},
{
label: t('Edit'),
icon: 'Edit',
action: 'edit',
className: 'text-amber-500'
},
{
label: t('Delete'),
icon: 'Trash2',
action: 'delete',
className: 'text-red-500'
}
]
},
search: {
enabled: true,
placeholder: t('Search coupons...'),
fields: ['name', 'code']
},
filters: [
{
key: 'type',
label: t('Type'),
type: 'select',
options: [
{ value: 'all', label: t('All Types') },
{ value: 'percentage', label: t('Percentage') },
{ value: 'flat', label: t('Flat Amount') }
]
},
{
key: 'status',
label: t('Status'),
type: 'select',
options: [
{ value: 'all', label: t('All Status') },
{ value: '1', label: t('Active') },
{ value: '0', label: t('Inactive') }
]
}
],
form: {
fields: [
{
name: 'name',
label: t('Coupon Name'),
type: 'text',
required: true,
colSpan: 12,
placeholder: t('Enter coupon name')
},
{
name: 'type',
label: t('Discount Type'),
type: 'select',
required: true,
colSpan: 6,
options: [
{ value: 'percentage', label: t('Percentage (%)') },
{ value: 'flat', label: t('Fixed Amount ($)') }
]
},
{
name: 'discount_amount',
label: t('Discount Value'),
type: 'number',
required: true,
colSpan: 6,
min: 0,
max: 99,
step: 0.01,
placeholder: t('Enter value')
},
{
name: 'code_type',
label: t('Code Generation'),
type: 'radio',
required: true,
colSpan: 12,
options: [
{ value: 'manual', label: t('Manual Entry') },
{ value: 'auto', label: t('Auto Generate') }
],
defaultValue: 'manual'
},
{
name: 'code',
label: t('Coupon Code'),
type: 'custom',
colSpan: 12,
render: (field: any, formData: any, onChange: any) => {
const generateCode = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < 10; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
onChange('code', result);
};
const isAuto = formData.code_type === 'auto';
return React.createElement('div', { className: 'space-y-2' }, [
React.createElement('div', {
className: isAuto ? 'flex gap-2' : '',
key: 'input-group'
}, isAuto ? [
React.createElement('input', {
key: 'code-input',
type: 'text',
value: formData.code || '',
onChange: (e: any) => onChange('code', e.target.value.toUpperCase()),
placeholder: t('Click generate to create code'),
className: 'flex-1 px-3 py-2 border rounded-md'
}),
React.createElement('button', {
key: 'generate-btn',
type: 'button',
onClick: generateCode,
className: 'px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600'
}, t('Generate'))
] : [
React.createElement('input', {
key: 'code-input-manual',
type: 'text',
value: formData.code || '',
onChange: (e: any) => onChange('code', e.target.value.toUpperCase()),
placeholder: t('Enter coupon code'),
className: 'w-full px-3 py-2 border rounded-md'
})
])
]);
}
},
{
name: 'minimum_spend',
label: t('Minimum Spend'),
type: 'number',
colSpan: 6,
min: 0,
step: 0.01,
placeholder: t('Optional')
},
{
name: 'maximum_spend',
label: t('Maximum Spend'),
type: 'number',
colSpan: 6,
min: 0,
step: 0.01,
placeholder: t('Optional')
},
{
name: 'use_limit_per_coupon',
label: t('Total Usage Limit'),
type: 'number',
colSpan: 6,
min: 1,
placeholder: t('Leave empty for unlimited')
},
{
name: 'use_limit_per_user',
label: t('Usage Limit Per User'),
type: 'number',
colSpan: 6,
min: 1,
placeholder: t('Leave empty for unlimited')
},
{
name: 'expiry_date',
label: t('Expiry Date'),
type: 'date',
colSpan: 6
},
{
name: 'status',
label: t('Status'),
type: 'switch',
colSpan: 6,
defaultValue: true,
placeholder: t('Enable or disable this coupon')
}
]
}
};