efril #13

Merged
aefril merged 19 commits from efril into main 2025-09-18 04:01:33 +00:00
Showing only changes of commit 3a56e56c69 - Show all commits

View File

@ -32,9 +32,40 @@ type Props = {
data?: Tier // Data tier untuk edit (jika ada) data?: Tier // Data tier untuk edit (jika ada)
} }
// Static benefit keys with their configurations
const STATIC_BENEFIT_KEYS = {
birthday_bonus: {
label: 'Birthday Bonus',
type: 'boolean' as const,
description: 'Bonus ulang tahun khusus member'
},
exclusive_discounts: {
label: 'Exclusive Discounts',
type: 'boolean' as const,
description: 'Akses diskon eksklusif'
},
point_multiplier: {
label: 'Point Multiplier',
type: 'number' as const,
description: 'Pengali poin (contoh: 1.1 = +10%)',
suffix: 'x'
},
priority_support: {
label: 'Priority Support',
type: 'boolean' as const,
description: 'Dukungan pelanggan prioritas'
},
special_discount: {
label: 'Special Discount',
type: 'number' as const,
description: 'Diskon khusus dalam persen',
suffix: '%'
}
} as const
// Benefit item type // Benefit item type
type BenefitItem = { type BenefitItem = {
key: string key: keyof typeof STATIC_BENEFIT_KEYS
value: any value: any
type: 'boolean' | 'number' | 'string' type: 'boolean' | 'number' | 'string'
} }
@ -43,19 +74,13 @@ type FormValidateType = {
name: string name: string
min_points: number min_points: number
benefits: BenefitItem[] benefits: BenefitItem[]
newBenefitKey: string
newBenefitValue: string
newBenefitType: 'boolean' | 'number' | 'string'
} }
// Initial form data // Initial form data
const initialData: FormValidateType = { const initialData: FormValidateType = {
name: '', name: '',
min_points: 0, min_points: 0,
benefits: [], benefits: []
newBenefitKey: '',
newBenefitValue: '',
newBenefitType: 'boolean'
} }
const AddEditTierDrawer = (props: Props) => { const AddEditTierDrawer = (props: Props) => {
@ -84,17 +109,16 @@ const AddEditTierDrawer = (props: Props) => {
}) })
const watchedBenefits = watch('benefits') const watchedBenefits = watch('benefits')
const watchedNewBenefitKey = watch('newBenefitKey')
const watchedNewBenefitValue = watch('newBenefitValue')
const watchedNewBenefitType = watch('newBenefitType')
// Helper function to convert benefits object to BenefitItem array // Helper function to convert benefits object to BenefitItem array
const convertBenefitsToArray = (benefits: Record<string, any>): BenefitItem[] => { const convertBenefitsToArray = (benefits: Record<string, any>): BenefitItem[] => {
if (!benefits) return [] if (!benefits) return []
return Object.entries(benefits).map(([key, value]) => ({ return Object.entries(benefits)
key, .filter(([key]) => key in STATIC_BENEFIT_KEYS)
.map(([key, value]) => ({
key: key as keyof typeof STATIC_BENEFIT_KEYS,
value, value,
type: typeof value === 'boolean' ? 'boolean' : typeof value === 'number' ? 'number' : 'string' type: STATIC_BENEFIT_KEYS[key as keyof typeof STATIC_BENEFIT_KEYS].type
})) }))
} }
@ -116,19 +140,26 @@ const AddEditTierDrawer = (props: Props) => {
// Helper function to format benefit display // Helper function to format benefit display
const formatBenefitDisplay = (item: BenefitItem): string => { const formatBenefitDisplay = (item: BenefitItem): string => {
const readableKey = item.key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) const config = STATIC_BENEFIT_KEYS[item.key]
if (item.type === 'boolean') { if (item.type === 'boolean') {
return `${readableKey}: ${item.value ? 'Ya' : 'Tidak'}` return `${config.label}: ${item.value ? 'Ya' : 'Tidak'}`
} else if (item.type === 'number') { } else if (item.type === 'number') {
if (item.key.includes('multiplier')) { const suffix = config.suffix || ''
return `${readableKey}: ${item.value}x` return `${config.label}: ${item.value}${suffix}`
} else if (item.key.includes('discount') || item.key.includes('bonus')) {
return `${readableKey}: ${item.value}%`
} }
return `${readableKey}: ${item.value}` return `${config.label}: ${item.value}`
} }
return `${readableKey}: ${item.value}`
// Get available benefit keys (not already added)
const getAvailableBenefitKeys = () => {
const usedKeys = watchedBenefits?.map(b => b.key) || []
return Object.entries(STATIC_BENEFIT_KEYS)
.filter(([key]) => !usedKeys.includes(key as keyof typeof STATIC_BENEFIT_KEYS))
.map(([key, config]) => ({
key: key as keyof typeof STATIC_BENEFIT_KEYS,
...config
}))
} }
// Effect to populate form when editing // Effect to populate form when editing
@ -141,10 +172,7 @@ const AddEditTierDrawer = (props: Props) => {
const formData: FormValidateType = { const formData: FormValidateType = {
name: data.name || '', name: data.name || '',
min_points: data.min_points || 0, min_points: data.min_points || 0,
benefits: benefitsArray, benefits: benefitsArray
newBenefitKey: '',
newBenefitValue: '',
newBenefitType: 'boolean'
} }
resetForm(formData) resetForm(formData)
@ -156,57 +184,6 @@ const AddEditTierDrawer = (props: Props) => {
} }
}, [data, isEditMode, resetForm]) }, [data, isEditMode, resetForm])
const handleAddBenefit = () => {
const key = watchedNewBenefitKey.trim()
const value = watchedNewBenefitValue.trim()
const type = watchedNewBenefitType
if (key && value) {
// Check if key already exists
const existingKeys = watchedBenefits.map(b => b.key)
if (existingKeys.includes(key)) {
alert('Key benefit sudah ada!')
return
}
let processedValue: any = value
if (type === 'boolean') {
processedValue = value === 'true' || value === 'yes' || value === '1'
} else if (type === 'number') {
processedValue = Number(value)
if (isNaN(processedValue)) {
alert('Nilai harus berupa angka!')
return
}
}
const newBenefit: BenefitItem = {
key,
value: processedValue,
type
}
const currentBenefits = watchedBenefits || []
setValue('benefits', [...currentBenefits, newBenefit])
setValue('newBenefitKey', '')
setValue('newBenefitValue', '')
setValue('newBenefitType', 'boolean')
}
}
const handleRemoveBenefit = (index: number) => {
const currentBenefits = watchedBenefits || []
const newBenefits = currentBenefits.filter((_, i) => i !== index)
setValue('benefits', newBenefits)
}
const handleKeyPress = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
handleAddBenefit()
}
}
const handleFormSubmit = async (formData: FormValidateType) => { const handleFormSubmit = async (formData: FormValidateType) => {
try { try {
setIsSubmitting(true) setIsSubmitting(true)
@ -257,6 +234,11 @@ const AddEditTierDrawer = (props: Props) => {
setShowMore(false) setShowMore(false)
} }
// Get placeholder and validation info based on selected benefit key
const getBenefitInputInfo = () => {
return { placeholder: 'Tidak diperlukan lagi', type: 'text' }
}
const formatNumber = (value: number) => { const formatNumber = (value: number) => {
return new Intl.NumberFormat('id-ID').format(value) return new Intl.NumberFormat('id-ID').format(value)
} }
@ -357,109 +339,133 @@ const AddEditTierDrawer = (props: Props) => {
{/* Benefits */} {/* Benefits */}
<div> <div>
<Typography variant='body2' className='mb-2'> <Typography variant='body2' className='mb-3'>
Manfaat Tier <span className='text-red-500'>*</span> Manfaat Tier <span className='text-red-500'>*</span>
</Typography> </Typography>
{/* Display current benefits */} {/* All Benefits in Horizontal Layout */}
{watchedBenefits && watchedBenefits.length > 0 && ( <div className='space-y-4'>
<div className='flex flex-col gap-2 mb-3'> {Object.entries(STATIC_BENEFIT_KEYS).map(([key, config]) => {
{watchedBenefits.map((benefit, index) => ( const benefitKey = key as keyof typeof STATIC_BENEFIT_KEYS
<Chip const existingBenefit = watchedBenefits?.find(b => b.key === benefitKey)
key={index} const isActive = Boolean(existingBenefit)
label={formatBenefitDisplay(benefit)}
onDelete={() => handleRemoveBenefit(index)} return (
color='primary' <div key={benefitKey} className='border rounded-lg p-3'>
variant='outlined' <div className='flex items-center justify-between mb-2'>
size='small' <div className='flex-1'>
sx={{ <Typography variant='body2' className='font-medium'>
justifyContent: 'space-between', {config.label}
'& .MuiChip-label': { </Typography>
overflow: 'visible', <Typography variant='caption' color='text.secondary'>
textOverflow: 'unset', {config.description}
whiteSpace: 'normal' </Typography>
</div>
<FormControlLabel
control={
<Switch
checked={isActive}
onChange={e => {
if (e.target.checked) {
// Add default benefit
const defaultValue =
config.type === 'boolean'
? true
: config.type === 'number'
? benefitKey === 'point_multiplier'
? 1.1
: benefitKey === 'special_discount'
? 5
: 1
: ''
const newBenefit: BenefitItem = {
key: benefitKey,
value: defaultValue,
type: config.type
}
const currentBenefits = watchedBenefits || []
setValue('benefits', [...currentBenefits, newBenefit])
} else {
// Remove benefit
const currentBenefits = watchedBenefits || []
const newBenefits = currentBenefits.filter(b => b.key !== benefitKey)
setValue('benefits', newBenefits)
} }
}} }}
/>
))}
</div>
)}
{/* Add new benefit - Key */}
<div className='mb-3'>
<Controller
name='newBenefitKey'
control={control}
render={({ field }) => (
<CustomTextField
{...field}
fullWidth
placeholder='Key benefit (contoh: birthday_bonus, point_multiplier)'
label='Key Benefit'
size='small' size='small'
/> />
)} }
label=''
sx={{ margin: 0 }}
/> />
</div> </div>
{/* Type selector */} {/* Value Input - Only show when active */}
<div className='mb-3'> {isActive && (
<Controller <div className='mt-2'>
name='newBenefitType' {config.type === 'boolean' ? (
control={control} <FormControl size='small' fullWidth>
render={({ field }) => ( <Select
<FormControl fullWidth size='small'> value={existingBenefit?.value ? 'true' : 'false'}
<InputLabel>Tipe Value</InputLabel> onChange={e => {
<Select {...field} label='Tipe Value'> const currentBenefits = watchedBenefits || []
<MenuItem value='boolean'>Boolean (Ya/Tidak)</MenuItem> const updatedBenefits = currentBenefits.map(b =>
<MenuItem value='number'>Number (Angka)</MenuItem> b.key === benefitKey ? { ...b, value: e.target.value === 'true' } : b
<MenuItem value='string'>String (Teks)</MenuItem> )
setValue('benefits', updatedBenefits)
}}
>
<MenuItem value='true'>Ya</MenuItem>
<MenuItem value='false'>Tidak</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
)} ) : (
/>
</div>
{/* Add new benefit - Value */}
<div className='mb-3'>
<Controller
name='newBenefitValue'
control={control}
render={({ field }) => (
<CustomTextField <CustomTextField
{...field}
fullWidth fullWidth
placeholder={
watchedNewBenefitType === 'boolean'
? 'true/false, yes/no, 1/0'
: watchedNewBenefitType === 'number'
? 'Contoh: 1.5, 10, 5'
: 'Contoh: Premium access, VIP status'
}
label='Value Benefit'
size='small' size='small'
onKeyPress={handleKeyPress} type='number'
InputProps={{ value={existingBenefit?.value || ''}
endAdornment: ( onChange={e => {
<InputAdornment position='end'> const newValue = Number(e.target.value)
<Button if (!isNaN(newValue)) {
size='small' const currentBenefits = watchedBenefits || []
onClick={handleAddBenefit} const updatedBenefits = currentBenefits.map(b =>
disabled={!watchedNewBenefitKey?.trim() || !watchedNewBenefitValue?.trim()} b.key === benefitKey ? { ...b, value: newValue } : b
>
Tambah
</Button>
</InputAdornment>
) )
setValue('benefits', updatedBenefits)
}
}}
placeholder={
benefitKey === 'point_multiplier'
? 'Contoh: 1.1, 1.5, 2.0'
: benefitKey === 'special_discount'
? 'Contoh: 5, 10, 15'
: 'Masukkan angka'
}
InputProps={{
endAdornment: config.suffix && (
<InputAdornment position='end'>{config.suffix}</InputAdornment>
),
inputProps: {
step: benefitKey === 'point_multiplier' ? '0.1' : '1',
min: benefitKey === 'point_multiplier' ? '0.1' : '0',
max: benefitKey === 'special_discount' ? '100' : undefined
}
}} }}
/> />
)} )}
/> </div>
)}
</div>
)
})}
</div> </div>
{(!watchedBenefits || watchedBenefits.length === 0) && ( {(!watchedBenefits || watchedBenefits.length === 0) && (
<Typography variant='caption' color='error'> <Typography variant='caption' color='error' className='mt-2 block'>
Minimal satu manfaat harus ditambahkan Minimal satu manfaat harus diaktifkan
</Typography> </Typography>
)} )}
</div> </div>