-
+
{row.original.name}
@@ -356,38 +277,6 @@ const CampaignListTable = () => {
/>
)
}),
- columnHelper.accessor('metadata', {
- header: 'Minimum Pembelian',
- cell: ({ row }) => {
- const minPurchase = getMinimumPurchase(row.original)
- return (
-
-
- {minPurchase > 0 ? formatCurrency(minPurchase) : '-'}
-
- )
- }
- }),
- columnHelper.accessor('rules', {
- header: 'Reward Utama',
- cell: ({ row }) => {
- const reward = getPrimaryReward(row.original)
- return (
-
-
-
-
- )
- }
- }),
columnHelper.accessor('start_date', {
header: 'Periode Kampanye',
cell: ({ row }) => (
diff --git a/src/views/apps/marketing/campaign/detail/AddEditCampaignRuleDrawer.tsx b/src/views/apps/marketing/campaign/detail/AddEditCampaignRuleDrawer.tsx
new file mode 100644
index 0000000..6534850
--- /dev/null
+++ b/src/views/apps/marketing/campaign/detail/AddEditCampaignRuleDrawer.tsx
@@ -0,0 +1,417 @@
+// React Imports
+import { useState, useEffect } from 'react'
+
+// MUI Imports
+import Button from '@mui/material/Button'
+import Drawer from '@mui/material/Drawer'
+import IconButton from '@mui/material/IconButton'
+import MenuItem from '@mui/material/MenuItem'
+import Typography from '@mui/material/Typography'
+import Box from '@mui/material/Box'
+
+// Third-party Imports
+import { useForm, Controller } from 'react-hook-form'
+
+// Component Imports
+import CustomTextField from '@core/components/mui/TextField'
+
+// Types
+import { CampaignRule, RuleType, RewardType, CampaignRuleRequest } from '@/types/services/campaign'
+import { useCampaignRulesMutation } from '@/services/mutations/campaign'
+
+type Props = {
+ open: boolean
+ handleClose: () => void
+ campaignId: string // Required campaign ID
+ data?: CampaignRule | null // Data for edit mode
+}
+
+type FormValidateType = {
+ rule_type: RuleType
+ condition_value: string
+ reward_type: RewardType
+ reward_value: number
+ reward_subtype: string
+ reward_ref_id: string
+}
+
+// Initial form data
+const initialData: FormValidateType = {
+ rule_type: 'SPEND',
+ condition_value: '',
+ reward_type: 'POINTS',
+ reward_value: 0,
+ reward_subtype: '',
+ reward_ref_id: ''
+}
+
+const AddEditCampaignRuleDrawer = (props: Props) => {
+ // Props
+ const { open, handleClose, campaignId, data } = props
+
+ // States
+ const [isSubmitting, setIsSubmitting] = useState(false)
+
+ const { createCampaignRule, updateCampaignRule } = useCampaignRulesMutation()
+
+ // Determine if this is edit mode
+ const isEditMode = Boolean(data?.id)
+
+ // Hooks
+ const {
+ control,
+ reset: resetForm,
+ handleSubmit,
+ watch,
+ formState: { errors }
+ } = useForm
({
+ defaultValues: initialData
+ })
+
+ const watchedRewardType = watch('reward_type')
+
+ // Effect to populate form when editing
+ useEffect(() => {
+ if (isEditMode && data) {
+ const formData: FormValidateType = {
+ rule_type: data.rule_type,
+ condition_value: data.condition_value || '',
+ reward_type: data.reward_type,
+ reward_value: data.reward_value || 0,
+ reward_subtype: data.reward_subtype || '',
+ reward_ref_id: data.reward_ref_id || ''
+ }
+
+ resetForm(formData)
+ } else {
+ // Reset to initial data for add mode
+ resetForm(initialData)
+ }
+ }, [data, isEditMode, resetForm])
+
+ const handleFormSubmit = async (formData: FormValidateType) => {
+ try {
+ setIsSubmitting(true)
+
+ // Create CampaignRuleRequest object
+ const ruleRequest: CampaignRuleRequest = {
+ campaign_id: campaignId,
+ rule_type: formData.rule_type,
+ condition_value: formData.condition_value.trim() || undefined,
+ reward_type: formData.reward_type,
+ reward_value: formData.reward_value > 0 ? formData.reward_value : undefined,
+ reward_subtype: formData.reward_subtype.trim() || undefined,
+ reward_ref_id: formData.reward_ref_id.trim() || undefined
+ }
+
+ if (isEditMode && data?.id) {
+ // Update existing campaign rule
+ updateCampaignRule.mutate(
+ { id: data.id, payload: ruleRequest },
+ {
+ onSuccess: () => {
+ handleReset()
+ handleClose()
+ },
+ onError: error => {
+ console.error('Error updating campaign rule:', error)
+ }
+ }
+ )
+ } else {
+ // Create new campaign rule
+ createCampaignRule.mutate(ruleRequest, {
+ onSuccess: () => {
+ handleReset()
+ handleClose()
+ },
+ onError: error => {
+ console.error('Error creating campaign rule:', error)
+ }
+ })
+ }
+ } catch (error) {
+ console.error('Error submitting campaign rule:', error)
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ const handleReset = () => {
+ handleClose()
+ resetForm(initialData)
+ }
+
+ // Helper function to get rule type options
+ const getRuleTypeOptions = () => [
+ { value: 'TIER', label: 'Tier Based', icon: 'tabler-medal' },
+ { value: 'SPEND', label: 'Spending Amount', icon: 'tabler-wallet' },
+ { value: 'PRODUCT', label: 'Product Based', icon: 'tabler-package' },
+ { value: 'CATEGORY', label: 'Category Based', icon: 'tabler-category' },
+ { value: 'DAY', label: 'Day Based', icon: 'tabler-calendar' },
+ { value: 'LOCATION', label: 'Location Based', icon: 'tabler-map-pin' }
+ ]
+
+ // Helper function to get reward type options
+ const getRewardTypeOptions = () => [
+ { value: 'POINTS', label: 'Points', icon: 'tabler-coins' },
+ { value: 'TOKENS', label: 'Tokens', icon: 'tabler-ticket' },
+ { value: 'REWARD', label: 'Reward Item', icon: 'tabler-gift' }
+ ]
+
+ // Helper function to get condition placeholder based on rule type
+ const getConditionPlaceholder = (ruleType: RuleType) => {
+ switch (ruleType) {
+ case 'TIER':
+ return 'e.g., GOLD, SILVER, BRONZE'
+ case 'SPEND':
+ return 'e.g., 100000 (minimum spend amount)'
+ case 'PRODUCT':
+ return 'e.g., product_id_123'
+ case 'CATEGORY':
+ return 'e.g., electronics, fashion'
+ case 'DAY':
+ return 'e.g., monday, weekend'
+ case 'LOCATION':
+ return 'e.g., jakarta, bandung'
+ default:
+ return 'Enter condition value'
+ }
+ }
+
+ // Helper function to get reward subtype options based on reward type
+ const getRewardSubtypeOptions = (rewardType: RewardType) => {
+ switch (rewardType) {
+ case 'POINTS':
+ return ['bonus', 'cashback', 'referral']
+ case 'TOKENS':
+ return ['game', 'lottery', 'voucher']
+ case 'REWARD':
+ return ['product', 'discount', 'currency', 'experience']
+ default:
+ return []
+ }
+ }
+
+ return (
+
+ {/* Sticky Header */}
+
+
+ {isEditMode ? 'Edit Campaign Rule' : 'Add New Campaign Rule'}
+
+
+
+
+
+
+ {/* Scrollable Content */}
+
+
+
+
+ {/* Sticky Footer */}
+
+
+
+
+
+
+
+ )
+}
+
+export default AddEditCampaignRuleDrawer
diff --git a/src/views/apps/marketing/campaign/detail/index.tsx b/src/views/apps/marketing/campaign/detail/index.tsx
new file mode 100644
index 0000000..d7d8d85
--- /dev/null
+++ b/src/views/apps/marketing/campaign/detail/index.tsx
@@ -0,0 +1,401 @@
+'use client'
+
+import Loading from '@/components/layout/shared/Loading'
+import { useCampaignById, useCampaignRulesByCampaignId } from '@/services/queries/campaign'
+import { CampaignRule, RewardType, RuleType } from '@/types/services/campaign'
+import { formatCurrency } from '@/utils/transform'
+import {
+ Avatar,
+ Card,
+ CardHeader,
+ CardContent,
+ Chip,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ Box,
+ Divider,
+ Button,
+ IconButton,
+ Tooltip,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ DialogContentText
+} from '@mui/material'
+// Using Tabler icons instead of Material-UI icons
+import { useParams } from 'next/navigation'
+import { useState } from 'react'
+
+const CampaignDetailContent = () => {
+ const params = useParams()
+
+ // State for dialogs
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+ const [selectedRuleId, setSelectedRuleId] = useState(null)
+ const [selectedRule, setSelectedRule] = useState(null)
+
+ const { data: campaign, isLoading, error } = useCampaignById(params?.id as string)
+ const { data: rules, isLoading: rulesLoading, error: rulesError } = useCampaignRulesByCampaignId(params?.id as string)
+
+ if (isLoading) return
+
+ // Action handlers
+ const handleAddRule = () => {
+ // TODO: Implement add rule functionality
+ // Could open a modal/form or navigate to add rule page
+ console.log('Add new rule for campaign:', params?.id)
+ }
+
+ const handleEditRule = (rule: CampaignRule) => {
+ // TODO: Implement edit rule functionality
+ // Could open a modal/form or navigate to edit rule page
+ console.log('Edit rule:', rule.id)
+ setSelectedRule(rule)
+ // You might want to open an edit modal here
+ }
+
+ const handleDeleteRule = (rule: CampaignRule) => {
+ setSelectedRule(rule)
+ setSelectedRuleId(rule.id)
+ setDeleteDialogOpen(true)
+ }
+
+ const confirmDeleteRule = async () => {
+ if (selectedRuleId) {
+ try {
+ // TODO: Implement actual delete API call
+ console.log('Deleting rule:', selectedRuleId)
+ // await deleteRuleMutation.mutateAsync(selectedRuleId)
+
+ // Close dialog and reset state
+ setDeleteDialogOpen(false)
+ setSelectedRuleId(null)
+ setSelectedRule(null)
+
+ // Show success message or refresh data
+ } catch (error) {
+ console.error('Failed to delete rule:', error)
+ // Handle error (show toast, etc.)
+ }
+ }
+ }
+
+ const cancelDelete = () => {
+ setDeleteDialogOpen(false)
+ setSelectedRuleId(null)
+ setSelectedRule(null)
+ }
+
+ // Helper function to format rule type display
+ const formatRuleType = (ruleType: RuleType) => {
+ const ruleTypeLabels: Record = {
+ TIER: 'Tier Based',
+ SPEND: 'Spending Amount',
+ PRODUCT: 'Product Based',
+ CATEGORY: 'Category Based',
+ DAY: 'Day Based',
+ LOCATION: 'Location Based'
+ }
+ return ruleTypeLabels[ruleType] || ruleType
+ }
+
+ // Helper function to get rule type color
+ const getRuleTypeColor = (ruleType: RuleType): 'primary' | 'secondary' | 'success' | 'warning' | 'info' => {
+ const colorMap: Record = {
+ TIER: 'primary',
+ SPEND: 'success',
+ PRODUCT: 'info',
+ CATEGORY: 'warning',
+ DAY: 'secondary',
+ LOCATION: 'primary'
+ }
+ return colorMap[ruleType] || 'primary'
+ }
+
+ // Helper function to format reward type display
+ const formatRewardType = (rewardType: RewardType) => {
+ const rewardTypeLabels: Record = {
+ POINTS: 'Points',
+ TOKENS: 'Tokens',
+ REWARD: 'Reward Item'
+ }
+ return rewardTypeLabels[rewardType] || rewardType
+ }
+
+ // Helper function to get reward type color
+ const getRewardTypeColor = (rewardType: RewardType): 'primary' | 'secondary' | 'success' => {
+ const colorMap: Record = {
+ POINTS: 'success',
+ TOKENS: 'primary',
+ REWARD: 'secondary'
+ }
+ return colorMap[rewardType] || 'primary'
+ }
+
+ // Helper function to format reward value
+ const formatRewardValue = (rewardValue?: number, rewardType?: RewardType, rewardSubtype?: string) => {
+ if (!rewardValue) return '-'
+
+ // Format based on reward type
+ switch (rewardType) {
+ case 'POINTS':
+ return `${rewardValue.toLocaleString()} pts`
+ case 'TOKENS':
+ return `${rewardValue.toLocaleString()} tokens`
+ case 'REWARD':
+ // For reward items, check if it's currency-based
+ if (rewardSubtype === 'currency') {
+ return formatCurrency(rewardValue)
+ }
+ return rewardValue.toString()
+ default:
+ return rewardValue.toString()
+ }
+ }
+
+ // Helper function to format date
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+ }
+
+ return (
+ <>
+
+ {/* Header Card */}
+
+ }
+ title={
+
+
+ {campaign?.name} ({campaign?.type})
+
+
+
+
+
+ }
+ subheader={
+
+
+ {campaign?.start_date} - {campaign?.end_date}
+
+
+ {campaign?.description}
+
+
+ }
+ />
+
+
+ {/* Campaign Rules Table */}
+
+
+
+
+ Campaign Rules
+
+ {rules && rules.length > 0 && (
+
+ )}
+
+ }
+ onClick={handleAddRule}
+ size='small'
+ >
+ Add Rule
+
+
+ }
+ subheader={
+
+ Rules and rewards configuration for this campaign
+
+ }
+ />
+
+
+ {rulesLoading ? (
+
+
+
+ ) : rulesError ? (
+
+
+ Failed to load campaign rules
+
+
+ {rulesError.message || 'Please try refreshing the page'}
+
+
+ ) : rules && rules.length > 0 ? (
+
+
+
+
+ Rule Type
+ Condition
+ Reward Type
+ Reward Value
+ Subtype
+ Created
+ Updated
+
+ Actions
+
+
+
+
+ {rules.map((rule: CampaignRule) => (
+
+
+
+
+
+ {rule.condition_value || '-'}
+
+
+
+
+
+
+ {formatRewardValue(rule.reward_value, rule.reward_type, rule.reward_subtype)}
+
+
+
+ {rule.reward_subtype && (
+
+ )}
+
+
+
+ {formatDate(rule.created_at)}
+
+
+
+
+ {formatDate(rule.updated_at)}
+
+
+
+
+
+ handleEditRule(rule)} color='primary'>
+
+
+
+
+ handleDeleteRule(rule)} color='error'>
+
+
+
+
+
+
+ ))}
+
+
+
+ ) : (
+
+
+ No rules configured for this campaign
+
+
+ Add rules to define how rewards are distributed
+
+ }
+ onClick={handleAddRule}
+ className='mt-4'
+ >
+ Add First Rule
+
+
+ )}
+
+
+