diff --git a/src/app/[lang]/(dashboard)/(private)/apps/marketing/voucher/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/marketing/voucher/page.tsx new file mode 100644 index 0000000..0d76719 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/marketing/voucher/page.tsx @@ -0,0 +1,7 @@ +import VoucherList from '@/views/apps/marketing/voucher' + +const VoucherPage = () => { + return +} + +export default VoucherPage diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 5c4ce34..2234b27 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -165,6 +165,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => { {dictionary['navigation'].customer_analytics} + {dictionary['navigation'].voucher} }> diff --git a/src/data/dictionaries/en.json b/src/data/dictionaries/en.json index ecaea77..be52350 100644 --- a/src/data/dictionaries/en.json +++ b/src/data/dictionaries/en.json @@ -133,6 +133,7 @@ "gamification": "Gamification", "wheel_spin": "Wheel Spin", "campaign": "Campaign", - "customer_analytics": "Customer Analytics" + "customer_analytics": "Customer Analytics", + "voucher": "Voucher" } } diff --git a/src/data/dictionaries/id.json b/src/data/dictionaries/id.json index 9b0a4a0..66f3bcf 100644 --- a/src/data/dictionaries/id.json +++ b/src/data/dictionaries/id.json @@ -133,6 +133,7 @@ "gamification": "Gamifikasi", "wheel_spin": "Wheel Spin", "campaign": "Kampanye", - "customer_analytics": "Analisis Pelanggan" + "customer_analytics": "Analisis Pelanggan", + "voucher": "Vocher" } } diff --git a/src/types/services/voucher.ts b/src/types/services/voucher.ts index 03eb4b9..9a4c13b 100644 --- a/src/types/services/voucher.ts +++ b/src/types/services/voucher.ts @@ -20,4 +20,16 @@ export interface VoucherApiResponse { success: boolean data: VoucherRowsResponse errors: any -} \ No newline at end of file +} + +export interface VoucherType { + id: number + code: string + type: 'discount' | 'cashback' | 'free_shipping' | 'product' + discountType?: 'fixed' | 'percent' + discountValue?: number + minPurchase?: number + validFrom: string + validUntil: string + isActive: boolean +} diff --git a/src/views/apps/marketing/voucher/AddEditVoucherDrawer.tsx b/src/views/apps/marketing/voucher/AddEditVoucherDrawer.tsx new file mode 100644 index 0000000..6870648 --- /dev/null +++ b/src/views/apps/marketing/voucher/AddEditVoucherDrawer.tsx @@ -0,0 +1,896 @@ +// 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 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' +import Avatar from '@mui/material/Avatar' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import FormHelperText from '@mui/material/FormHelperText' + +// Third-party Imports +import { useForm, Controller } from 'react-hook-form' + +// Component Imports +import CustomTextField from '@core/components/mui/TextField' + +// Types - Updated to match the integrated voucher structure +export interface VoucherCatalogType { + id: string + name: string + description?: string + pointCost: number + stock?: number + isActive: boolean + validUntil?: Date + imageUrl?: string + createdAt: Date + updatedAt: Date + // Voucher-specific fields + code: string + type: 'discount' | 'cashback' | 'free_shipping' | 'product' + discountType?: 'fixed' | 'percent' + discountValue?: number + minPurchase?: number + validFrom: string +} + +export interface VoucherRequest { + name: string + description?: string + pointCost: number + stock?: number + isActive: boolean + validUntil?: Date + imageUrl?: string + code: string + type: 'discount' | 'cashback' | 'free_shipping' | 'product' + discountType?: 'fixed' | 'percent' + discountValue?: number + minPurchase?: number + validFrom: string + terms?: string +} + +type Props = { + open: boolean + handleClose: () => void + data?: VoucherCatalogType // Data voucher untuk edit (jika ada) +} + +type FormValidateType = { + name: string + description: string + pointCost: number + stock: number | '' + isActive: boolean + validUntil: string + imageUrl: string + code: string + type: 'discount' | 'cashback' | 'free_shipping' | 'product' + discountType: 'fixed' | 'percent' + discountValue: number | '' + minPurchase: number | '' + validFrom: string + terms: string + hasUnlimitedStock: boolean + hasValidUntil: boolean + hasMinPurchase: boolean +} + +// Initial form data +const initialData: FormValidateType = { + name: '', + description: '', + pointCost: 100, + stock: '', + isActive: true, + validUntil: '', + imageUrl: '', + code: '', + type: 'discount', + discountType: 'fixed', + discountValue: '', + minPurchase: '', + validFrom: new Date().toISOString().split('T')[0], + terms: '', + hasUnlimitedStock: false, + hasValidUntil: false, + hasMinPurchase: false +} + +// Mock mutation hooks (replace with actual hooks) +const useVoucherMutation = () => { + const createVoucher = { + mutate: (data: VoucherRequest, options?: { onSuccess?: () => void }) => { + console.log('Creating voucher:', data) + setTimeout(() => options?.onSuccess?.(), 1000) + } + } + + const updateVoucher = { + mutate: (data: { id: string; payload: VoucherRequest }, options?: { onSuccess?: () => void }) => { + console.log('Updating voucher:', data) + setTimeout(() => options?.onSuccess?.(), 1000) + } + } + + return { createVoucher, updateVoucher } +} + +// Voucher types +const VOUCHER_TYPES = [ + { value: 'discount', label: 'Diskon', icon: 'tabler-percentage' }, + { value: 'cashback', label: 'Cashback', icon: 'tabler-cash' }, + { value: 'free_shipping', label: 'Gratis Ongkir', icon: 'tabler-truck-delivery' }, + { value: 'product', label: 'Produk Fisik', icon: 'tabler-package' } +] + +// Discount types +const DISCOUNT_TYPES = [ + { value: 'fixed', label: 'Nilai Tetap (Rp)' }, + { value: 'percent', label: 'Persentase (%)' } +] + +const AddEditVoucherDrawer = (props: Props) => { + // Props + const { open, handleClose, data } = props + + // States + const [showMore, setShowMore] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [imagePreview, setImagePreview] = useState(null) + + const { createVoucher, updateVoucher } = useVoucherMutation() + + // Determine if this is edit mode + const isEditMode = Boolean(data?.id) + + // Hooks + const { + control, + reset: resetForm, + handleSubmit, + watch, + setValue, + formState: { errors } + } = useForm({ + defaultValues: initialData + }) + + const watchedImageUrl = watch('imageUrl') + const watchedHasUnlimitedStock = watch('hasUnlimitedStock') + const watchedHasValidUntil = watch('hasValidUntil') + const watchedHasMinPurchase = watch('hasMinPurchase') + const watchedStock = watch('stock') + const watchedPointCost = watch('pointCost') + const watchedType = watch('type') + const watchedDiscountType = watch('discountType') + const watchedDiscountValue = watch('discountValue') + + // Effect to populate form when editing + useEffect(() => { + if (isEditMode && data) { + // Populate form with existing data + const formData: FormValidateType = { + name: data.name || '', + description: data.description || '', + pointCost: data.pointCost || 100, + stock: data.stock ?? '', + isActive: data.isActive ?? true, + validUntil: data.validUntil ? new Date(data.validUntil).toISOString().split('T')[0] : '', + imageUrl: data.imageUrl || '', + code: data.code || '', + type: data.type || 'discount', + discountType: data.discountType || 'fixed', + discountValue: data.discountValue ?? '', + minPurchase: data.minPurchase ?? '', + validFrom: data.validFrom + ? new Date(data.validFrom).toISOString().split('T')[0] + : new Date().toISOString().split('T')[0], + terms: '', + hasUnlimitedStock: data.stock === undefined || data.stock === null, + hasValidUntil: Boolean(data.validUntil), + hasMinPurchase: Boolean(data.minPurchase) + } + + resetForm(formData) + setShowMore(true) // Always show more for edit mode + setImagePreview(data.imageUrl || null) + } else { + // Reset to initial data for add mode + resetForm(initialData) + setShowMore(false) + setImagePreview(null) + } + }, [data, isEditMode, resetForm]) + + // Handle image URL change + useEffect(() => { + if (watchedImageUrl) { + setImagePreview(watchedImageUrl) + } else { + setImagePreview(null) + } + }, [watchedImageUrl]) + + // Handle unlimited stock toggle + useEffect(() => { + if (watchedHasUnlimitedStock) { + setValue('stock', '') + } + }, [watchedHasUnlimitedStock, setValue]) + + // Handle valid until toggle + useEffect(() => { + if (!watchedHasValidUntil) { + setValue('validUntil', '') + } + }, [watchedHasValidUntil, setValue]) + + // Handle minimum purchase toggle + useEffect(() => { + if (!watchedHasMinPurchase) { + setValue('minPurchase', '') + } + }, [watchedHasMinPurchase, setValue]) + + // Auto-generate voucher code + const generateVoucherCode = () => { + const prefix = watchedType.toUpperCase() + const randomSuffix = Math.random().toString(36).substring(2, 8).toUpperCase() + return `${prefix}_${randomSuffix}` + } + + const handleFormSubmit = async (formData: FormValidateType) => { + try { + setIsSubmitting(true) + + // Create VoucherRequest object + const voucherRequest: VoucherRequest = { + name: formData.name, + description: formData.description || undefined, + pointCost: formData.pointCost, + stock: formData.hasUnlimitedStock ? undefined : (formData.stock as number) || undefined, + isActive: formData.isActive, + validUntil: formData.hasValidUntil && formData.validUntil ? new Date(formData.validUntil) : undefined, + imageUrl: formData.imageUrl || undefined, + code: formData.code, + type: formData.type, + discountType: ['discount', 'cashback'].includes(formData.type) ? formData.discountType : undefined, + discountValue: + ['discount', 'cashback'].includes(formData.type) && formData.discountValue + ? Number(formData.discountValue) + : undefined, + minPurchase: formData.hasMinPurchase && formData.minPurchase ? Number(formData.minPurchase) : undefined, + validFrom: formData.validFrom, + terms: formData.terms || undefined + } + + if (isEditMode && data?.id) { + // Update existing voucher + updateVoucher.mutate( + { id: data.id, payload: voucherRequest }, + { + onSuccess: () => { + handleReset() + handleClose() + } + } + ) + } else { + // Create new voucher + createVoucher.mutate(voucherRequest, { + onSuccess: () => { + handleReset() + handleClose() + } + }) + } + } catch (error) { + console.error('Error submitting voucher:', error) + // Handle error (show toast, etc.) + } finally { + setIsSubmitting(false) + } + } + + const handleReset = () => { + handleClose() + resetForm(initialData) + setShowMore(false) + setImagePreview(null) + } + + const formatPoints = (value: number) => { + return value.toLocaleString('id-ID') + ' poin' + } + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR' + }).format(value) + } + + const getStockDisplay = () => { + if (watchedHasUnlimitedStock) return 'Unlimited' + if (watchedStock === '' || watchedStock === 0) return 'Tidak ada stok' + return `${watchedStock} item` + } + + const getDiscountValueDisplay = () => { + if (!watchedDiscountValue) return '' + if (watchedDiscountType === 'percent') { + return `${watchedDiscountValue}%` + } else { + return formatCurrency(Number(watchedDiscountValue)) + } + } + + const shouldShowDiscountFields = ['discount', 'cashback'].includes(watchedType) + + return ( + + {/* Sticky Header */} + +
+ {isEditMode ? 'Edit Voucher' : 'Tambah Voucher Baru'} + + + +
+
+ + {/* Scrollable Content */} + +
+
+ {/* Image Preview */} + {imagePreview && ( + + + + Preview Gambar + + + + + + + )} + + {/* Nama Voucher */} +
+ + Nama Voucher * + + ( + + )} + /> +
+ + {/* Tipe Voucher */} +
+ + Tipe Voucher * + + ( + + {VOUCHER_TYPES.map(type => ( + +
+ + {type.label} +
+
+ ))} +
+ )} + /> +
+ + {/* Kode Voucher */} +
+ + Kode Voucher * + + ( + + + + ) + }} + onChange={e => field.onChange(e.target.value.toUpperCase())} + /> + )} + /> +
+ + {/* Discount Fields - Only show for discount and cashback */} + {shouldShowDiscountFields && ( + <> + {/* Tipe Diskon */} +
+ + Tipe Diskon * + + ( + + {DISCOUNT_TYPES.map(type => ( + + {type.label} + + ))} + + )} + /> +
+ + {/* Nilai Diskon */} +
+ + Nilai Diskon * + + ( + + {watchedDiscountType === 'percent' ? '%' : 'Rp'} + + ) + }} + onChange={e => field.onChange(e.target.value ? Number(e.target.value) : '')} + value={field.value === '' ? '' : field.value} + /> + )} + /> +
+ + )} + + {/* Point Cost */} +
+ + Biaya Poin * + + ( + 0 ? formatPoints(field.value) : '')} + InputProps={{ + startAdornment: ( + + + + ) + }} + onChange={e => field.onChange(Number(e.target.value))} + /> + )} + /> +
+ + {/* Valid From */} +
+ + Berlaku Mulai * + + ( + + )} + /> +
+ + {/* Minimum Purchase */} +
+ + Pembelian Minimum + + ( + } + label='Memiliki syarat pembelian minimum' + className='mb-2' + /> + )} + /> + {watchedHasMinPurchase && ( + ( + Rp + }} + onChange={e => field.onChange(e.target.value ? Number(e.target.value) : '')} + value={field.value === '' ? '' : field.value} + /> + )} + /> + )} +
+ + {/* Stock Management */} +
+ + Manajemen Stok + + ( + } + label='Stok Unlimited' + className='mb-2' + /> + )} + /> + {!watchedHasUnlimitedStock && ( + ( + Qty + }} + onChange={e => field.onChange(e.target.value ? Number(e.target.value) : '')} + value={field.value === '' ? '' : field.value} + /> + )} + /> + )} +
+ + {/* Status Aktif */} +
+ ( + } + label='Voucher Aktif' + /> + )} + /> +
+ + {/* Tampilkan selengkapnya */} + {!showMore && ( + + )} + + {/* Konten tambahan */} + {showMore && ( + <> + {/* Description */} +
+ + Deskripsi Voucher + + ( + + )} + /> +
+ + {/* Image URL */} +
+ + URL Gambar + + ( + + + + ) + }} + /> + )} + /> +
+ + {/* Valid Until */} +
+ + Masa Berlaku + + ( + } + label='Memiliki batas waktu' + className='mb-2' + /> + )} + /> + {watchedHasValidUntil && ( + ( + + )} + /> + )} +
+ + {/* Terms & Conditions */} +
+ + Syarat & Ketentuan + + ( + + )} + /> +
+ + {/* Sembunyikan */} + + + )} +
+ +
+ + {/* Sticky Footer */} + +
+ + +
+
+
+ ) +} + +export default AddEditVoucherDrawer diff --git a/src/views/apps/marketing/voucher/VoucherListTable.tsx b/src/views/apps/marketing/voucher/VoucherListTable.tsx new file mode 100644 index 0000000..8b5e385 --- /dev/null +++ b/src/views/apps/marketing/voucher/VoucherListTable.tsx @@ -0,0 +1,755 @@ +'use client' + +// React Imports +import { useEffect, useState, useMemo, useCallback } from 'react' + +// Next Imports +import Link from 'next/link' +import { useParams } from 'next/navigation' + +// MUI Imports +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import Button from '@mui/material/Button' +import Typography from '@mui/material/Typography' +import Chip from '@mui/material/Chip' +import Checkbox from '@mui/material/Checkbox' +import IconButton from '@mui/material/IconButton' +import { styled } from '@mui/material/styles' +import TablePagination from '@mui/material/TablePagination' +import type { TextFieldProps } from '@mui/material/TextField' +import MenuItem from '@mui/material/MenuItem' + +// Third-party Imports +import classnames from 'classnames' +import { rankItem } from '@tanstack/match-sorter-utils' +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, + getFilteredRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFacetedMinMaxValues, + getPaginationRowModel, + getSortedRowModel +} from '@tanstack/react-table' +import type { ColumnDef, FilterFn } from '@tanstack/react-table' +import type { RankingInfo } from '@tanstack/match-sorter-utils' + +// Type Imports +import type { ThemeColor } from '@core/types' +import type { Locale } from '@configs/i18n' + +// Component Imports +import OptionMenu from '@core/components/option-menu' +import TablePaginationComponent from '@components/TablePaginationComponent' +import CustomTextField from '@core/components/mui/TextField' +import CustomAvatar from '@core/components/mui/Avatar' + +// Util Imports +import { getInitials } from '@/utils/getInitials' +import { getLocalizedUrl } from '@/utils/i18n' +import { formatCurrency } from '@/utils/transform' + +// Style Imports +import tableStyles from '@core/styles/table.module.css' +import Loading from '@/components/layout/shared/Loading' +import AddEditVoucherDrawer from './AddEditVoucherDrawer' + +// Voucher Type Interface +export interface VoucherType { + id: number + code: string + type: 'discount' | 'cashback' | 'free_shipping' | 'product' + discountType?: 'fixed' | 'percent' + discountValue?: number + minPurchase?: number + validFrom: string + validUntil: string + isActive: boolean +} + +// Main Voucher Catalog Type Interface +export interface VoucherCatalogType { + id: string + name: string + description?: string + pointCost: number + stock?: number + isActive: boolean + validUntil?: Date + imageUrl?: string + createdAt: Date + updatedAt: Date + // Voucher-specific fields + code: string + type: 'discount' | 'cashback' | 'free_shipping' | 'product' + discountType?: 'fixed' | 'percent' + discountValue?: number + minPurchase?: number + validFrom: string +} + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn + } + interface FilterMeta { + itemRank: RankingInfo + } +} + +type VoucherCatalogTypeWithAction = VoucherCatalogType & { + action?: string +} + +// Styled Components +const Icon = styled('i')({}) + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +const DebouncedInput = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit) => { + // States + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + return setValue(e.target.value)} /> +} + +// Updated dummy data with integrated voucher information +const DUMMY_VOUCHER_DATA: VoucherCatalogType[] = [ + { + id: '1', + name: 'Voucher Diskon 50K', + description: 'Voucher diskon Rp 50.000 untuk pembelian minimal Rp 200.000', + pointCost: 500, + stock: 100, + isActive: true, + validUntil: new Date('2024-12-31'), + imageUrl: 'https://example.com/voucher-50k.jpg', + createdAt: new Date('2024-01-15'), + updatedAt: new Date('2024-02-10'), + code: 'DISC50K', + type: 'discount', + discountType: 'fixed', + discountValue: 50000, + minPurchase: 200000, + validFrom: '2024-01-15' + }, + { + id: '2', + name: 'Free Shipping Voucher', + description: 'Gratis ongkos kirim untuk seluruh Indonesia', + pointCost: 200, + stock: 500, + isActive: true, + validUntil: new Date('2024-06-30'), + imageUrl: 'https://example.com/free-shipping.jpg', + createdAt: new Date('2024-01-20'), + updatedAt: new Date('2024-02-15'), + code: 'FREESHIP', + type: 'free_shipping', + validFrom: '2024-01-20' + }, + { + id: '3', + name: 'Bluetooth Speaker Premium', + description: 'Speaker bluetooth kualitas premium dengan bass yang menggelegar', + pointCost: 2500, + stock: 25, + isActive: true, + validUntil: new Date('2024-09-30'), + imageUrl: 'https://example.com/bluetooth-speaker.jpg', + createdAt: new Date('2024-01-25'), + updatedAt: new Date('2024-02-20'), + code: 'SPEAKER25', + type: 'product', + validFrom: '2024-01-25' + }, + { + id: '4', + name: 'Voucher Cashback 20%', + description: 'Cashback 20% maksimal Rp 100.000 untuk kategori elektronik', + pointCost: 800, + stock: 200, + isActive: true, + validUntil: new Date('2024-08-31'), + createdAt: new Date('2024-02-01'), + updatedAt: new Date('2024-02-25'), + code: 'CASHBACK20', + type: 'cashback', + discountType: 'percent', + discountValue: 20, + minPurchase: 100000, + validFrom: '2024-02-01' + }, + { + id: '5', + name: 'Smartwatch Fitness', + description: 'Smartwatch dengan fitur fitness tracking dan heart rate monitor', + pointCost: 5000, + stock: 15, + isActive: true, + validUntil: new Date('2024-12-31'), + createdAt: new Date('2024-02-05'), + updatedAt: new Date('2024-03-01'), + code: 'WATCH15', + type: 'product', + validFrom: '2024-02-05' + }, + { + id: '6', + name: 'Tumbler Stainless Premium', + description: 'Tumbler stainless steel 500ml dengan desain eksklusif', + pointCost: 1200, + stock: 50, + isActive: true, + validUntil: new Date('2024-10-31'), + createdAt: new Date('2024-02-10'), + updatedAt: new Date('2024-03-05'), + code: 'TUMBLER50', + type: 'product', + validFrom: '2024-02-10' + }, + { + id: '7', + name: 'Gift Card 100K', + description: 'Gift card senilai Rp 100.000 yang bisa digunakan untuk semua produk', + pointCost: 1000, + stock: 300, + isActive: true, + validUntil: new Date('2024-12-31'), + createdAt: new Date('2024-02-15'), + updatedAt: new Date('2024-03-10'), + code: 'GIFT100K', + type: 'discount', + discountType: 'fixed', + discountValue: 100000, + validFrom: '2024-02-15' + }, + { + id: '8', + name: 'Wireless Earbuds', + description: 'Earbuds wireless dengan noise cancellation dan case charging', + pointCost: 3500, + stock: 30, + isActive: true, + validUntil: new Date('2024-11-30'), + createdAt: new Date('2024-03-01'), + updatedAt: new Date('2024-03-15'), + code: 'EARBUDS30', + type: 'product', + validFrom: '2024-03-01' + }, + { + id: '9', + name: 'Voucher Buy 1 Get 1', + description: 'Beli 1 gratis 1 untuk kategori fashion wanita', + pointCost: 600, + stock: 150, + isActive: false, + validUntil: new Date('2024-07-31'), + createdAt: new Date('2024-03-05'), + updatedAt: new Date('2024-03-20'), + code: 'BUY1GET1', + type: 'discount', + discountType: 'percent', + discountValue: 50, + minPurchase: 50000, + validFrom: '2024-03-05' + }, + { + id: '10', + name: 'Power Bank 20000mAh', + description: 'Power bank fast charging 20000mAh dengan 3 port USB', + pointCost: 1800, + stock: 40, + isActive: true, + validUntil: new Date('2024-12-31'), + createdAt: new Date('2024-03-10'), + updatedAt: new Date('2024-03-25'), + code: 'POWERBANK40', + type: 'product', + validFrom: '2024-03-10' + } +] + +// Helper function to get voucher type display text +const getVoucherTypeDisplay = (type: string) => { + const typeMap = { + discount: 'Diskon', + cashback: 'Cashback', + free_shipping: 'Gratis Ongkir', + product: 'Produk' + } + return typeMap[type as keyof typeof typeMap] || type +} + +// Helper function to get voucher value display +const getVoucherValueDisplay = (voucher: VoucherCatalogType) => { + if (voucher.type === 'free_shipping') return 'Gratis Ongkir' + if (voucher.type === 'product') return 'Produk Fisik' + + if (voucher.discountValue) { + if (voucher.discountType === 'percent') { + return `${voucher.discountValue}%` + } else { + return formatCurrency(voucher.discountValue) + } + } + + return '-' +} + +// Mock data hook with dummy data +const useVoucherCatalog = ({ page, limit, search }: { page: number; limit: number; search: string }) => { + const [isLoading, setIsLoading] = useState(false) + + // Simulate loading + useEffect(() => { + setIsLoading(true) + const timer = setTimeout(() => setIsLoading(false), 500) + return () => clearTimeout(timer) + }, [page, limit, search]) + + // Filter data based on search + const filteredData = useMemo(() => { + if (!search) return DUMMY_VOUCHER_DATA + + return DUMMY_VOUCHER_DATA.filter( + voucher => + voucher.name.toLowerCase().includes(search.toLowerCase()) || + voucher.description?.toLowerCase().includes(search.toLowerCase()) || + voucher.code.toLowerCase().includes(search.toLowerCase()) + ) + }, [search]) + + // Paginate data + const paginatedData = useMemo(() => { + const startIndex = (page - 1) * limit + const endIndex = startIndex + limit + return filteredData.slice(startIndex, endIndex) + }, [filteredData, page, limit]) + + return { + data: { + vouchers: paginatedData, + total_count: filteredData.length + }, + isLoading, + error: null, + isFetching: isLoading + } +} + +// Column Definitions +const columnHelper = createColumnHelper() + +const VoucherListTable = () => { + // States + const [addVoucherOpen, setAddVoucherOpen] = useState(false) + const [editVoucherData, setEditVoucherData] = useState(undefined) + const [rowSelection, setRowSelection] = useState({}) + const [globalFilter, setGlobalFilter] = useState('') + const [currentPage, setCurrentPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [search, setSearch] = useState('') + + const { data, isLoading, error, isFetching } = useVoucherCatalog({ + page: currentPage, + limit: pageSize, + search + }) + + const vouchers = data?.vouchers ?? [] + const totalCount = data?.total_count ?? 0 + + // Hooks + const { lang: locale } = useParams() + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setCurrentPage(newPage) + }, []) + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10) + setPageSize(newPageSize) + setCurrentPage(1) // Reset to first page + }, []) + + const handleEditVoucher = (voucher: VoucherCatalogType) => { + setEditVoucherData(voucher) + setAddVoucherOpen(true) + } + + const handleDeleteVoucher = (voucherId: string) => { + if (confirm('Apakah Anda yakin ingin menghapus voucher ini?')) { + console.log('Deleting voucher:', voucherId) + // Add your delete logic here + // deleteVoucher.mutate(voucherId) + } + } + + const handleToggleActive = (voucherId: string, currentStatus: boolean) => { + console.log('Toggling active status for voucher:', voucherId, !currentStatus) + // Add your toggle logic here + // toggleVoucherStatus.mutate({ id: voucherId, isActive: !currentStatus }) + } + + const handleCloseVoucherDrawer = () => { + setAddVoucherOpen(false) + setEditVoucherData(undefined) + } + + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ) + }, + columnHelper.accessor('name', { + header: 'Nama Voucher', + cell: ({ row }) => ( +
+ + {getInitials(row.original.name)} + +
+ + + {row.original.name} + + + {row.original.description && ( + + {row.original.description} + + )} + + {row.original.code} + +
+
+ ) + }), + columnHelper.accessor('type', { + header: 'Tipe Voucher', + cell: ({ row }) => { + const typeColors = { + discount: 'primary', + cashback: 'success', + free_shipping: 'info', + product: 'warning' + } as const + + return ( + + ) + } + }), + columnHelper.accessor('discountValue', { + header: 'Nilai Voucher', + cell: ({ row }) => ( + + {getVoucherValueDisplay(row.original)} + + ) + }), + columnHelper.accessor('pointCost', { + header: 'Biaya Poin', + cell: ({ row }) => ( +
+ + + {row.original.pointCost.toLocaleString('id-ID')} poin + +
+ ) + }), + columnHelper.accessor('minPurchase', { + header: 'Min. Pembelian', + cell: ({ row }) => ( + + {row.original.minPurchase ? formatCurrency(row.original.minPurchase) : '-'} + + ) + }), + columnHelper.accessor('stock', { + header: 'Stok', + cell: ({ row }) => { + const stock = row.original.stock + const stockColor = stock === 0 ? 'error' : stock && stock <= 10 ? 'warning' : 'success' + const stockText = stock === undefined ? 'Unlimited' : stock === 0 ? 'Habis' : stock.toString() + + return + } + }), + columnHelper.accessor('isActive', { + header: 'Status', + cell: ({ row }) => ( + + ) + }), + columnHelper.accessor('validUntil', { + header: 'Berlaku Hingga', + cell: ({ row }) => ( + + {row.original.validUntil + ? new Date(row.original.validUntil).toLocaleDateString('id-ID', { + year: 'numeric', + month: 'short', + day: 'numeric' + }) + : 'Tidak terbatas'} + + ) + }), + { + id: 'actions', + header: 'Aksi', + cell: ({ row }) => ( +
+ handleToggleActive(row.original.id, row.original.isActive) + } + }, + { + text: 'Edit', + icon: 'tabler-edit text-[22px]', + menuItemProps: { + className: 'flex items-center gap-2 text-textSecondary', + onClick: () => handleEditVoucher(row.original) + } + }, + { + text: 'Hapus', + icon: 'tabler-trash text-[22px]', + menuItemProps: { + className: 'flex items-center gap-2 text-textSecondary', + onClick: () => handleDeleteVoucher(row.original.id) + } + } + ]} + /> +
+ ), + enableSorting: false + } + ], + // eslint-disable-next-line react-hooks/exhaustive-deps + [locale, handleEditVoucher, handleDeleteVoucher, handleToggleActive] + ) + + const table = useReactTable({ + data: vouchers as VoucherCatalogType[], + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + rowSelection, + globalFilter, + pagination: { + pageIndex: currentPage, + pageSize + } + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize) + }) + + return ( + <> + +
+ table.setPageSize(Number(e.target.value))} + className='max-sm:is-full sm:is-[70px]' + > + 10 + 25 + 50 + +
+ setSearch(value as string)} + placeholder='Cari Voucher' + className='max-sm:is-full' + /> + + +
+
+
+ {isLoading ? ( + + ) : ( + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + {table.getFilteredRowModel().rows.length === 0 ? ( + + + + + + ) : ( + + {table + .getRowModel() + .rows.slice(0, table.getState().pagination.pageSize) + .map(row => { + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ) + })} + + )} +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: + }[header.column.getIsSorted() as 'asc' | 'desc'] ?? null} +
+ + )} +
+ Tidak ada data tersedia +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ )} +
+ ( + + )} + count={totalCount} + rowsPerPage={pageSize} + page={currentPage} + onPageChange={handlePageChange} + onRowsPerPageChange={handlePageSizeChange} + rowsPerPageOptions={[10, 25, 50]} + disabled={isLoading} + /> +
+ + + ) +} + +export default VoucherListTable diff --git a/src/views/apps/marketing/voucher/index.tsx b/src/views/apps/marketing/voucher/index.tsx new file mode 100644 index 0000000..3d779f4 --- /dev/null +++ b/src/views/apps/marketing/voucher/index.tsx @@ -0,0 +1,17 @@ +// MUI Imports +import Grid from '@mui/material/Grid2' +import VoucherListTable from './VoucherListTable' + +// Type Imports + +const VoucherList = () => { + return ( + + + + + + ) +} + +export default VoucherList