From e55357e3057647577cf0bd5fa095d7dd2459f3fa Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 19 Sep 2025 16:10:52 +0700 Subject: [PATCH 1/3] add campaign --- src/services/api.ts | 3 +- src/types/services/campaign.ts | 10 +- src/utils/transform.ts | 10 +- .../campaign/AddEditCampaignDrawer.tsx | 401 +++--------------- 4 files changed, 72 insertions(+), 352 deletions(-) diff --git a/src/services/api.ts b/src/services/api.ts index 5783ecf..b0fb547 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -5,8 +5,9 @@ const getToken = () => { return localStorage.getItem('authToken') } +// baseURL: 'https://api-pos.apskel.id/api/v1', export const api = axios.create({ - baseURL: 'https://api-pos.apskel.id/api/v1', + baseURL: 'http://127.0.0.1:4000/api/v1', headers: { 'Content-Type': 'application/json' }, diff --git a/src/types/services/campaign.ts b/src/types/services/campaign.ts index e5657b0..8d9481c 100644 --- a/src/types/services/campaign.ts +++ b/src/types/services/campaign.ts @@ -14,8 +14,9 @@ export interface Campaign { is_active: boolean show_on_app: boolean position: number - metadata?: Record - rules?: CampaignRule[] + metadata?: { + banner_url?: string + } created_at: string // ISO string updated_at: string // ISO string } @@ -43,8 +44,9 @@ export interface CampaignRequest { is_active: boolean show_on_app: boolean position: number - metadata?: Record - rules: CampaignRuleRequest[] + metadata?: { + banner_url?: string + } } export interface CampaignRuleRequest { diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 9f1e131..aee93f4 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -43,6 +43,15 @@ export const formatDateDDMMYYYY = (dateString: Date | string) => { return `${day}-${month}-${year}` } +export const formatDateYYYYMMDD = (dateString: Date | string) => { + const date = new Date(dateString) + date.setHours(0, 0, 0, 0) + const day = String(date.getDate()).padStart(2, '0') + const month = String(date.getMonth() + 1).padStart(2, '0') + const year = date.getFullYear() + return `${year}-${month}-${day}` +} + export const formatForInputDate = (dateString: Date | string) => { const date = new Date(dateString) const day = String(date.getDate()).padStart(2, '0') @@ -51,7 +60,6 @@ export const formatForInputDate = (dateString: Date | string) => { return `${year}-${month}-${day}` } - export const formatDatetime = (dateString: string | number | Date) => { const date = new Date(dateString) diff --git a/src/views/apps/marketing/campaign/AddEditCampaignDrawer.tsx b/src/views/apps/marketing/campaign/AddEditCampaignDrawer.tsx index 477ba94..f385a00 100644 --- a/src/views/apps/marketing/campaign/AddEditCampaignDrawer.tsx +++ b/src/views/apps/marketing/campaign/AddEditCampaignDrawer.tsx @@ -7,49 +7,22 @@ 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 Divider from '@mui/material/Divider' -import Grid from '@mui/material/Grid2' import Box from '@mui/material/Box' import Switch from '@mui/material/Switch' import FormControlLabel from '@mui/material/FormControlLabel' -import Chip from '@mui/material/Chip' -import InputAdornment from '@mui/material/InputAdornment' // Third-party Imports -import { useForm, Controller, useFieldArray } from 'react-hook-form' +import { useForm, Controller } from 'react-hook-form' // Component Imports import CustomTextField from '@core/components/mui/TextField' +import ImageUpload from '@/components/ImageUpload' // Types -import { Campaign } from '@/types/services/campaign' +import { Campaign, CampaignRequest, CampaignType } from '@/types/services/campaign' import { useCampaignsMutation } from '@/services/mutations/campaign' - -// Updated Type Definitions -export type CampaignType = 'REWARD' | 'POINTS' | 'TOKENS' | 'MIXED' -export type RuleType = 'TIER' | 'SPEND' | 'PRODUCT' | 'CATEGORY' | 'DAY' | 'LOCATION' -export type RewardType = 'POINTS' | 'TOKENS' | 'REWARD' - -export interface CampaignRequest { - name: string - description?: string - type: CampaignType - start_date: string // ISO string - end_date: string // ISO string - is_active: boolean - show_on_app: boolean - position: number - metadata?: Record - rules: CampaignRuleRequest[] -} - -export interface CampaignRuleRequest { - rule_type: RuleType - condition_value?: string - reward_type: RewardType - reward_value?: number - reward_subtype?: string -} +import { useFilesMutation } from '@/services/mutations/files' +import { formatDateYYYYMMDD } from '@/utils/transform' type Props = { open: boolean @@ -66,14 +39,7 @@ type FormValidateType = { is_active: boolean show_on_app: boolean position: number - // Rules array - rules: { - rule_type: RuleType - condition_value: string - reward_type: RewardType - reward_value: number - reward_subtype: string - }[] + banner_url: string } // Initial form data @@ -86,16 +52,7 @@ const initialData: FormValidateType = { is_active: true, show_on_app: true, position: 1, - // Initial rule - rules: [ - { - rule_type: 'SPEND', - condition_value: '', - reward_type: 'POINTS', - reward_value: 0, - reward_subtype: '' - } - ] + banner_url: '' } const AddEditCampaignDrawer = (props: Props) => { @@ -105,8 +62,10 @@ const AddEditCampaignDrawer = (props: Props) => { // States const [showMore, setShowMore] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) + const [bannerUrl, setBannerUrl] = useState('') const { createCampaign, updateCampaign } = useCampaignsMutation() + const { mutate: uploadFile, isPending: isUploadPending } = useFilesMutation().uploadFile // Determine if this is edit mode const isEditMode = Boolean(data?.id) @@ -123,12 +82,6 @@ const AddEditCampaignDrawer = (props: Props) => { defaultValues: initialData }) - // Field array for rules - const { fields, append, remove } = useFieldArray({ - control, - name: 'rules' - }) - const watchedStartDate = watch('start_date') const watchedEndDate = watch('end_date') @@ -144,29 +97,16 @@ const AddEditCampaignDrawer = (props: Props) => { is_active: data.is_active ?? true, show_on_app: data.show_on_app ?? true, position: data.position || 1, - // Map existing rules - rules: data.rules?.map(rule => ({ - rule_type: rule.rule_type, - condition_value: rule.condition_value || '', - reward_type: rule.reward_type, - reward_value: rule.reward_value || 0, - reward_subtype: rule.reward_subtype || '' - })) || [ - { - rule_type: 'SPEND', - condition_value: '', - reward_type: 'POINTS', - reward_value: 0, - reward_subtype: '' - } - ] + banner_url: data.metadata?.banner_url || '' } resetForm(formData) + setBannerUrl(data.metadata?.banner_url || '') setShowMore(true) // Always show more for edit mode } else { // Reset to initial data for add mode resetForm(initialData) + setBannerUrl('') setShowMore(false) } }, [data, isEditMode, resetForm]) @@ -175,20 +115,10 @@ const AddEditCampaignDrawer = (props: Props) => { try { setIsSubmitting(true) - // Create rules array - const rulesRequest: CampaignRuleRequest[] = formData.rules.map(rule => ({ - rule_type: rule.rule_type, - condition_value: rule.condition_value || undefined, - reward_type: rule.reward_type, - reward_value: rule.reward_value || undefined, - reward_subtype: rule.reward_subtype || undefined - })) - - // Create metadata from rules if needed - const metadata: Record = {} - const spendRule = formData.rules.find(rule => rule.rule_type === 'SPEND') - if (spendRule?.condition_value) { - metadata.minPurchase = parseInt(spendRule.condition_value) + // Create metadata object + const metadata: { banner_url?: string } = {} + if (bannerUrl.trim()) { + metadata.banner_url = bannerUrl.trim() } // Create CampaignRequest object @@ -196,13 +126,12 @@ const AddEditCampaignDrawer = (props: Props) => { name: formData.name, description: formData.description || undefined, type: formData.type, - start_date: new Date(formData.start_date).toISOString(), - end_date: new Date(formData.end_date).toISOString(), + start_date: formatDateYYYYMMDD(formData.start_date), + end_date: formatDateYYYYMMDD(formData.end_date), is_active: formData.is_active, show_on_app: formData.show_on_app, position: formData.position, - metadata: Object.keys(metadata).length > 0 ? metadata : undefined, - rules: rulesRequest + metadata: Object.keys(metadata).length > 0 ? metadata : undefined } if (isEditMode && data?.id) { @@ -236,70 +165,29 @@ const AddEditCampaignDrawer = (props: Props) => { const handleReset = () => { handleClose() resetForm(initialData) + setBannerUrl('') setShowMore(false) } - const formatCurrency = (value: number) => { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0 - }).format(value) - } + // Handle file upload + const handleBannerUpload = async (file: File): Promise => { + return new Promise((resolve, reject) => { + const formData = new FormData() + formData.append('file', file) + formData.append('file_type', 'image') + formData.append('description', 'campaign banner upload') - const getRewardTypeLabel = (type: RewardType) => { - switch (type) { - case 'POINTS': - return 'Poin' - case 'TOKENS': - return 'Token' - case 'REWARD': - return 'Reward' - default: - return type - } - } - - const getRewardValuePlaceholder = (type: RewardType) => { - switch (type) { - case 'POINTS': - return 'Jumlah poin yang diberikan' - case 'TOKENS': - return 'Jumlah token yang diberikan' - case 'REWARD': - return 'Nilai reward' - default: - return 'Nilai reward' - } - } - - const getConditionValuePlaceholder = (ruleType: RuleType) => { - switch (ruleType) { - case 'SPEND': - return 'Minimum pembelian (Rupiah)' - case 'TIER': - return 'Tier pelanggan (misal: GOLD, SILVER)' - case 'PRODUCT': - return 'ID atau nama produk' - case 'CATEGORY': - return 'Kategori produk' - case 'DAY': - return 'Hari dalam seminggu (misal: MONDAY)' - case 'LOCATION': - return 'Lokasi atau kota' - default: - return 'Nilai kondisi' - } - } - - const getConditionValueInputProps = (ruleType: RuleType) => { - if (ruleType === 'SPEND') { - return { - startAdornment: Rp, - type: 'number' as const - } - } - return { type: 'text' as const } + uploadFile(formData, { + onSuccess: r => { + setBannerUrl(r.file_url) + setValue('banner_url', r.file_url) // Update form value + resolve(r.id) + }, + onError: er => { + reject(er) + } + }) + }) } return ( @@ -402,202 +290,6 @@ const AddEditCampaignDrawer = (props: Props) => { /> - {/* Rules Section */} -
- - Aturan Kampanye - - - - {fields.map((field, index) => ( - - - - Aturan {index + 1} - - {fields.length > 1 && ( - remove(index)}> - - - )} - - -
- {/* Rule Type */} -
- - Tipe Aturan * - - ( - - Minimum Pembelian - Tier Pelanggan - Produk Tertentu - Kategori Produk - Hari Tertentu - Lokasi Tertentu - - )} - /> -
- - {/* Condition Value */} -
- - Nilai Kondisi * - - { - const ruleType = watch(`rules.${index}.rule_type`) - return ( - - ) - }} - /> -
- - {/* Reward Type */} -
- - Jenis Reward * - - ( - - -
- - Points -
-
- -
- - Tokens -
-
- -
- - Reward -
-
-
- )} - /> -
- - {/* Reward Value */} -
- - Nilai Reward * - - { - const rewardType = watch(`rules.${index}.reward_type`) - return ( - Poin - ) : rewardType === 'TOKENS' ? ( - Token - ) : undefined - }} - onChange={e => field.onChange(Number(e.target.value))} - /> - ) - }} - /> -
- - {/* Reward Subtype (jika reward type adalah REWARD) */} - {watch(`rules.${index}.reward_type`) === 'REWARD' && ( -
- - Sub-tipe Reward - - ( - - Diskon Persentase - Diskon Nominal - Cashback - Gratis Ongkir - - )} - /> -
- )} -
-
- ))} -
- {/* Tanggal Mulai */}
@@ -723,6 +415,23 @@ const AddEditCampaignDrawer = (props: Props) => { />
+ {/* Banner Upload */} +
+ + Banner Kampanye + + + + Format: JPG, PNG. Ukuran maksimal: 1MB + +
+ {/* Position */}
-- 2.47.2 From 1bf2bad52c9b5940d3f6e5ddce937e89be5b3858 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 19 Sep 2025 17:38:48 +0700 Subject: [PATCH 2/3] campaign detail --- .../marketing/campaign/[id]/detail/page.tsx | 7 + src/services/mutations/campaign.ts | 50 ++- src/services/queries/campaign.ts | 12 +- src/types/services/campaign.ts | 6 +- .../marketing/campaign/CampaignListTable.tsx | 113 +---- .../detail/AddEditCampaignRuleDrawer.tsx | 417 ++++++++++++++++++ .../apps/marketing/campaign/detail/index.tsx | 401 +++++++++++++++++ 7 files changed, 890 insertions(+), 116 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/marketing/campaign/[id]/detail/page.tsx create mode 100644 src/views/apps/marketing/campaign/detail/AddEditCampaignRuleDrawer.tsx create mode 100644 src/views/apps/marketing/campaign/detail/index.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/marketing/campaign/[id]/detail/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/marketing/campaign/[id]/detail/page.tsx new file mode 100644 index 0000000..81017e4 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/marketing/campaign/[id]/detail/page.tsx @@ -0,0 +1,7 @@ +import CampaignDetailContent from '@/views/apps/marketing/campaign/detail' + +const CampaignDetailPage = () => { + return +} + +export default CampaignDetailPage diff --git a/src/services/mutations/campaign.ts b/src/services/mutations/campaign.ts index 50f7e9f..32a0c48 100644 --- a/src/services/mutations/campaign.ts +++ b/src/services/mutations/campaign.ts @@ -1,4 +1,4 @@ -import { CampaignRequest } from '@/types/services/campaign' +import { CampaignRequest, CampaignRuleRequest } from '@/types/services/campaign' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'react-toastify' import { api } from '../api' @@ -50,3 +50,51 @@ export const useCampaignsMutation = () => { return { createCampaign, updateCampaign, deleteCampaign } } + +export const useCampaignRulesMutation = () => { + const queryClient = useQueryClient() + + const createCampaignRule = useMutation({ + mutationFn: async (newCampaignRule: CampaignRuleRequest) => { + const response = await api.post('/marketing/campaign-rules', newCampaignRule) + return response.data + }, + onSuccess: () => { + toast.success('CampaignRule created successfully!') + queryClient.invalidateQueries({ queryKey: ['campaign-rules/campaign'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Create failed') + } + }) + + const updateCampaignRule = useMutation({ + mutationFn: async ({ id, payload }: { id: string; payload: CampaignRuleRequest }) => { + const response = await api.put(`/marketing/campaign-rules/${id}`, payload) + return response.data + }, + onSuccess: () => { + toast.success('CampaignRule updated successfully!') + queryClient.invalidateQueries({ queryKey: ['campaign-rules/campaign'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Update failed') + } + }) + + const deleteCampaignRule = useMutation({ + mutationFn: async (id: string) => { + const response = await api.delete(`/marketing/campaign-rules/${id}`) + return response.data + }, + onSuccess: () => { + toast.success('CampaignRule deleted successfully!') + queryClient.invalidateQueries({ queryKey: ['campaign-rules/campaign'] }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.errors?.[0]?.cause || 'Delete failed') + } + }) + + return { createCampaignRule, updateCampaignRule, deleteCampaignRule } +} diff --git a/src/services/queries/campaign.ts b/src/services/queries/campaign.ts index acd4fcf..35cf114 100644 --- a/src/services/queries/campaign.ts +++ b/src/services/queries/campaign.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { api } from '../api' -import { Campaign, Campaigns } from '@/types/services/campaign' +import { Campaign, CampaignRule, Campaigns } from '@/types/services/campaign' interface CampaignQueryParams { page?: number @@ -44,3 +44,13 @@ export function useCampaignById(id: string) { } }) } + +export function useCampaignRulesByCampaignId(id: string) { + return useQuery({ + queryKey: ['campaign-rules/campaign', id], + queryFn: async () => { + const res = await api.get(`/marketing/campaign-rules/campaign/${id}`) + return res.data.data + } + }) +} diff --git a/src/types/services/campaign.ts b/src/types/services/campaign.ts index 8d9481c..ee2e2d2 100644 --- a/src/types/services/campaign.ts +++ b/src/types/services/campaign.ts @@ -50,11 +50,13 @@ export interface CampaignRequest { } export interface CampaignRuleRequest { - rule_type: RuleType + campaign_id: string + rule_type: 'TIER' | 'SPEND' | 'PRODUCT' | 'CATEGORY' | 'DAY' | 'LOCATION' condition_value?: string - reward_type: RewardType + reward_type: 'POINTS' | 'TOKENS' | 'REWARD' reward_value?: number reward_subtype?: string + reward_ref_id?: string } export interface Campaigns { diff --git a/src/views/apps/marketing/campaign/CampaignListTable.tsx b/src/views/apps/marketing/campaign/CampaignListTable.tsx index 8f6e5a0..8f4ad1e 100644 --- a/src/views/apps/marketing/campaign/CampaignListTable.tsx +++ b/src/views/apps/marketing/campaign/CampaignListTable.tsx @@ -141,85 +141,6 @@ const getCampaignTypeColor = (type: string): ThemeColor => { } } -const getCampaignTypeIcon = (type: string): string => { - switch (type) { - case 'POINTS': - return 'tabler-coins' - case 'TOKENS': - return 'tabler-ticket' - case 'REWARD': - return 'tabler-gift' - case 'MIXED': - return 'tabler-layers-intersect' - default: - return 'tabler-tag' - } -} - -const getRewardTypeColor = (rewardType: string): ThemeColor => { - switch (rewardType) { - case 'POINTS': - return 'primary' - case 'TOKENS': - return 'success' - case 'REWARD': - return 'warning' - default: - return 'info' - } -} - -const getRewardTypeIcon = (rewardType: string): string => { - switch (rewardType) { - case 'POINTS': - return 'tabler-coins' - case 'TOKENS': - return 'tabler-ticket' - case 'REWARD': - return 'tabler-percentage' - default: - return 'tabler-gift' - } -} - -const formatRewardValue = (rewardType: string, rewardValue?: number, rewardSubtype?: string): string => { - if (!rewardValue) return '-' - - switch (rewardType) { - case 'POINTS': - return `${formatPoints(rewardValue)} Poin` - case 'TOKENS': - return formatCurrency(rewardValue) - case 'REWARD': - if (rewardSubtype === 'DISCOUNT_PERCENT') { - return `${rewardValue}%` - } - return formatCurrency(rewardValue) - default: - return rewardValue.toString() - } -} - -const getMinimumPurchase = (campaign: Campaign): number => { - // Check rules for spend condition - const spendRule = campaign.rules?.find(rule => rule.rule_type === 'SPEND') - if (spendRule?.condition_value) { - return parseInt(spendRule.condition_value) - } - - // Fallback to metadata - return campaign.metadata?.minPurchase || 0 -} - -const getPrimaryReward = (campaign: Campaign): { type: string; value?: number; subtype?: string } => { - const primaryRule = campaign.rules?.[0] - return { - type: primaryRule?.reward_type || 'POINTS', - value: primaryRule?.reward_value, - subtype: primaryRule?.reward_subtype - } -} - // Column Definitions const columnHelper = createColumnHelper() @@ -331,7 +252,7 @@ const CampaignListTable = () => { cell: ({ row }) => (
- + {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 */} + +
+
+ {/* Rule Type */} +
+ + Rule Type * + + ( + + {getRuleTypeOptions().map(option => ( + +
+ + {option.label} +
+
+ ))} +
+ )} + /> +
+ + {/* Condition Value */} +
+ + Condition Value + + ( + + )} + /> +
+ + {/* Reward Type */} +
+ + Reward Type * + + ( + + {getRewardTypeOptions().map(option => ( + +
+ + {option.label} +
+
+ ))} +
+ )} + /> +
+ + {/* Reward Value */} +
+ + Reward Value + + ( + field.onChange(Number(e.target.value))} + /> + )} + /> +
+ + {/* Reward Subtype */} +
+ + Reward Subtype + + ( + + + None + + {getRewardSubtypeOptions(watchedRewardType).map(subtype => ( + + {subtype.charAt(0).toUpperCase() + subtype.slice(1)} + + ))} + + )} + /> +
+ + {/* Reward Reference ID */} +
+ + Reward Reference ID + + ( + + )} + /> +
+
+ +
+ + {/* 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 && ( + + )} +
+ +
+ } + 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 + + + + )} +
+ +
+ + {/* Delete Confirmation Dialog */} + + Confirm Delete Rule + + + Are you sure you want to delete this rule? This action cannot be undone. + {selectedRule && ( + + + Rule Details: + + + Type: {formatRuleType(selectedRule.rule_type)} + + + Reward:{' '} + {formatRewardValue(selectedRule.reward_value, selectedRule.reward_type, selectedRule.reward_subtype)} + + + )} + + + + + + + + + ) +} + +export default CampaignDetailContent -- 2.47.2 From 9b92f4742f96f70b7e05fd4c31cb58b8ee64556c Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 20 Sep 2025 18:03:11 +0700 Subject: [PATCH 3/3] add printer type --- src/views/apps/ecommerce/products/add/ProductOrganize.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/views/apps/ecommerce/products/add/ProductOrganize.tsx b/src/views/apps/ecommerce/products/add/ProductOrganize.tsx index 80f55db..ea6bafc 100644 --- a/src/views/apps/ecommerce/products/add/ProductOrganize.tsx +++ b/src/views/apps/ecommerce/products/add/ProductOrganize.tsx @@ -82,6 +82,8 @@ const ProductOrganize = () => { onChange={e => handleSelectChange('printer_type', e.target.value)} > Kitchen + Bar + Ticket -- 2.47.2