- {progressGroupExample1.map((item, index) => (
-
-
- {item.title}
-
-
-
-
-
+
+
+
+
Active Users
+
{formatNumber(dashboardData.todayStats.activeUsers)}
- ))}
-
-
-
+
+
-
Pageviews
-
78,623
+
Gifts Opened
+
{formatNumber(dashboardData.todayStats.giftsOpened)}
-
+
+
-
-
-
-
-
- {progressGroupExample2.map((item, index) => (
-
-
-
- {item.title}
- {item.value}%
-
-
-
-
+
Points Claimed
+
{formatVND(dashboardData.todayStats.pointsClaimed)}
- ))}
-
-
-
- {progressGroupExample3.map((item, index) => (
-
-
-
- {item.title}
-
- {item.value}{' '}
- ({item.percent}%)
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
- User
-
- Country
-
- Usage
-
- Payment Method
-
- Activity
-
-
-
- {tableExample.map((item, index) => (
-
-
-
-
-
- {item.user.name}
-
- {item.user.new ? 'New' : 'Recurring'} | Registered:{' '}
- {item.user.registered}
-
-
-
-
-
-
-
-
{item.usage.value}%
-
- {item.usage.period}
-
-
-
-
-
-
-
-
- Last login
- {item.activity}
-
-
- ))}
-
-
+
+ {/* Weekly Trends */}
+
+
+ Weekly Trends
+ Last 7 days
+
+
+
+
+ Users
+
+
+
+ Gift Boxes
+
+
+
+ Points Claimed
+
+
+
+
+
>
)
}
diff --git a/src/views/dashboard/MainChart.js b/src/views/dashboard/MainChart.js
deleted file mode 100644
index 922c0d021..000000000
--- a/src/views/dashboard/MainChart.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import React, { useEffect, useRef } from 'react'
-
-import { CChartLine } from '@coreui/react-chartjs'
-import { getStyle } from '@coreui/utils'
-
-const MainChart = () => {
- const chartRef = useRef(null)
-
- useEffect(() => {
- document.documentElement.addEventListener('ColorSchemeChange', () => {
- if (chartRef.current) {
- setTimeout(() => {
- chartRef.current.options.scales.x.grid.borderColor = getStyle(
- '--cui-border-color-translucent',
- )
- chartRef.current.options.scales.x.grid.color = getStyle('--cui-border-color-translucent')
- chartRef.current.options.scales.x.ticks.color = getStyle('--cui-body-color')
- chartRef.current.options.scales.y.grid.borderColor = getStyle(
- '--cui-border-color-translucent',
- )
- chartRef.current.options.scales.y.grid.color = getStyle('--cui-border-color-translucent')
- chartRef.current.options.scales.y.ticks.color = getStyle('--cui-body-color')
- chartRef.current.update()
- })
- }
- })
- }, [chartRef])
-
- const random = () => Math.round(Math.random() * 100)
-
- return (
- <>
-
- >
- )
-}
-
-export default MainChart
diff --git a/src/views/events/EventCategories.js b/src/views/events/EventCategories.js
new file mode 100644
index 000000000..2558b78cf
--- /dev/null
+++ b/src/views/events/EventCategories.js
@@ -0,0 +1,269 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CModal,
+ CModalHeader,
+ CModalTitle,
+ CModalBody,
+ CModalFooter,
+ CForm,
+ CFormInput,
+ CFormLabel,
+ CFormTextarea,
+ CAlert,
+ CBadge,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilPlus, cilPencil, cilTrash } from '@coreui/icons'
+import { eventApi } from 'src/services/api'
+
+const EventCategories = () => {
+ const [categories, setCategories] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [showModal, setShowModal] = useState(false)
+ const [currentCategory, setCurrentCategory] = useState({ name: '', description: '', color: '#3399ff' })
+ const [isEditing, setIsEditing] = useState(false)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+
+ // Fetch categories on component mount
+ useEffect(() => {
+ fetchCategories()
+ }, [])
+
+ const fetchCategories = async () => {
+ try {
+ setLoading(true)
+ const response = await eventApi.getCategories()
+ setCategories(response)
+ } catch (error) {
+ setError('Failed to load categories. Please try again.')
+ console.error(error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleOpenModal = (category = null) => {
+ if (category) {
+ setCurrentCategory(category)
+ setIsEditing(true)
+ } else {
+ setCurrentCategory({ name: '', description: '', color: '#3399ff' })
+ setIsEditing(false)
+ }
+ setShowModal(true)
+ }
+
+ const handleCloseModal = () => {
+ setShowModal(false)
+ setError(null)
+ setSuccess(null)
+ }
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target
+ setCurrentCategory({ ...currentCategory, [name]: value })
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ try {
+ if (isEditing) {
+ await eventApi.updateCategory(currentCategory.id, currentCategory)
+ setSuccess('Category updated successfully!')
+ } else {
+ await eventApi.createCategory(currentCategory)
+ setSuccess('Category created successfully!')
+ }
+
+ // Refresh categories list
+ fetchCategories()
+
+ // Close modal after short delay
+ setTimeout(() => {
+ handleCloseModal()
+ }, 1500)
+ } catch (error) {
+ setError(error.message || 'Failed to save category. Please try again.')
+ console.error(error)
+ }
+ }
+
+ const handleDelete = async (id) => {
+ if (window.confirm('Are you sure you want to delete this category?')) {
+ try {
+ await eventApi.deleteCategory(id)
+ setCategories(categories.filter(category => category.id !== id))
+ } catch (error) {
+ setError('Failed to delete category. It may be in use by existing events.')
+ console.error(error)
+ }
+ }
+ }
+
+ return (
+
+
+
+
+ Event Categories
+ handleOpenModal()}
+ >
+
+ Add Category
+
+
+
+ {error && {error} }
+
+ {loading ? (
+ Loading categories...
+ ) : (
+
+
+
+ Name
+ Description
+ Color
+ Events Count
+ Actions
+
+
+
+ {categories.length > 0 ? (
+ categories.map(category => (
+
+
+ {category.name}
+
+
+ {category.description || 'No description'}
+
+
+
+ {category.color}
+
+
+
+ {category.eventsCount || 0} events
+
+
+ handleOpenModal(category)}
+ >
+
+
+ handleDelete(category.id)}
+ >
+
+
+
+
+ ))
+ ) : (
+
+
+ No categories found
+
+
+ )}
+
+
+ )}
+
+
+
+
+ {/* Category Modal */}
+
+
+ {isEditing ? 'Edit Category' : 'Add New Category'}
+
+
+ {error && {error} }
+ {success && {success} }
+
+
+
+ Name
+
+
+
+
+ Description
+
+
+
+
+
+
+
+ Cancel
+
+
+ {isEditing ? 'Update' : 'Create'}
+
+
+
+
+
+
+ )
+}
+
+export default EventCategories
\ No newline at end of file
diff --git a/src/views/events/EventForm.js b/src/views/events/EventForm.js
new file mode 100644
index 000000000..cb042e623
--- /dev/null
+++ b/src/views/events/EventForm.js
@@ -0,0 +1,425 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CForm,
+ CFormInput,
+ CFormLabel,
+ CFormTextarea,
+ CFormSelect,
+ CButton,
+ CAlert,
+ CFormCheck,
+ CInputGroup,
+ CInputGroupText,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilSave, cilX } from '@coreui/icons'
+import { eventApi } from 'src/services/api'
+import { useNavigate, useParams } from 'react-router-dom'
+
+const emptyEvent = {
+ name: '',
+ description: '',
+ startDate: '',
+ endDate: '',
+ time: '',
+ location: '',
+ venue: '',
+ address: '',
+ city: '',
+ country: '',
+ capacity: '',
+ price: '',
+ categoryId: '',
+ featuredImage: '',
+ status: 'draft',
+ isPublic: true,
+ isFeatured: false,
+ showRemainingTickets: true,
+ organizerId: '',
+}
+
+const EventForm = () => {
+ const { id } = useParams()
+ const navigate = useNavigate()
+ const [event, setEvent] = useState(emptyEvent)
+ const [loading, setLoading] = useState(false)
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(false)
+ const [categories, setCategories] = useState([])
+ const [organizers, setOrganizers] = useState([])
+ const isEditMode = !!id
+
+ // Fetch event data if in edit mode
+ useEffect(() => {
+ const fetchEventData = async () => {
+ if (isEditMode) {
+ try {
+ setLoading(true)
+ const eventData = await eventApi.getEvent(id)
+ setEvent(eventData)
+ } catch (error) {
+ setError('Failed to load event data. Please try again.')
+ console.error(error)
+ } finally {
+ setLoading(false)
+ }
+ }
+ }
+
+ fetchEventData()
+ }, [id, isEditMode])
+
+ // Fetch categories and organizers on component mount
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const categoriesData = await eventApi.getCategories()
+ setCategories(categoriesData)
+
+ // Placeholder for fetching organizers when that API is available
+ // const organizersData = await userApi.getOrganizers()
+ // setOrganizers(organizersData)
+
+ // Mock organizer data for now
+ setOrganizers([
+ { id: '1', name: 'Main Organizer' },
+ { id: '2', name: 'Partner Organization' },
+ ])
+ } catch (error) {
+ console.error('Failed to fetch form data:', error)
+ }
+ }
+
+ fetchData()
+ }, [])
+
+ const handleInputChange = (e) => {
+ const { name, value, type, checked } = e.target
+
+ if (type === 'checkbox') {
+ setEvent({ ...event, [name]: checked })
+ } else {
+ setEvent({ ...event, [name]: value })
+ }
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ try {
+ setSaving(true)
+ setError(null)
+
+ let result
+ if (isEditMode) {
+ result = await eventApi.updateEvent(id, event)
+ } else {
+ result = await eventApi.createEvent(event)
+ }
+
+ setSuccess(true)
+
+ // Navigate back to events list after short delay
+ setTimeout(() => {
+ navigate('/events/list')
+ }, 1500)
+ } catch (error) {
+ setError(error.message || 'Failed to save event. Please try again.')
+ console.error(error)
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ if (loading) {
+ return
Loading event data...
+ }
+
+ return (
+
+
+
+
+ {isEditMode ? 'Edit Event' : 'Create New Event'}
+
+
+ {error && {error} }
+ {success && (
+
+ Event successfully {isEditMode ? 'updated' : 'created'}!
+
+ )}
+
+
+
+
+ Event Name
+
+
+
+ Status
+
+ Draft
+ Published
+ Cancelled
+ Completed
+
+
+
+
+
+
+ Description
+
+
+
+
+
+
+ Start Date
+
+
+
+ End Date
+
+
+
+ Time
+
+
+
+
+
+
+ Venue
+
+
+
+ Location
+
+
+
+
+
+
+ Address
+
+
+
+
+
+
+ City
+
+
+
+ Country
+
+
+
+
+
+
+ Capacity
+
+
+
+ Price
+
+ $
+
+
+
+
+
+
+
+ Category
+
+ Select a category
+ {categories.map(category => (
+
+ {category.name}
+
+ ))}
+
+
+
+ Organizer
+
+ Select an organizer
+ {organizers.map(organizer => (
+
+ {organizer.name}
+
+ ))}
+
+
+
+
+
+
+ Featured Image URL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ navigate('/events/list')}
+ >
+
+ Cancel
+
+
+
+ {saving ? 'Saving...' : isEditMode ? 'Update Event' : 'Create Event'}
+
+
+
+
+
+
+
+ )
+}
+
+export default EventForm
\ No newline at end of file
diff --git a/src/views/events/EventReport.js b/src/views/events/EventReport.js
new file mode 100644
index 000000000..9661d004c
--- /dev/null
+++ b/src/views/events/EventReport.js
@@ -0,0 +1,378 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CFormSelect,
+ CForm,
+ CFormInput,
+ CBadge,
+ CAlert,
+ CSpinner,
+ CButton,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CInputGroup,
+ CInputGroupText,
+ CPagination,
+ CPaginationItem,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilCloudDownload, cilFilter, cilSearch, cilCalendar } from '@coreui/icons'
+import { eventApi } from '../../services/api'
+import { CSVLink } from 'react-csv'
+
+// Component cho phần tìm kiếm và lọc
+const SearchAndFilters = ({ searchTerm, setSearchTerm, dateRange, setDateRange, handleSearch }) => {
+ return (
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+ setDateRange({...dateRange, start: e.target.value})}
+ />
+
+
+
+
+
+
+
+ setDateRange({...dateRange, end: e.target.value})}
+ />
+
+
+
+
+ Generate Report
+
+
+
+
+ )
+}
+
+// Component cho bảng báo cáo
+const ReportTable = ({ reportData, formatDate }) => {
+ return (
+
+
+
+ Date Export
+ User ID
+ Email
+ KYC
+ Tổng ref
+ reffereId
+ Ref success KYC
+ total Luckybox
+ total Luckybox claim
+ IP claim luckybox
+ Total reward (usdt)
+
+
+
+ {reportData.map((item, index) => (
+
+ {formatDate(item.exportDate)}
+ {item.userId}
+ {item.email}
+
+
+ {item.kyc ? 'Completed' : 'Not Completed'}
+
+
+ {item.totalRef}
+ {item.referrerId || '-'}
+ {item.refSuccessKYC}
+ {item.totalLuckybox}
+ {item.totalLuckyboxClaim}
+ {item.ipClaimLuckybox}
+ {item.totalRewardUSDT.toFixed(2)}
+
+ ))}
+
+
+ )
+}
+
+// Component cho phân trang
+const PaginationComponent = ({ pagination, page, handlePageChange, formatNumber }) => {
+ if (pagination.totalPages <= 1) return null;
+
+ return (
+ <>
+
+
+ handlePageChange(page - 1)}
+ >
+ Previous
+
+
+ {[...Array(Math.min(5, pagination.totalPages)).keys()].map((i) => {
+ // Show 5 pages around current page
+ let pageNum
+ if (page <= 3) {
+ pageNum = i + 1
+ } else if (page >= pagination.totalPages - 2) {
+ pageNum = pagination.totalPages - 4 + i
+ } else {
+ pageNum = page - 2 + i
+ }
+
+ if (pageNum > 0 && pageNum <= pagination.totalPages) {
+ return (
+ handlePageChange(pageNum)}
+ >
+ {pageNum}
+
+ )
+ }
+ return null
+ })}
+
+ {pagination.totalPages > 5 && page < pagination.totalPages - 2 && (
+ <>
+ ...
+ handlePageChange(pagination.totalPages)}>
+ {pagination.totalPages}
+
+ >
+ )}
+
+ handlePageChange(page + 1)}
+ >
+ Next
+
+
+
+
+
+ Showing {((page - 1) * pagination.pageSize) + 1} to {Math.min(page * pagination.pageSize, pagination.total)} of {formatNumber(pagination.total)} entries
+
+ >
+ )
+}
+
+// Component chính
+const EventReport = () => {
+ // State
+ const [searchTerm, setSearchTerm] = useState('')
+ const [dateRange, setDateRange] = useState({ start: '', end: '' })
+ const [reportData, setReportData] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [page, setPage] = useState(1)
+ const [pagination, setPagination] = useState({
+ total: 0,
+ pageSize: 10,
+ totalPages: 0
+ })
+
+ // Format date for display
+ const formatDate = (dateString) => {
+ const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }
+ return new Date(dateString).toLocaleString(undefined, options)
+ }
+
+ // Format numbers for easier reading
+ const formatNumber = (num) => {
+ return new Intl.NumberFormat().format(num)
+ }
+
+ // Handle search and generate report
+ const handleSearch = (e) => {
+ e.preventDefault()
+ generateReport()
+ }
+
+ // Handle pagination change
+ const handlePageChange = (newPage) => {
+ setPage(newPage)
+ }
+
+ // Generate report
+ const generateReport = async () => {
+ setLoading(true)
+ setError(null)
+
+ try {
+ // In a real app, replace with actual API call:
+ // const response = await eventApi.getEventReport(page, searchTerm, dateRange)
+
+ // Mock data for demonstration
+ const mockData = getMockReportData()
+
+ setTimeout(() => {
+ setReportData(mockData.data)
+ setPagination({
+ total: mockData.total,
+ pageSize: 10,
+ totalPages: Math.ceil(mockData.total / 10)
+ })
+ setLoading(false)
+ }, 800)
+
+ } catch (err) {
+ setError('Failed to generate report. Please try again later.')
+ setLoading(false)
+ console.error(err)
+ }
+ }
+
+ // Generate mock data
+ const getMockReportData = () => {
+ const data = []
+ for (let i = 0; i < 10; i++) {
+ data.push({
+ exportDate: new Date().toISOString(),
+ userId: `user${1000 + i}`,
+ email: `user${1000 + i}@example.com`,
+ kyc: Math.random() > 0.3,
+ totalRef: Math.floor(Math.random() * 20),
+ referrerId: Math.random() > 0.2 ? `user${900 + Math.floor(Math.random() * 100)}` : null,
+ refSuccessKYC: Math.floor(Math.random() * 10),
+ totalLuckybox: Math.floor(Math.random() * 15) + 5,
+ totalLuckyboxClaim: Math.floor(Math.random() * 10),
+ ipClaimLuckybox: `192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
+ totalRewardUSDT: Math.random() * 100
+ })
+ }
+
+ return {
+ data,
+ total: 256
+ }
+ }
+
+ // Prepare data for CSV export
+ const prepareExportData = () => {
+ if (!reportData) return []
+
+ const headers = [
+ ['Date Export', 'User ID', 'Email', 'KYC', 'Tổng ref', 'reffereId', 'Ref success KYC', 'total Luckybox', 'total Luckybox claim', 'IP claim luckybox', 'Total reward (usdt)']
+ ]
+
+ const rows = reportData.map(item => [
+ formatDate(item.exportDate),
+ item.userId,
+ item.email,
+ item.kyc ? 'Completed' : 'Not Completed',
+ item.totalRef,
+ item.referrerId || '-',
+ item.refSuccessKYC,
+ item.totalLuckybox,
+ item.totalLuckyboxClaim,
+ item.ipClaimLuckybox,
+ item.totalRewardUSDT.toFixed(2)
+ ])
+
+ return [...headers, ...rows]
+ }
+
+ // Load initial data
+ useEffect(() => {
+ generateReport()
+ }, [page])
+
+ return (
+
+
+
+
+ Event Report
+
+ {reportData && (
+
+
+ Export to CSV
+
+ )}
+
+
+
+ {error && {error} }
+
+ {/* Search & Filters Component */}
+
+
+ {loading ? (
+
+
+
Generating report...
+
+ ) : reportData ? (
+ <>
+ {/* Report Table Component */}
+
+
+ {/* Pagination Component */}
+
+ >
+ ) : (
+
+ Use the filters above to generate a report
+
+ )}
+
+
+
+
+ )
+}
+
+export default EventReport
\ No newline at end of file
diff --git a/src/views/events/EventStatistics.js b/src/views/events/EventStatistics.js
new file mode 100644
index 000000000..8e55b2c2d
--- /dev/null
+++ b/src/views/events/EventStatistics.js
@@ -0,0 +1,660 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CFormSelect,
+ CForm,
+ CFormInput,
+ CBadge,
+ CAlert,
+ CSpinner,
+ CPagination,
+ CPaginationItem,
+ CInputGroup,
+ CInputGroupText,
+ CProgress
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import {
+ cilCloudDownload,
+ cilFilter,
+ cilSearch,
+ cilPeople,
+ cilUser,
+ cilGift,
+ cilMoney
+} from '@coreui/icons'
+import { eventApi } from '../../services/api'
+import { CSVLink } from 'react-csv'
+
+// Component cho phần hiển thị thông tin tổng hợp
+const SummaryCards = ({ summary }) => {
+ // Format numbers for easier reading
+ const formatNumber = (num) => {
+ return new Intl.NumberFormat().format(num)
+ }
+
+ // Format points in Vietnamese Dong
+ const formatVND = (points) => {
+ return new Intl.NumberFormat('vi-VN', {
+ style: 'currency',
+ currency: 'VND',
+ maximumFractionDigits: 0
+ }).format(points)
+ }
+
+ return (
+
+
+
+
+
+
+ {formatNumber(summary.totalParticipants)}
+
+
Total Participants
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatNumber(summary.kycCompleted)}
+
+
KYC Completed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatNumber(summary.totalGifts)}
+
+ (avg: {summary.averageGiftsPerUser})
+
+
+
Total Gifts
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatVND(summary.totalPoints)}
+
+ (avg: {formatVND(summary.averagePointsPerUser)})
+
+
+
Total Points
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+// Component cho phần tìm kiếm và lọc
+const SearchAndFilters = ({ searchTerm, setSearchTerm, filters, handleFilterChange, handleSearch }) => {
+ const resetFilters = () => {
+ setSearchTerm('')
+ handleFilterChange('isKyc', '')
+ handleFilterChange('device', '')
+ handleFilterChange('hasBtc', '')
+ }
+
+ return (
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+ handleFilterChange('isKyc', e.target.value)}
+ className="w-auto"
+ >
+ All KYC Status
+ KYC Completed
+ KYC Not Completed
+
+ handleFilterChange('device', e.target.value)}
+ className="w-auto"
+ >
+ All Devices
+ Mobile
+ Desktop
+ Tablet
+
+ handleFilterChange('hasBtc', e.target.value)}
+ className="w-auto"
+ >
+ All BTC Status
+ BTC Given
+ BTC Not Given
+
+
+ Clear Filters
+
+
+
+
+ )
+}
+
+// Component cho phần bảng dữ liệu
+const StatisticsTable = ({ records, formatDate, formatVND }) => {
+ return (
+
+
+
+ User ID
+ Email
+ IP Address
+ Device
+ KYC Status
+ Referral Code
+ Gifts
+ Points
+ BTC Given
+ Last Active
+
+
+
+ {records.length > 0 ? (
+ records.map((record, index) => (
+
+ {record.userId}
+ {record.email}
+ {record.ip_registered}
+
+
+ {record.device}
+
+
+
+
+ {record.is_kyc ? 'Completed' : 'Not Completed'}
+
+
+ {record.referral_code}
+ {record.gifts}
+ {formatVND(record.points)}
+
+
+ {record.btcGiven ? 'Yes' : 'No'}
+
+
+ {formatDate(record.lastActive)}
+
+ ))
+ ) : (
+
+
+ No records found
+
+
+ )}
+
+
+ )
+}
+
+// Component cho phân trang
+const PaginationComponent = ({ pagination, page, handlePageChange, formatNumber }) => {
+ if (pagination.totalPages <= 1) return null;
+
+ return (
+ <>
+
+
+ handlePageChange(page - 1)}
+ >
+ Previous
+
+
+ {[...Array(Math.min(5, pagination.totalPages)).keys()].map((i) => {
+ // Show 5 pages around current page
+ let pageNum
+ if (page <= 3) {
+ pageNum = i + 1
+ } else if (page >= pagination.totalPages - 2) {
+ pageNum = pagination.totalPages - 4 + i
+ } else {
+ pageNum = page - 2 + i
+ }
+
+ if (pageNum > 0 && pageNum <= pagination.totalPages) {
+ return (
+ handlePageChange(pageNum)}
+ >
+ {pageNum}
+
+ )
+ }
+ return null
+ })}
+
+ {pagination.totalPages > 5 && page < pagination.totalPages - 2 && (
+ <>
+ ...
+ handlePageChange(pagination.totalPages)}>
+ {pagination.totalPages}
+
+ >
+ )}
+
+ handlePageChange(page + 1)}
+ >
+ Next
+
+
+
+
+
+ Showing {((page - 1) * pagination.pageSize) + 1} to {Math.min(page * pagination.pageSize, pagination.total)} of {formatNumber(pagination.total)} entries
+
+ >
+ )
+}
+
+// Component chính
+const EventStatistics = () => {
+ // State
+ const [statistics, setStatistics] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [page, setPage] = useState(1)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [filters, setFilters] = useState({
+ isKyc: '',
+ device: '',
+ hasBtc: ''
+ })
+
+ // Fetch statistics and records
+ useEffect(() => {
+ const fetchStatistics = async () => {
+ setLoading(true)
+ try {
+ // Sẵn sàng cho API thực tế:
+ // const response = await eventApi.getEventParticipationStatistics(page, searchTerm, filters)
+ // setStatistics(response)
+
+ // Mock data for demonstration
+ const mockData = getMockData(page)
+
+ setTimeout(() => {
+ setStatistics(mockData)
+ setLoading(false)
+ }, 800)
+
+ } catch (err) {
+ setError('Failed to load statistics. Please try again later.')
+ setLoading(false)
+ console.error(err)
+ }
+ }
+
+ fetchStatistics()
+ }, [page, searchTerm, filters])
+
+ // Hàm tạo dữ liệu giả cho demo
+ const getMockData = (currentPage) => {
+ return {
+ records: [
+ {
+ userId: "user123",
+ email: "user123@example.com",
+ ip_registered: "192.168.1.1",
+ device: "mobile",
+ is_kyc: true,
+ referral_code: "REF123",
+ gifts: 5,
+ points: 250000,
+ btcGiven: false,
+ lastActive: "2023-10-21T15:30:00Z"
+ },
+ {
+ userId: "user456",
+ email: "user456@example.com",
+ ip_registered: "192.168.1.2",
+ device: "desktop",
+ is_kyc: false,
+ referral_code: "REF456",
+ gifts: 3,
+ points: 150000,
+ btcGiven: true,
+ lastActive: "2023-10-20T10:15:00Z"
+ },
+ {
+ userId: "user789",
+ email: "user789@example.com",
+ ip_registered: "192.168.1.3",
+ device: "tablet",
+ is_kyc: true,
+ referral_code: "REF789",
+ gifts: 8,
+ points: 400000,
+ btcGiven: false,
+ lastActive: "2023-10-21T08:45:00Z"
+ },
+ {
+ userId: "user101",
+ email: "user101@example.com",
+ ip_registered: "192.168.1.4",
+ device: "mobile",
+ is_kyc: false,
+ referral_code: "REF101",
+ gifts: 2,
+ points: 100000,
+ btcGiven: false,
+ lastActive: "2023-10-19T14:20:00Z"
+ },
+ {
+ userId: "user202",
+ email: "user202@example.com",
+ ip_registered: "192.168.1.5",
+ device: "desktop",
+ is_kyc: true,
+ referral_code: "REF202",
+ gifts: 10,
+ points: 500000,
+ btcGiven: true,
+ lastActive: "2023-10-21T12:10:00Z"
+ },
+ {
+ userId: "user303",
+ email: "user303@example.com",
+ ip_registered: "192.168.1.6",
+ device: "mobile",
+ is_kyc: false,
+ referral_code: "REF303",
+ gifts: 1,
+ points: 50000,
+ btcGiven: false,
+ lastActive: "2023-10-18T09:30:00Z"
+ },
+ {
+ userId: "user404",
+ email: "user404@example.com",
+ ip_registered: "192.168.1.7",
+ device: "desktop",
+ is_kyc: true,
+ referral_code: "REF404",
+ gifts: 6,
+ points: 300000,
+ btcGiven: false,
+ lastActive: "2023-10-20T16:45:00Z"
+ },
+ {
+ userId: "user505",
+ email: "user505@example.com",
+ ip_registered: "192.168.1.8",
+ device: "mobile",
+ is_kyc: true,
+ referral_code: "REF505",
+ gifts: 4,
+ points: 200000,
+ btcGiven: true,
+ lastActive: "2023-10-21T11:25:00Z"
+ },
+ {
+ userId: "user606",
+ email: "user606@example.com",
+ ip_registered: "192.168.1.9",
+ device: "tablet",
+ is_kyc: false,
+ referral_code: "REF606",
+ gifts: 2,
+ points: 100000,
+ btcGiven: false,
+ lastActive: "2023-10-19T13:15:00Z"
+ },
+ {
+ userId: "user707",
+ email: "user707@example.com",
+ ip_registered: "192.168.1.10",
+ device: "desktop",
+ is_kyc: true,
+ referral_code: "REF707",
+ gifts: 7,
+ points: 350000,
+ btcGiven: false,
+ lastActive: "2023-10-20T08:50:00Z"
+ }
+ ],
+ pagination: {
+ total: 1250,
+ page: currentPage,
+ pageSize: 10,
+ totalPages: 125
+ },
+ summary: {
+ totalParticipants: 1250,
+ kycCompleted: 450,
+ totalGifts: 4500,
+ averageGiftsPerUser: 3.6,
+ totalPoints: 1234500000,
+ averagePointsPerUser: 987600
+ }
+ }
+ }
+
+ // Format date for display
+ const formatDate = (dateString) => {
+ const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }
+ return new Date(dateString).toLocaleString(undefined, options)
+ }
+
+ // Format numbers for easier reading
+ const formatNumber = (num) => {
+ return new Intl.NumberFormat().format(num)
+ }
+
+ // Format points in Vietnamese Dong
+ const formatVND = (points) => {
+ return new Intl.NumberFormat('vi-VN', {
+ style: 'currency',
+ currency: 'VND',
+ maximumFractionDigits: 0
+ }).format(points)
+ }
+
+ // Handle pagination change
+ const handlePageChange = (newPage) => {
+ setPage(newPage)
+ }
+
+ // Handle search
+ const handleSearch = (e) => {
+ e.preventDefault()
+ setPage(1) // Reset to first page on new search
+ }
+
+ // Handle filter change
+ const handleFilterChange = (name, value) => {
+ setFilters(prev => ({
+ ...prev,
+ [name]: value
+ }))
+ setPage(1) // Reset to first page on filter change
+ }
+
+ // Prepare data for CSV export
+ const prepareExportData = () => {
+ if (!statistics) return []
+
+ const headers = [
+ ['Event Participation Statistics Report'],
+ ['Generated on:', new Date().toLocaleString()],
+ [''],
+ ['Summary'],
+ ['Total Participants:', statistics.summary.totalParticipants],
+ ['KYC Completed:', `${statistics.summary.kycCompleted} (${(statistics.summary.kycCompleted / statistics.summary.totalParticipants * 100).toFixed(1)}%)`],
+ ['Total Gifts:', statistics.summary.totalGifts],
+ ['Average Gifts per User:', statistics.summary.averageGiftsPerUser],
+ ['Total Points:', statistics.summary.totalPoints],
+ ['Average Points per User:', statistics.summary.averagePointsPerUser],
+ [''],
+ ['Participant Records'],
+ ['User ID', 'Email', 'IP Address', 'Device', 'KYC Status', 'Referral Code', 'Gifts', 'Points', 'BTC Given', 'Last Active']
+ ]
+
+ const rows = statistics.records.map(record => [
+ record.userId,
+ record.email,
+ record.ip_registered,
+ record.device,
+ record.is_kyc ? 'Completed' : 'Not Completed',
+ record.referral_code,
+ record.gifts,
+ record.points,
+ record.btcGiven ? 'Yes' : 'No',
+ formatDate(record.lastActive)
+ ])
+
+ return [...headers, ...rows]
+ }
+
+ // Render hàm chính
+ return (
+
+
+
+
+ Event Participation Statistics
+
+ {statistics && (
+
+
+ Export to CSV
+
+ )}
+
+
+
+
+ {error && {error} }
+
+ {loading && !statistics ? (
+
+
+
Loading statistics...
+
+ ) : statistics ? (
+ <>
+ {/* Summary Cards Component */}
+
+
+ {/* Search & Filters Component */}
+
+
+ {/* Records Table Component */}
+
+
+ {/* Pagination Component */}
+
+ >
+ ) : (
+ No data available
+ )}
+
+
+
+
+ )
+}
+
+export default EventStatistics
\ No newline at end of file
diff --git a/src/views/events/EventsList.js b/src/views/events/EventsList.js
new file mode 100644
index 000000000..491565604
--- /dev/null
+++ b/src/views/events/EventsList.js
@@ -0,0 +1,292 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CDropdown,
+ CDropdownToggle,
+ CDropdownMenu,
+ CDropdownItem,
+ CBadge,
+ CFormInput,
+ CForm,
+ CFormSelect,
+ CPagination,
+ CPaginationItem,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilOptions, cilPencil, cilTrash, cilPlus, cilSearch } from '@coreui/icons'
+import { eventApi } from 'src/services/api'
+import { Link, useNavigate } from 'react-router-dom'
+
+const EventsList = () => {
+ const navigate = useNavigate()
+ const [events, setEvents] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [categories, setCategories] = useState([])
+ const [page, setPage] = useState(1)
+ const [totalPages, setTotalPages] = useState(1)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [filterCategory, setFilterCategory] = useState('')
+ const [filterStatus, setFilterStatus] = useState('')
+
+ // Fetch events on component mount and when filters change
+ useEffect(() => {
+ const fetchEvents = async () => {
+ try {
+ setLoading(true)
+ const filters = {}
+ if (searchTerm) filters.search = searchTerm
+ if (filterCategory) filters.category = filterCategory
+ if (filterStatus) filters.status = filterStatus
+
+ const response = await eventApi.getEvents(page, 10, filters)
+ setEvents(response.data)
+ setTotalPages(response.totalPages || 1)
+ } catch (error) {
+ console.error('Failed to fetch events:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchEvents()
+ }, [page, searchTerm, filterCategory, filterStatus])
+
+ // Fetch categories for filter dropdown
+ useEffect(() => {
+ const fetchCategories = async () => {
+ try {
+ const response = await eventApi.getCategories()
+ setCategories(response)
+ } catch (error) {
+ console.error('Failed to fetch categories:', error)
+ }
+ }
+
+ fetchCategories()
+ }, [])
+
+ const handleSearch = (e) => {
+ e.preventDefault()
+ setPage(1) // Reset to first page on new search
+ }
+
+ const handleDelete = async (id) => {
+ if (window.confirm('Are you sure you want to delete this event?')) {
+ try {
+ await eventApi.deleteEvent(id)
+ // Refresh events after deletion
+ setEvents(events.filter(event => event.id !== id))
+ } catch (error) {
+ console.error('Failed to delete event:', error)
+ }
+ }
+ }
+
+ const getStatusBadge = (status) => {
+ const statusMap = {
+ 'draft': { color: 'secondary', label: 'Draft' },
+ 'published': { color: 'success', label: 'Published' },
+ 'cancelled': { color: 'danger', label: 'Cancelled' },
+ 'completed': { color: 'info', label: 'Completed' },
+ }
+
+ const statusInfo = statusMap[status] || { color: 'light', label: status }
+
+ return (
+
{statusInfo.label}
+ )
+ }
+
+ return (
+
+
+
+
+ Events
+ navigate('/events/create')}
+ >
+
+ Create Event
+
+
+
+ {/* Search and Filters */}
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+ {
+ setFilterCategory(e.target.value)
+ setPage(1)
+ }}
+ >
+ All Categories
+ {categories.map(category => (
+
+ {category.name}
+
+ ))}
+
+
+
+ {
+ setFilterStatus(e.target.value)
+ setPage(1)
+ }}
+ >
+ All Statuses
+ Draft
+ Published
+ Cancelled
+ Completed
+
+
+
+
+
+ Search
+
+
+
+
+
+ {/* Events Table */}
+ {loading ? (
+ Loading events...
+ ) : (
+ <>
+
+
+
+ Event Name
+ Date
+ Location
+ Category
+ Status
+ Actions
+
+
+
+ {events.length > 0 ? (
+ events.map(event => (
+
+
+ {event.name}
+ ID: {event.id}
+
+
+ {new Date(event.startDate).toLocaleDateString()}{' '}
+ {event.endDate && <>- {new Date(event.endDate).toLocaleDateString()}>}
+ {event.time}
+
+ {event.location}
+
+ {event.category ? event.category.name : 'N/A'}
+
+
+ {getStatusBadge(event.status)}
+
+
+
+
+
+
+
+
+ View Details
+
+
+
+ Edit
+
+ handleDelete(event.id)}
+ style={{ color: 'red' }}
+ >
+
+ Delete
+
+
+
+
+
+ ))
+ ) : (
+
+
+ No events found
+
+
+ )}
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+ setPage(page - 1)}
+ >
+ Previous
+
+
+ {[...Array(totalPages).keys()].map(number => (
+ setPage(number + 1)}
+ >
+ {number + 1}
+
+ ))}
+
+ setPage(page + 1)}
+ >
+ Next
+
+
+ )}
+ >
+ )}
+
+
+
+
+ )
+}
+
+export default EventsList
\ No newline at end of file
diff --git a/src/views/gift-baskets/GiftBasketForm.js b/src/views/gift-baskets/GiftBasketForm.js
new file mode 100644
index 000000000..ff2fdc9e7
--- /dev/null
+++ b/src/views/gift-baskets/GiftBasketForm.js
@@ -0,0 +1,456 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CForm,
+ CFormInput,
+ CFormLabel,
+ CFormTextarea,
+ CFormSelect,
+ CButton,
+ CAlert,
+ CInputGroup,
+ CInputGroupText,
+ CFormCheck,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilSave, cilX, cilPlus, cilTrash } from '@coreui/icons'
+import { useNavigate, useParams } from 'react-router-dom'
+
+// Mock gift basket data
+const mockBaskets = [
+ {
+ id: '1',
+ name: 'Premium Gift Basket',
+ description: 'A luxury gift basket with premium items',
+ basePrice: 99.99,
+ totalItems: 10,
+ probability: 0.05,
+ status: 'active',
+ items: [
+ { id: 1, name: 'Luxury Chocolate Box', quantity: 1, value: 25.99 },
+ { id: 2, name: 'Premium Wine Bottle', quantity: 1, value: 40.00 },
+ { id: 3, name: 'Gourmet Cheese Selection', quantity: 1, value: 18.50 },
+ { id: 4, name: 'Artisan Crackers', quantity: 2, value: 7.99 },
+ ]
+ },
+ {
+ id: '2',
+ name: 'Standard Gift Basket',
+ description: 'A balanced gift basket with standard items',
+ basePrice: 49.99,
+ totalItems: 8,
+ probability: 0.15,
+ status: 'active',
+ items: [
+ { id: 1, name: 'Chocolate Box', quantity: 1, value: 12.99 },
+ { id: 2, name: 'Wine Bottle', quantity: 1, value: 20.00 },
+ { id: 3, name: 'Cheese Selection', quantity: 1, value: 9.50 },
+ { id: 4, name: 'Crackers', quantity: 1, value: 3.99 },
+ ]
+ },
+]
+
+const GiftBasketForm = () => {
+ const navigate = useNavigate()
+ const { id } = useParams()
+ const isEditMode = !!id
+
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ basePrice: '',
+ probability: 0.1,
+ status: 'active',
+ items: [],
+ })
+
+ const [newItem, setNewItem] = useState({
+ name: '',
+ quantity: 1,
+ value: '',
+ })
+
+ const [loading, setLoading] = useState(isEditMode)
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+
+ // If in edit mode, fetch basket data
+ useEffect(() => {
+ if (isEditMode) {
+ // Find basket by ID
+ const basket = mockBaskets.find(basket => basket.id === id)
+
+ if (basket) {
+ setFormData(basket)
+ } else {
+ setError('Gift basket not found')
+ }
+
+ setLoading(false)
+ }
+ }, [id, isEditMode])
+
+ const handleInputChange = (e) => {
+ const { name, value, type, checked } = e.target
+
+ let finalValue = value
+
+ // Handle numeric values
+ if (name === 'basePrice' || name === 'probability') {
+ finalValue = parseFloat(value) || 0
+ }
+
+ if (type === 'checkbox') {
+ setFormData({ ...formData, [name]: checked })
+ } else {
+ setFormData({ ...formData, [name]: finalValue })
+ }
+ }
+
+ const handleNewItemChange = (e) => {
+ const { name, value } = e.target
+
+ let finalValue = value
+
+ // Handle numeric values
+ if (name === 'quantity') {
+ finalValue = parseInt(value) || 1
+ } else if (name === 'value') {
+ finalValue = parseFloat(value) || 0
+ }
+
+ setNewItem({ ...newItem, [name]: finalValue })
+ }
+
+ const addItem = () => {
+ // Validate
+ if (!newItem.name || !newItem.value) {
+ setError('Item name and value are required')
+ return
+ }
+
+ // Add item with a unique ID
+ const newId = formData.items.length > 0
+ ? Math.max(...formData.items.map(item => item.id)) + 1
+ : 1
+
+ const items = [...formData.items, { ...newItem, id: newId }]
+
+ // Update form data
+ setFormData({ ...formData, items })
+
+ // Reset new item form
+ setNewItem({
+ name: '',
+ quantity: 1,
+ value: '',
+ })
+
+ // Clear any error
+ setError(null)
+ }
+
+ const removeItem = (id) => {
+ const items = formData.items.filter(item => item.id !== id)
+ setFormData({ ...formData, items })
+ }
+
+ const calculateTotalValue = () => {
+ return formData.items.reduce((total, item) => {
+ return total + (parseFloat(item.value) * item.quantity)
+ }, 0)
+ }
+
+ const validateForm = () => {
+ // Reset error
+ setError(null)
+
+ // Basic validation
+ if (!formData.name) {
+ setError('Basket name is required')
+ return false
+ }
+
+ if (formData.basePrice <= 0) {
+ setError('Base price must be greater than zero')
+ return false
+ }
+
+ if (formData.probability < 0 || formData.probability > 1) {
+ setError('Probability must be between 0 and 1')
+ return false
+ }
+
+ if (formData.items.length === 0) {
+ setError('At least one item is required in the basket')
+ return false
+ }
+
+ return true
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ if (!validateForm()) {
+ return
+ }
+
+ setSaving(true)
+
+ try {
+ // Simulate API call
+ await new Promise(resolve => setTimeout(resolve, 1000))
+
+ setSuccess(isEditMode ? 'Gift basket updated successfully!' : 'Gift basket created successfully!')
+
+ // Redirect after successful save
+ setTimeout(() => {
+ navigate('/gift-baskets/list')
+ }, 1500)
+ } catch (error) {
+ setError('Failed to save gift basket. Please try again.')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ if (loading) {
+ return
Loading gift basket data...
+ }
+
+ return (
+
+
+
+
+ {isEditMode ? 'Edit Gift Basket' : 'Create New Gift Basket'}
+
+
+ {error && {error} }
+ {success && {success} }
+
+
+
+
+ Basket Name
+
+
+
+ Status
+
+ Active
+ Inactive
+
+
+
+
+
+ Description
+
+
+
+
+
+ Base Price
+
+ $
+
+
+
+
+
+ Probability (0-1)
+
+ {formData.probability && `${(formData.probability * 100).toFixed(1)}%`}
+
+
+
+
+ Note: Overall probabilities can be adjusted in the Probabilities page
+
+
+
+
+
+
+ Basket Items
+
+
+
+
+
+ Item Name
+
+
+
+ Quantity
+
+
+
+ Value
+
+ $
+
+
+
+
+
+
+ Add
+
+
+
+
+
+
+
+
+
+ #
+ Item Name
+ Quantity
+ Value
+ Total
+ Actions
+
+
+
+ {formData.items.length > 0 ? (
+ <>
+ {formData.items.map((item, index) => (
+
+ {index + 1}
+ {item.name}
+ {item.quantity}
+ ${parseFloat(item.value).toFixed(2)}
+ ${(item.quantity * parseFloat(item.value)).toFixed(2)}
+
+ removeItem(item.id)}
+ >
+
+
+
+
+ ))}
+
+
+ Total Value:
+
+ ${calculateTotalValue().toFixed(2)}
+
+
+ >
+ ) : (
+
+
+ No items added yet
+
+
+ )}
+
+
+
+
+ navigate('/gift-baskets/list')}
+ >
+
+ Cancel
+
+
+
+ {saving ? 'Saving...' : 'Save Gift Basket'}
+
+
+
+
+
+
+
+ )
+}
+
+export default GiftBasketForm
\ No newline at end of file
diff --git a/src/views/gift-baskets/GiftBasketProbabilities.js b/src/views/gift-baskets/GiftBasketProbabilities.js
new file mode 100644
index 000000000..cd0bd967b
--- /dev/null
+++ b/src/views/gift-baskets/GiftBasketProbabilities.js
@@ -0,0 +1,273 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CForm,
+ CFormInput,
+ CFormLabel,
+ CProgress,
+ CProgressBar,
+ CTooltip,
+ CAlert,
+ CInputGroup,
+ CInputGroupText,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilWarning, cilSave, cilRedo } from '@coreui/icons'
+
+// Mock data for gift baskets with probabilities
+const initialBaskets = [
+ {
+ id: 1,
+ name: 'Premium Gift Basket',
+ basePrice: 99.99,
+ probability: 0.05,
+ },
+ {
+ id: 2,
+ name: 'Standard Gift Basket',
+ basePrice: 49.99,
+ probability: 0.15,
+ },
+ {
+ id: 3,
+ name: 'Basic Gift Basket',
+ basePrice: 29.99,
+ probability: 0.30,
+ },
+ {
+ id: 4,
+ name: 'Seasonal Special',
+ basePrice: 69.99,
+ probability: 0.10,
+ },
+ {
+ id: 5,
+ name: 'Holiday Bundle',
+ basePrice: 79.99,
+ probability: 0.10,
+ },
+ {
+ id: 6,
+ name: 'Mini Gift Basket',
+ basePrice: 19.99,
+ probability: 0.30,
+ },
+]
+
+const GiftBasketProbabilities = () => {
+ const [baskets, setBaskets] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [totalProbability, setTotalProbability] = useState(0)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+ const [saving, setSaving] = useState(false)
+
+ // Load mock data on component mount
+ useEffect(() => {
+ const fetchBaskets = () => {
+ setLoading(true)
+
+ // Simulate API call
+ setTimeout(() => {
+ setBaskets(initialBaskets)
+ setLoading(false)
+ }, 500)
+ }
+
+ fetchBaskets()
+ }, [])
+
+ // Calculate total probability whenever baskets change
+ useEffect(() => {
+ const total = baskets.reduce((sum, basket) => sum + parseFloat(basket.probability || 0), 0)
+ setTotalProbability(total)
+ }, [baskets])
+
+ const handleProbabilityChange = (id, value) => {
+ // Convert value to number and ensure it's between 0 and 1
+ const probability = Math.min(Math.max(parseFloat(value) || 0, 0), 1)
+
+ setBaskets(baskets.map(basket =>
+ basket.id === id ? { ...basket, probability } : basket
+ ))
+ }
+
+ const handleSave = async () => {
+ // Validate total probability is close to 1
+ if (Math.abs(totalProbability - 1) > 0.01) {
+ setError('Total probability must equal 100%. Please adjust the values.')
+ return
+ }
+
+ setSaving(true)
+ setError(null)
+
+ try {
+ // Simulate API call
+ await new Promise(resolve => setTimeout(resolve, 1000))
+
+ setSuccess('Probability settings saved successfully!')
+
+ // Clear success message after delay
+ setTimeout(() => {
+ setSuccess(null)
+ }, 3000)
+ } catch (error) {
+ setError('Failed to save probability settings. Please try again.')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const handleReset = () => {
+ if (window.confirm('Are you sure you want to reset probabilities to default values?')) {
+ setBaskets(initialBaskets)
+ }
+ }
+
+ const distributeEvenly = () => {
+ const evenProbability = 1 / baskets.length
+ setBaskets(baskets.map(basket => ({ ...basket, probability: evenProbability })))
+ }
+
+ const getProgressBarColor = () => {
+ if (Math.abs(totalProbability - 1) < 0.01) return 'success'
+ if (totalProbability > 1) return 'danger'
+ return 'warning'
+ }
+
+ return (
+
+
+
+
+ Gift Basket Probabilities
+
+
+ {error && {error} }
+ {success && {success} }
+
+
+
+
+
+
Probability Distribution
+
+
+ Adjust the probability of each gift basket being selected. The total probability should equal 100%.
+
+
+
+
Total Probability
+
+ {(totalProbability * 100).toFixed(1)}%
+
+
+
+
+
+
+
+
+
+
+ Distribute Evenly
+
+
+ Reset to Default
+
+
+
+
+
+ {loading ? (
+ Loading gift baskets...
+ ) : (
+
+
+
+
+ Basket Name
+ Base Price
+ Probability
+ Percentage
+
+
+
+ {baskets.map(basket => (
+
+
+ {basket.name}
+
+
+ ${basket.basePrice.toFixed(2)}
+
+
+
+ handleProbabilityChange(basket.id, e.target.value)}
+ style={{ flexGrow: 2 }}
+ />
+ handleProbabilityChange(basket.id, e.target.value)}
+ style={{ width: '80px' }}
+ />
+
+
+
+ {(basket.probability * 100).toFixed(1)}%
+
+
+ ))}
+
+
+
+
+
+ Reset
+
+ 0.01}
+ >
+
+ {saving ? 'Saving...' : 'Save Changes'}
+
+
+
+ )}
+
+
+
+
+ )
+}
+
+export default GiftBasketProbabilities
\ No newline at end of file
diff --git a/src/views/gift-baskets/GiftBasketsList.js b/src/views/gift-baskets/GiftBasketsList.js
new file mode 100644
index 000000000..89d686dbf
--- /dev/null
+++ b/src/views/gift-baskets/GiftBasketsList.js
@@ -0,0 +1,350 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CDropdown,
+ CDropdownToggle,
+ CDropdownMenu,
+ CDropdownItem,
+ CBadge,
+ CFormInput,
+ CForm,
+ CFormSelect,
+ CPagination,
+ CPaginationItem,
+ CProgress,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilOptions, cilPencil, cilTrash, cilPlus, cilSearch } from '@coreui/icons'
+import { Link, useNavigate } from 'react-router-dom'
+
+// Mock data for gift baskets
+const mockBaskets = [
+ {
+ id: 1,
+ name: 'Premium Gift Basket',
+ description: 'A luxury gift basket with premium items',
+ basePrice: 99.99,
+ totalItems: 10,
+ probability: 0.05,
+ status: 'active',
+ createdAt: '2023-06-15T10:30:00Z',
+ },
+ {
+ id: 2,
+ name: 'Standard Gift Basket',
+ description: 'A balanced gift basket with standard items',
+ basePrice: 49.99,
+ totalItems: 8,
+ probability: 0.15,
+ status: 'active',
+ createdAt: '2023-07-20T14:20:00Z',
+ },
+ {
+ id: 3,
+ name: 'Basic Gift Basket',
+ description: 'An affordable gift basket with essential items',
+ basePrice: 29.99,
+ totalItems: 6,
+ probability: 0.30,
+ status: 'active',
+ createdAt: '2023-08-05T11:45:00Z',
+ },
+ {
+ id: 4,
+ name: 'Seasonal Special',
+ description: 'Limited-time seasonal gift basket',
+ basePrice: 69.99,
+ totalItems: 12,
+ probability: 0.10,
+ status: 'inactive',
+ createdAt: '2023-09-01T09:15:00Z',
+ },
+]
+
+const GiftBasketsList = () => {
+ const navigate = useNavigate()
+ const [baskets, setBaskets] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [page, setPage] = useState(1)
+ const [totalPages, setTotalPages] = useState(1)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [filterStatus, setFilterStatus] = useState('')
+ const [filterPriceRange, setFilterPriceRange] = useState('')
+
+ // Load mock data on component mount
+ useEffect(() => {
+ const fetchBaskets = () => {
+ setLoading(true)
+
+ // Filter mock data based on filters
+ let filtered = [...mockBaskets]
+
+ if (searchTerm) {
+ const term = searchTerm.toLowerCase()
+ filtered = filtered.filter(basket =>
+ basket.name.toLowerCase().includes(term) ||
+ basket.description.toLowerCase().includes(term)
+ )
+ }
+
+ if (filterStatus) {
+ filtered = filtered.filter(basket => basket.status === filterStatus)
+ }
+
+ if (filterPriceRange) {
+ switch (filterPriceRange) {
+ case 'under30':
+ filtered = filtered.filter(basket => basket.basePrice < 30)
+ break
+ case '30to50':
+ filtered = filtered.filter(basket => basket.basePrice >= 30 && basket.basePrice <= 50)
+ break
+ case '50to100':
+ filtered = filtered.filter(basket => basket.basePrice > 50 && basket.basePrice <= 100)
+ break
+ case 'over100':
+ filtered = filtered.filter(basket => basket.basePrice > 100)
+ break
+ default:
+ break
+ }
+ }
+
+ setBaskets(filtered)
+ setTotalPages(Math.max(1, Math.ceil(filtered.length / 10)))
+ setLoading(false)
+ }
+
+ fetchBaskets()
+ }, [searchTerm, filterStatus, filterPriceRange])
+
+ const handleSearch = (e) => {
+ e.preventDefault()
+ setPage(1) // Reset to first page on new search
+ }
+
+ const handleDelete = (id) => {
+ if (window.confirm('Are you sure you want to delete this gift basket?')) {
+ // For mock data, just filter out the deleted basket
+ setBaskets(baskets.filter(basket => basket.id !== id))
+ }
+ }
+
+ const getStatusBadge = (status) => {
+ return status === 'active' ?
+
Active :
+
Inactive
+ }
+
+ const getProbabilityBadge = (probability) => {
+ const percentage = probability * 100
+ let color = 'success'
+
+ if (percentage < 10) color = 'danger'
+ else if (percentage < 20) color = 'warning'
+
+ return (
+
+
{percentage.toFixed(1)}%
+
+
+ )
+ }
+
+ // Get paginated baskets
+ const paginatedBaskets = baskets.slice((page - 1) * 10, page * 10)
+
+ return (
+
+
+
+
+ Gift Baskets
+ navigate('/gift-baskets/create')}
+ >
+
+ Create Basket
+
+
+
+ {/* Search and Filters */}
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+ {
+ setFilterPriceRange(e.target.value)
+ setPage(1)
+ }}
+ >
+ All Price Ranges
+ Under $30
+ $30 - $50
+ $50 - $100
+ Over $100
+
+
+
+ {
+ setFilterStatus(e.target.value)
+ setPage(1)
+ }}
+ >
+ All Status
+ Active
+ Inactive
+
+
+
+
+
+ Search
+
+
+
+
+
+ {/* Gift Baskets Table */}
+ {loading ? (
+ Loading gift baskets...
+ ) : (
+ <>
+
+
+
+ Name
+ Description
+ Price
+ Items
+ Probability
+ Status
+ Actions
+
+
+
+ {paginatedBaskets.length > 0 ? (
+ paginatedBaskets.map(basket => (
+
+
+ {basket.name}
+ ID: {basket.id}
+
+
+ {basket.description}
+
+
+ ${basket.basePrice.toFixed(2)}
+
+
+ {basket.totalItems}
+
+
+ {getProbabilityBadge(basket.probability)}
+
+
+ {getStatusBadge(basket.status)}
+
+
+
+
+
+
+
+
+ View Details
+
+
+
+ Edit
+
+ handleDelete(basket.id)}
+ style={{ color: 'red' }}
+ >
+
+ Delete
+
+
+
+
+
+ ))
+ ) : (
+
+
+ No gift baskets found
+
+
+ )}
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+ setPage(page - 1)}
+ >
+ Previous
+
+
+ {[...Array(totalPages).keys()].map(number => (
+ setPage(number + 1)}
+ >
+ {number + 1}
+
+ ))}
+
+ setPage(page + 1)}
+ >
+ Next
+
+
+ )}
+ >
+ )}
+
+
+
+
+ )
+}
+
+export default GiftBasketsList
\ No newline at end of file
diff --git a/src/views/gift-boxes/GiftBoxForm.js b/src/views/gift-boxes/GiftBoxForm.js
new file mode 100644
index 000000000..2ab20ac0e
--- /dev/null
+++ b/src/views/gift-boxes/GiftBoxForm.js
@@ -0,0 +1,295 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CForm,
+ CFormInput,
+ CFormLabel,
+ CFormSelect,
+ CButton,
+ CAlert,
+ CInputGroup,
+ CInputGroupText,
+ CInputGroupAppend,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilSave, cilX } from '@coreui/icons'
+import { useNavigate, useParams } from 'react-router-dom'
+
+// Mock data from API structure
+const mockBoxesData = {
+ firstBox: {
+ btcGivenToday: [
+ { type: "points", value: 50000, description: "VND 50,000", probability: 50.0 },
+ { type: "points", value: 70000, description: "VND 70,000", probability: 30.0 },
+ { type: "points", value: 100000, description: "VND 100,000", probability: 10.0 },
+ { type: "points", value: 120000, description: "VND 120,000", probability: 10.0 }
+ ],
+ btcNotGivenToday: [
+ { type: "points", value: 50000, description: "VND 50,000", probability: 50.0 },
+ { type: "points", value: 70000, description: "VND 70,000", probability: 30.0 },
+ { type: "points", value: 100000, description: "VND 100,000", probability: 10.0 },
+ { type: "points", value: 120000, description: "VND 120,000", probability: 9.9 },
+ { type: "btc", value: 1, description: "BTC", probability: 0.1 }
+ ]
+ },
+ regularBox: {
+ btcGivenToday: [
+ { type: "points", value: 1000, description: "VND 1.000", probability: 31.65 },
+ { type: "points", value: 20000, description: "VND 20.000", probability: 25.32 },
+ { type: "points", value: 30000, description: "VND 30.000", probability: 18.99 },
+ { type: "points", value: 50000, description: "VND 50.000", probability: 12.66 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 6.33 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 3.16 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 1.90 }
+ ],
+ btcNotGivenToday: [
+ { type: "points", value: 10000, description: "VND 10.000", probability: 31.64 },
+ { type: "points", value: 20000, description: "VND 20.000", probability: 25.31 },
+ { type: "points", value: 30000, description: "VND 30.000", probability: 18.98 },
+ { type: "points", value: 50000, description: "VND 50.000", probability: 12.65 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 6.33 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 3.16 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 1.90 },
+ { type: "btc", value: 1, description: "BTC", probability: 0.0063 }
+ ]
+ }
+}
+
+const GiftBoxForm = () => {
+ const navigate = useNavigate()
+ // Get parameters from the URL
+ const { boxType, scenario, index } = useParams()
+ const isEditMode = !!index
+
+ const [formData, setFormData] = useState({
+ type: 'points',
+ value: '',
+ description: '',
+ probability: '',
+ })
+
+ const [loading, setLoading] = useState(isEditMode)
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+
+ // If in edit mode, fetch item data
+ useEffect(() => {
+ if (isEditMode && boxType && scenario) {
+ setLoading(true)
+
+ // Simulate API call to get existing data
+ setTimeout(() => {
+ try {
+ const itemData = mockBoxesData[boxType][scenario][parseInt(index)]
+ if (itemData) {
+ setFormData(itemData)
+ } else {
+ setError('Gift box item not found')
+ }
+ } catch (err) {
+ setError('Failed to load item data')
+ } finally {
+ setLoading(false)
+ }
+ }, 500)
+ }
+ }, [boxType, scenario, index, isEditMode])
+
+ const handleInputChange = (e) => {
+ const { name, value, type } = e.target
+
+ let finalValue = value
+
+ // Handle numeric values
+ if (name === 'value') {
+ finalValue = parseInt(value) || 0
+ } else if (name === 'probability') {
+ finalValue = parseFloat(value) || 0
+ }
+
+ setFormData({ ...formData, [name]: finalValue })
+ }
+
+ const validateForm = () => {
+ // Reset error
+ setError(null)
+
+ // Basic validation
+ if (!formData.description) {
+ setError('Description is required')
+ return false
+ }
+
+ if (formData.value <= 0) {
+ setError('Value must be greater than zero')
+ return false
+ }
+
+ if (formData.probability <= 0 || formData.probability > 100) {
+ setError('Probability must be between 0 and 100')
+ return false
+ }
+
+ return true
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ if (!validateForm()) {
+ return
+ }
+
+ setSaving(true)
+
+ try {
+ // Simulate API call
+ await new Promise(resolve => setTimeout(resolve, 1000))
+
+ setSuccess(isEditMode ? 'Gift box item updated successfully!' : 'Gift box item created successfully!')
+
+ // Redirect after successful save
+ setTimeout(() => {
+ navigate('/gift-boxes/list')
+ }, 1500)
+ } catch (error) {
+ setError('Failed to save gift box item. Please try again.')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ if (loading) {
+ return
Loading gift box item data...
+ }
+
+ return (
+
+
+
+
+ {isEditMode ? 'Edit Gift Box Item' : 'Create New Gift Box Item'}
+
+
+ {error && {error} }
+ {success && {success} }
+
+
+
+
+ Box Type
+ navigate(`/gift-boxes/${isEditMode ? 'edit' : 'create'}/${e.target.value}/${scenario || 'btcNotGivenToday'}${isEditMode ? `/${index}` : ''}`)}
+ disabled={isEditMode}
+ >
+ First Box
+ Regular Box
+
+
+
+ Scenario
+ navigate(`/gift-boxes/${isEditMode ? 'edit' : 'create'}/${boxType || 'firstBox'}/${e.target.value}${isEditMode ? `/${index}` : ''}`)}
+ disabled={isEditMode}
+ >
+ BTC Not Given Today
+ BTC Given Today
+
+
+
+
+
+
+ Reward Type
+
+ Points
+ BTC
+
+
+
+ Value
+
+
+
+
+
+
+ Description
+
+
+
+ Probability (%)
+
+
+ %
+
+
+
+
+
+ navigate('/gift-boxes/list')}
+ >
+
+ Cancel
+
+
+
+ {saving ? 'Saving...' : 'Save Item'}
+
+
+
+
+
+
+
+ )
+}
+
+export default GiftBoxForm
\ No newline at end of file
diff --git a/src/views/gift-boxes/GiftBoxProbabilities.js b/src/views/gift-boxes/GiftBoxProbabilities.js
new file mode 100644
index 000000000..068b8b7e9
--- /dev/null
+++ b/src/views/gift-boxes/GiftBoxProbabilities.js
@@ -0,0 +1,405 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CFormSelect,
+ CFormInput,
+ CInputGroup,
+ CInputGroupText,
+ CProgress,
+ CNav,
+ CNavItem,
+ CNavLink,
+ CTabContent,
+ CTabPane,
+ CBadge,
+ CProgressBar,
+ CAlert,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilSave } from '@coreui/icons'
+
+// Mock data based on the new API structure - same as GiftBoxesList.js
+const mockBoxesData = {
+ firstBox: {
+ btcGivenToday: [
+ { type: "points", value: 50000, description: "VND 50.000", probability: 50.0 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 30.0 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 10.0 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 10.0 }
+ ],
+ btcNotGivenToday: [
+ { type: "points", value: 50000, description: "VND 50.000", probability: 50.0 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 30.0 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 10.0 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 9.9 },
+ { type: "btc", value: 1, description: "BTC", probability: 0.1 }
+ ]
+ },
+ regularBox: {
+ btcGivenToday: [
+ { type: "points", value: 1000, description: "VND 1.000", probability: 31.65 },
+ { type: "points", value: 20000, description: "VND 20.000", probability: 25.32 },
+ { type: "points", value: 30000, description: "VND 30.000", probability: 18.99 },
+ { type: "points", value: 50000, description: "VND 50.000", probability: 12.66 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 6.33 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 3.16 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 1.89 }
+ ],
+ btcNotGivenToday: [
+ { type: "points", value: 10000, description: "VND 10.000", probability: 31.64 },
+ { type: "points", value: 20000, description: "VND 20.000", probability: 25.31 },
+ { type: "points", value: 30000, description: "VND 30.000", probability: 18.98 },
+ { type: "points", value: 50000, description: "VND 50.000", probability: 12.65 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 6.33 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 3.16 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 1.9 },
+ { type: "btc", value: 1, description: "BTC", probability: 0.0063 }
+ ]
+ }
+}
+
+const GiftBoxProbabilities = () => {
+ const [boxesData, setBoxesData] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [activeTab, setActiveTab] = useState(1)
+ const [activeScenario, setActiveScenario] = useState('btcNotGivenToday')
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+
+ // Load mock data on component mount
+ useEffect(() => {
+ const fetchBoxes = () => {
+ setLoading(true)
+
+ // Simulate API call
+ setTimeout(() => {
+ setBoxesData(JSON.parse(JSON.stringify(mockBoxesData)))
+ setLoading(false)
+ }, 500)
+ }
+
+ fetchBoxes()
+ }, [])
+
+ const getBoxType = () => {
+ return activeTab === 1 ? 'firstBox' : 'regularBox'
+ }
+
+ const calculateTotalProbability = (items) => {
+ return items?.reduce((sum, item) => sum + item.probability, 0) || 0
+ }
+
+ const getTypeBadge = (type) => {
+ return type === 'btc' ?
+
BTC :
+
Points
+ }
+
+ const handleValueChange = (boxType, scenario, index, field, value) => {
+ if (!boxesData) return
+
+ const updatedBoxesData = {...boxesData}
+
+ if (field === 'probability') {
+ // Convert value to number and limit to valid range
+ let newProbability = parseFloat(value)
+ if (isNaN(newProbability)) newProbability = 0
+ if (newProbability < 0) newProbability = 0
+ if (newProbability > 100) newProbability = 100
+
+ // Round to 4 decimal places
+ newProbability = parseFloat(newProbability.toFixed(4))
+
+ const oldProbability = updatedBoxesData[boxType][scenario][index].probability
+ const difference = newProbability - oldProbability
+
+ // Update the changed item
+ updatedBoxesData[boxType][scenario][index].probability = newProbability
+
+ // Auto-balance other probabilities
+ if (difference !== 0) {
+ const otherItems = updatedBoxesData[boxType][scenario].filter((_, i) => i !== index)
+ const totalOtherProbability = otherItems.reduce((sum, item) => sum + item.probability, 0)
+
+ if (totalOtherProbability > 0) {
+ // Distribute the difference proportionally
+ updatedBoxesData[boxType][scenario].forEach((item, i) => {
+ if (i !== index) {
+ const ratio = item.probability / totalOtherProbability
+ let adjustedProbability = Math.max(0, item.probability - (difference * ratio))
+ // Round to 4 decimal places
+ adjustedProbability = parseFloat(adjustedProbability.toFixed(4))
+ updatedBoxesData[boxType][scenario][i].probability = adjustedProbability
+ }
+ })
+ }
+ }
+
+ // Ensure total is exactly 100%
+ const total = calculateTotalProbability(updatedBoxesData[boxType][scenario])
+ if (Math.abs(total - 100) > 0.0001) {
+ // Find the item with the highest probability (that's not the current one) to adjust
+ const itemsToAdjust = updatedBoxesData[boxType][scenario]
+ .map((item, i) => ({ index: i, probability: item.probability }))
+ .filter(item => item.index !== index)
+ .sort((a, b) => b.probability - a.probability)
+
+ if (itemsToAdjust.length > 0) {
+ const adjustIndex = itemsToAdjust[0].index
+ const adjustment = 100 - total
+ const newValue = parseFloat((updatedBoxesData[boxType][scenario][adjustIndex].probability + adjustment).toFixed(4))
+ updatedBoxesData[boxType][scenario][adjustIndex].probability = newValue
+ }
+ }
+ } else if (field === 'value') {
+ // Update numeric value
+ updatedBoxesData[boxType][scenario][index].value = parseInt(value) || 0
+ } else {
+ // Update other fields normally
+ updatedBoxesData[boxType][scenario][index][field] = value
+ }
+
+ setBoxesData(updatedBoxesData)
+ }
+
+ return (
+
+
+
+
+ Gift Box Probabilities
+
+
+ {error && {error} }
+ {success && {success} }
+
+ {/* Box Type Tabs */}
+
+
+ setActiveTab(1)}
+ >
+ First Box
+
+
+
+ setActiveTab(2)}
+ >
+ Regular Box
+
+
+
+
+ {/* Scenario Selection */}
+
+ setActiveScenario(e.target.value)}
+ >
+ Scenario: BTC Not Given Today
+ Scenario: BTC Given Today
+
+
+
+ {/* Box Content */}
+
+
+ {loading ? (
+ Loading gift box items...
+ ) : (
+ <>
+
+
First Box Items
+
+ Total Probability: {' '}
+
+ {calculateTotalProbability(boxesData?.firstBox?.[activeScenario]).toFixed(4)}%
+
+
+
+
+
+
+ #
+ Type
+ Value
+ Description
+ Probability (%)
+
+
+
+ {boxesData?.firstBox?.[activeScenario]?.length > 0 ? (
+ boxesData.firstBox[activeScenario].map((item, index) => (
+
+ {index + 1}
+ {getTypeBadge(item.type)}
+
+ handleValueChange('firstBox', activeScenario, index, 'value', e.target.value)}
+ min="0"
+ />
+
+
+ handleValueChange('firstBox', activeScenario, index, 'description', e.target.value)}
+ />
+
+
+
+ handleValueChange('firstBox', activeScenario, index, 'probability', e.target.value)}
+ min="0"
+ max="100"
+ step="0.0001"
+ />
+ %
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+ No gift box items found
+
+
+ )}
+
+
+ >
+ )}
+
+
+
+ {loading ? (
+ Loading gift box items...
+ ) : (
+ <>
+
+
Regular Box Items
+
+ Total Probability: {' '}
+
+ {calculateTotalProbability(boxesData?.regularBox?.[activeScenario]).toFixed(4)}%
+
+
+
+
+
+
+ #
+ Type
+ Value
+ Description
+ Probability (%)
+
+
+
+ {boxesData?.regularBox?.[activeScenario]?.length > 0 ? (
+ boxesData.regularBox[activeScenario].map((item, index) => (
+
+ {index + 1}
+ {getTypeBadge(item.type)}
+
+ handleValueChange('regularBox', activeScenario, index, 'value', e.target.value)}
+ min="0"
+ />
+
+
+ handleValueChange('regularBox', activeScenario, index, 'description', e.target.value)}
+ />
+
+
+
+ handleValueChange('regularBox', activeScenario, index, 'probability', e.target.value)}
+ min="0"
+ max="100"
+ step="0.0001"
+ />
+ %
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+ No gift box items found
+
+
+ )}
+
+
+ >
+ )}
+
+
+
+ {/* Submit Button */}
+
+ setSuccess('Changes saved successfully!')}
+ >
+
+ Save Changes
+
+
+
+
+
+
+ )
+}
+
+export default GiftBoxProbabilities
+
\ No newline at end of file
diff --git a/src/views/gift-boxes/GiftBoxProbabilities.js.bak b/src/views/gift-boxes/GiftBoxProbabilities.js.bak
new file mode 100644
index 000000000..3e09b21a3
--- /dev/null
+++ b/src/views/gift-boxes/GiftBoxProbabilities.js.bak
@@ -0,0 +1,479 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CForm,
+ CFormInput,
+ CFormLabel,
+ CFormSelect,
+ CProgress,
+ CNav,
+ CNavItem,
+ CNavLink,
+ CTabContent,
+ CTabPane,
+ CBadge,
+ CInputGroup,
+ CInputGroupText,
+ CTooltip,
+ CProgressBar,
+ CAlert,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilWarning, cilSave, cilRedo } from '@coreui/icons'
+
+// Mock data based on the new API structure - same as GiftBoxesList.js
+const mockBoxesData = {
+ firstBox: {
+ btcGivenToday: [
+ { type: "points", value: 50000, description: "VND 50.000", probability: 50.0 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 30.0 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 10.0 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 10.0 }
+ ],
+ btcNotGivenToday: [
+ { type: "points", value: 50000, description: "VND 50.000", probability: 50.0 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 30.0 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 10.0 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 9.9 },
+ { type: "btc", value: 1, description: "BTC", probability: 0.1 }
+ ]
+ },
+ regularBox: {
+ btcGivenToday: [
+ { type: "points", value: 1000, description: "VND 1.000", probability: 31.65 },
+ { type: "points", value: 20000, description: "VND 20.000", probability: 25.32 },
+ { type: "points", value: 30000, description: "VND 30.000", probability: 18.99 },
+ { type: "points", value: 50000, description: "VND 50.000", probability: 12.66 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 6.33 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 3.16 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 1.90 }
+ ],
+ btcNotGivenToday: [
+ { type: "points", value: 10000, description: "VND 10.000", probability: 31.64 },
+ { type: "points", value: 20000, description: "VND 20.000", probability: 25.31 },
+ { type: "points", value: 30000, description: "VND 30.000", probability: 18.98 },
+ { type: "points", value: 50000, description: "VND 50.000", probability: 12.65 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 6.33 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 3.16 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 1.90 },
+ { type: "btc", value: 1, description: "BTC", probability: 0.0063 }
+ ]
+ }
+}
+
+const GiftBoxProbabilities = () => {
+ const [boxesData, setBoxesData] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [activeTab, setActiveTab] = useState(1)
+ const [activeScenario, setActiveScenario] = useState('btcNotGivenToday')
+ const [editingItem, setEditingItem] = useState(null)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+ const [saving, setSaving] = useState(false)
+
+ // Load mock data on component mount
+ useEffect(() => {
+ const fetchBoxes = () => {
+ setLoading(true)
+
+ // Simulate API call
+ setTimeout(() => {
+ setBoxesData(JSON.parse(JSON.stringify(mockBoxesData)))
+ setLoading(false)
+ }, 500)
+ }
+
+ fetchBoxes()
+ }, [])
+
+ const getBoxType = () => {
+ return activeTab === 1 ? 'firstBox' : 'regularBox'
+ }
+
+ const getCurrentItems = () => {
+ if (!boxesData) return []
+ const boxType = getBoxType()
+ return boxesData[boxType][activeScenario] || []
+ }
+
+ const calculateTotalProbability = (items) => {
+ return items?.reduce((sum, item) => sum + item.probability, 0) || 0
+ }
+
+ const handleValueChange = (index, field, value) => {
+ if (!boxesData) return
+
+ const boxType = getBoxType()
+ const updatedBoxesData = {...boxesData}
+
+ if (field === 'probability') {
+ // Convert value to number and limit to valid range
+ let newProbability = parseFloat(value)
+ if (isNaN(newProbability)) newProbability = 0
+ if (newProbability < 0) newProbability = 0
+ if (newProbability > 100) newProbability = 100
+
+ const oldProbability = updatedBoxesData[boxType][activeScenario][index].probability
+ const difference = newProbability - oldProbability
+
+ // Update the changed item
+ updatedBoxesData[boxType][activeScenario][index].probability = newProbability
+
+ // Auto-balance other probabilities
+ if (difference !== 0) {
+ const otherItems = updatedBoxesData[boxType][activeScenario].filter((_, i) => i !== index)
+ const totalOtherProbability = otherItems.reduce((sum, item) => sum + item.probability, 0)
+
+ if (totalOtherProbability > 0) {
+ // Distribute the difference proportionally
+ updatedBoxesData[boxType][activeScenario].forEach((item, i) => {
+ if (i !== index) {
+ const ratio = item.probability / totalOtherProbability
+ const adjustedProbability = Math.max(0, item.probability - (difference * ratio))
+ updatedBoxesData[boxType][activeScenario][i].probability = adjustedProbability
+ }
+ })
+ }
+ }
+ } else if (field === 'value') {
+ // Update numeric value
+ updatedBoxesData[boxType][activeScenario][index].value = parseInt(value) || 0
+ } else {
+ // Update other fields normally
+ updatedBoxesData[boxType][activeScenario][index][field] = value
+ }
+
+ setBoxesData(updatedBoxesData)
+ }
+
+ const handleSave = async () => {
+ const currentItems = getCurrentItems()
+ const totalProbability = calculateTotalProbability(currentItems)
+
+ // Validate total probability is close to 100%
+ if (Math.abs(totalProbability - 100) > 0.1) {
+ setError('Total probability must equal 100%. Current total: ' + totalProbability.toFixed(2) + '%')
+ return
+ }
+
+ setSaving(true)
+ setError(null)
+
+ try {
+ // Simulate API call
+ await new Promise(resolve => setTimeout(resolve, 1000))
+
+ setSuccess('Probability settings saved successfully!')
+
+ // Clear success message after delay
+ setTimeout(() => {
+ setSuccess(null)
+ }, 3000)
+ } catch (error) {
+ setError('Failed to save probability settings. Please try again.')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const distributeEvenly = () => {
+ if (!boxesData) return
+
+ const boxType = getBoxType()
+ const updatedBoxesData = {...boxesData}
+ const items = updatedBoxesData[boxType][activeScenario]
+
+ if (items.length > 0) {
+ const evenProbability = 100 / items.length
+ items.forEach(item => {
+ item.probability = evenProbability
+ })
+
+ setBoxesData(updatedBoxesData)
+ }
+ }
+
+ const getTypeBadge = (type) => {
+ return type === 'btc' ?
+
BTC :
+
Points
+ }
+
+ return (
+
+
+
+
+ Gift Box Probabilities
+
+
+ {error && {error} }
+ {success && {success} }
+
+ {/* Box Type Tabs */}
+
+
+ setActiveTab(1)}
+ >
+ First Box
+
+
+
+ setActiveTab(2)}
+ >
+ Regular Box
+
+
+
+
+ {/* Scenario Selection */}
+
+ setActiveScenario(e.target.value)}
+ >
+ Scenario: BTC Not Given Today
+ Scenario: BTC Given Today
+
+
+
+ {/* Probability Distribution Info */}
+
+
+
+
+
Probability Distribution
+
+
+ Adjust the probability of each gift box item. The total probability should equal 100%.
+ Changes to one probability will automatically balance across other items.
+
+ {!loading && boxesData && (
+ <>
+
+
+
Total Probability
+
+ {calculateTotalProbability(getCurrentItems()).toFixed(2)}%
+
+
+
+
+
+
+
+
+ Distribute Evenly
+
+
+ >
+ )}
+
+
+
+ {/* Box Content */}
+
+
+ {loading ? (
+ Loading gift box items...
+ ) : (
+ <>
+
+
First Box Items
+
+ Total Probability: {' '}
+
+ {calculateTotalProbability(boxesData?.firstBox?.[activeScenario]).toFixed(2)}%
+
+
+
+
+
+
+ #
+ Type
+ Value
+ Description
+ Probability (%)
+
+
+
+ {boxesData?.firstBox?.[activeScenario]?.length > 0 ? (
+ boxesData.firstBox[activeScenario].map((item, index) => (
+
+ {index + 1}
+ {getTypeBadge(item.type)}
+
+ handleValueChange(index, 'value', e.target.value)}
+ min="0"
+ />
+
+
+ handleValueChange(index, 'description', e.target.value)}
+ />
+
+
+
+ handleValueChange(index, 'probability', e.target.value)}
+ min="0"
+ max="100"
+ step="0.1"
+ />
+ %
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+ No gift box items found
+
+
+ )}
+
+
+ >
+ )}
+
+
+
+ {loading ? (
+ Loading gift box items...
+ ) : (
+ <>
+
+
Regular Box Items
+
+ Total Probability: {' '}
+
+ {calculateTotalProbability(boxesData?.regularBox?.[activeScenario]).toFixed(2)}%
+
+
+
+
+
+
+ #
+ Type
+ Value
+ Description
+ Probability (%)
+
+
+
+ {boxesData?.regularBox?.[activeScenario]?.length > 0 ? (
+ boxesData.regularBox[activeScenario].map((item, index) => (
+
+ {index + 1}
+ {getTypeBadge(item.type)}
+
+ handleValueChange(index, 'value', e.target.value)}
+ min="0"
+ />
+
+
+ handleValueChange(index, 'description', e.target.value)}
+ />
+
+
+
+ handleValueChange(index, 'probability', e.target.value)}
+ min="0"
+ max="100"
+ step="0.1"
+ />
+ %
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+ No gift box items found
+
+
+ )}
+
+
+ >
+ )}
+
+
+
+ {/* Submit Button */}
+
+ 0.1)}
+ >
+
+ {saving ? 'Saving...' : 'Save Changes'}
+
+
+
+
+
+
+ )
+}
+
+export default GiftBoxProbabilities
\ No newline at end of file
diff --git a/src/views/gift-boxes/GiftBoxesList.js b/src/views/gift-boxes/GiftBoxesList.js
new file mode 100644
index 000000000..1d939118c
--- /dev/null
+++ b/src/views/gift-boxes/GiftBoxesList.js
@@ -0,0 +1,294 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CBadge,
+ CFormInput,
+ CForm,
+ CFormSelect,
+ CPagination,
+ CPaginationItem,
+ CProgress,
+ CNav,
+ CNavItem,
+ CNavLink,
+ CTabContent,
+ CTabPane,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilPencil, cilTrash, cilSearch } from '@coreui/icons'
+import { useNavigate } from 'react-router-dom'
+
+// Mock data based on the new API structure
+const mockBoxesData = {
+ firstBox: {
+ btcGivenToday: [
+ { type: "points", value: 50000, description: "VND 50.000", probability: 50.0 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 30.0 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 10.0 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 10.0 }
+ ],
+ btcNotGivenToday: [
+ { type: "points", value: 50000, description: "VND 50.000", probability: 50.0 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 30.0 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 10.0 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 9.9 },
+ { type: "btc", value: 1, description: "BTC", probability: 0.1 }
+ ]
+ },
+ regularBox: {
+ btcGivenToday: [
+ { type: "points", value: 1000, description: "VND 1.000", probability: 31.65 },
+ { type: "points", value: 20000, description: "VND 20.000", probability: 25.32 },
+ { type: "points", value: 30000, description: "VND 30.000", probability: 18.99 },
+ { type: "points", value: 50000, description: "VND 50.000", probability: 12.66 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 6.33 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 3.16 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 1.90 }
+ ],
+ btcNotGivenToday: [
+ { type: "points", value: 10000, description: "VND 10.000", probability: 31.64 },
+ { type: "points", value: 20000, description: "VND 20.000", probability: 25.31 },
+ { type: "points", value: 30000, description: "VND 30.000", probability: 18.98 },
+ { type: "points", value: 50000, description: "VND 50.000", probability: 12.65 },
+ { type: "points", value: 70000, description: "VND 70.000", probability: 6.33 },
+ { type: "points", value: 100000, description: "VND 100.000", probability: 3.16 },
+ { type: "points", value: 120000, description: "VND 120.000", probability: 1.90 },
+ { type: "btc", value: 1, description: "BTC", probability: 0.0063 }
+ ]
+ }
+}
+
+const GiftBoxesList = () => {
+ const navigate = useNavigate()
+ const [loading, setLoading] = useState(true)
+ const [boxesData, setBoxesData] = useState(null)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [activeTab, setActiveTab] = useState(1)
+ const [activeScenario, setActiveScenario] = useState('btcNotGivenToday')
+
+ // Load mock data on component mount
+ useEffect(() => {
+ const fetchBoxes = () => {
+ setLoading(true)
+
+ // Simulate API call
+ setTimeout(() => {
+ setBoxesData(mockBoxesData)
+ setLoading(false)
+ }, 500)
+ }
+
+ fetchBoxes()
+ }, [])
+
+ const handleSearch = (e) => {
+ e.preventDefault()
+ // Could implement client-side filtering here
+ }
+
+ const handleEditItem = (boxType, scenario, index) => {
+ // Navigate to edit view with necessary params
+ navigate(`/gift-boxes/edit/${boxType}/${scenario}/${index}`)
+ }
+
+ const handleDeleteItem = (boxType, scenario, index) => {
+ if (window.confirm('Are you sure you want to delete this item?')) {
+ // Create a deep copy of boxesData
+ const updatedBoxesData = JSON.parse(JSON.stringify(boxesData))
+ // Remove the item at the specified index
+ updatedBoxesData[boxType][scenario].splice(index, 1)
+ // Update state
+ setBoxesData(updatedBoxesData)
+ }
+ }
+
+ const getProbabilityBadge = (probability) => {
+ let color = 'success'
+
+ if (probability < 1) color = 'danger'
+ else if (probability < 10) color = 'warning'
+
+ return (
+
+
{probability.toFixed(4)}%
+
+
+ )
+ }
+
+ const getTypeBadge = (type) => {
+ return type === 'btc' ?
+
BTC :
+
Points
+ }
+
+ const calculateTotalProbability = (items) => {
+ return items?.reduce((sum, item) => sum + item.probability, 0) || 0
+ }
+
+ return (
+
+
+
+
+ Gift Boxes
+
+
+
+ {/* Box Type Tabs */}
+
+
+ setActiveTab(1)}
+ >
+ First Box
+
+
+
+ setActiveTab(2)}
+ >
+ Regular Box
+
+
+
+
+ {/* Scenario Selection */}
+
+ setActiveScenario(e.target.value)}
+ >
+ Scenario: BTC Not Given Today
+ Scenario: BTC Given Today
+
+
+
+ {/* Box Content */}
+
+
+ {loading ? (
+ Loading gift box items...
+ ) : (
+ <>
+
+
First Box Items
+
+ Total Probability: {' '}
+
+ {calculateTotalProbability(boxesData?.firstBox?.[activeScenario]).toFixed(2)}%
+
+
+
+
+
+
+ #
+ Type
+ Value
+ Description
+ Probability (%)
+
+
+
+ {boxesData?.firstBox?.[activeScenario]?.length > 0 ? (
+ boxesData.firstBox[activeScenario].map((item, index) => (
+
+ {index + 1}
+ {getTypeBadge(item.type)}
+ {item.value}
+ {item.description}
+ {getProbabilityBadge(item.probability)}
+
+ ))
+ ) : (
+
+
+ No gift box items found
+
+
+ )}
+
+
+ >
+ )}
+
+
+
+ {loading ? (
+ Loading gift box items...
+ ) : (
+ <>
+
+
Regular Box Items
+
+ Total Probability: {' '}
+
+ {calculateTotalProbability(boxesData?.regularBox?.[activeScenario]).toFixed(2)}%
+
+
+
+
+
+
+ #
+ Type
+ Value
+ Description
+ Probability (%)
+
+
+
+ {boxesData?.regularBox?.[activeScenario]?.length > 0 ? (
+ boxesData.regularBox[activeScenario].map((item, index) => (
+
+ {index + 1}
+ {getTypeBadge(item.type)}
+ {item.value}
+ {item.description}
+ {getProbabilityBadge(item.probability)}
+
+ ))
+ ) : (
+
+
+ No gift box items found
+
+
+ )}
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ )
+}
+
+export default GiftBoxesList
\ No newline at end of file
diff --git a/src/views/management/users/UserDetail.js b/src/views/management/users/UserDetail.js
new file mode 100644
index 000000000..6330a0edb
--- /dev/null
+++ b/src/views/management/users/UserDetail.js
@@ -0,0 +1,271 @@
+import React, { useState, useEffect } from 'react'
+import { useParams, Link } from 'react-router-dom'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CNav,
+ CNavItem,
+ CNavLink,
+ CTabContent,
+ CTabPane,
+ CTabs,
+ CButton,
+ CButtonGroup,
+ CListGroup,
+ CListGroupItem,
+ CBadge,
+ CSpinner,
+ CCardFooter
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilArrowLeft, cilPencil, cilLockLocked, cilTrash } from '@coreui/icons'
+
+const UserDetail = () => {
+ const { id } = useParams()
+ const [activeKey, setActiveKey] = useState(1)
+ const [isLoading, setIsLoading] = useState(false)
+ const [user, setUser] = useState(null)
+
+ // Mock data - would be fetched from API in real application
+ useEffect(() => {
+ setIsLoading(true)
+ // Simulate API call
+ setTimeout(() => {
+ const userData = {
+ id: parseInt(id),
+ username: 'admin',
+ name: 'Administrator',
+ email: 'admin@example.com',
+ role: 'Admin',
+ status: 'Active',
+ lastLogin: '2023-05-20 14:30:45',
+ created: '2023-01-15 08:00:00',
+ phone: '+1 (555) 123-4567',
+ groups: ['Administrators', 'Content Managers'],
+ permissions: [
+ 'users.view', 'users.create', 'users.edit', 'users.delete',
+ 'content.view', 'content.create', 'content.edit', 'content.delete',
+ 'settings.view', 'settings.edit'
+ ],
+ recentActivity: [
+ { date: '2023-05-20 14:30:45', action: 'Logged in', ip: '192.168.1.1' },
+ { date: '2023-05-19 16:45:22', action: 'Updated user settings', ip: '192.168.1.1' },
+ { date: '2023-05-18 09:12:51', action: 'Created new article', ip: '192.168.1.1' },
+ { date: '2023-05-17 11:30:08', action: 'Logged out', ip: '192.168.1.1' },
+ ]
+ }
+ setUser(userData)
+ setIsLoading(false)
+ }, 800)
+ }, [id])
+
+ const getBadgeColor = (status) => {
+ switch (status) {
+ case 'Active':
+ return 'success'
+ case 'Inactive':
+ return 'secondary'
+ case 'Suspended':
+ return 'danger'
+ case 'Pending':
+ return 'warning'
+ default:
+ return 'primary'
+ }
+ }
+
+ if (isLoading) {
+ return (
+
+
+
Loading user details...
+
+ )
+ }
+
+ if (!user) {
+ return (
+
+
+
+
User not found
+
The requested user does not exist or you don't have permission to view it.
+
+
+
+ Back to Users
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+ User Details
+ Viewing user information
+
+
+
+
+
+
+ Back
+
+
+
+
+
+ Edit
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+ setActiveKey(1)}
+ role="tab"
+ >
+ Profile
+
+
+
+ setActiveKey(2)}
+ role="tab"
+ >
+ Groups & Permissions
+
+
+
+ setActiveKey(3)}
+ role="tab"
+ >
+ Activity Log
+
+
+
+
+
+
+
{user.name}
+ {user.status}
+ {user.role}
+
+
+
+
+
+
+ Username
+ {user.username}
+
+
+ Email
+ {user.email}
+
+
+ Phone
+ {user.phone}
+
+
+
+
+
+
+ Last Login
+ {user.lastLogin}
+
+
+ Account Created
+ {user.created}
+
+
+
+
Security
+
+
+ Change Password
+
+
+
+
+
+
+
+
+
+
+ User Groups
+
+ {user.groups.map((group, index) => (
+
+ {group}
+ Member
+
+ ))}
+
+
+ Add to Group
+
+
+
+ Permissions
+
+ {user.permissions.map((permission, index) => (
+ {permission}
+ ))}
+
+
+
+
+
+ Recent Activity
+
+ {user.recentActivity.map((activity, index) => (
+
+
+
{activity.action}
+
IP: {activity.ip}
+
+ {activity.date}
+
+ ))}
+
+
+
+
+
+
+ Last updated: {new Date().toLocaleString()}
+
+
+
+
+
+ )
+}
+
+export default UserDetail
\ No newline at end of file
diff --git a/src/views/management/users/Users.js b/src/views/management/users/Users.js
new file mode 100644
index 000000000..bd66ce917
--- /dev/null
+++ b/src/views/management/users/Users.js
@@ -0,0 +1,198 @@
+import React, { useState } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableBody,
+ CTableDataCell,
+ CTableHead,
+ CTableHeaderCell,
+ CTableRow,
+ CButton,
+ CInputGroup,
+ CFormInput,
+ CDropdown,
+ CDropdownToggle,
+ CDropdownMenu,
+ CDropdownItem,
+ CButtonGroup,
+ CBadge,
+ CSpinner
+} from '@coreui/react'
+import { cilPeople, cilSearch, cilPlus } from '@coreui/icons'
+import CIcon from '@coreui/icons-react'
+import { Link } from 'react-router-dom'
+
+const Users = () => {
+ const [isLoading, setIsLoading] = useState(false)
+
+ // Mock data - would be fetched from API in real application
+ const users = [
+ {
+ id: 1,
+ username: 'admin',
+ name: 'Administrator',
+ email: 'admin@example.com',
+ role: 'Admin',
+ status: 'Active',
+ lastLogin: '2023-05-20 14:30:45'
+ },
+ {
+ id: 2,
+ username: 'editor',
+ name: 'Content Editor',
+ email: 'editor@example.com',
+ role: 'Editor',
+ status: 'Active',
+ lastLogin: '2023-05-19 09:15:22'
+ },
+ {
+ id: 3,
+ username: 'moderator',
+ name: 'Content Moderator',
+ email: 'moderator@example.com',
+ role: 'Moderator',
+ status: 'Inactive',
+ lastLogin: '2023-04-15 16:42:10'
+ },
+ {
+ id: 4,
+ username: 'user1',
+ name: 'Regular User',
+ email: 'user1@example.com',
+ role: 'User',
+ status: 'Active',
+ lastLogin: '2023-05-18 11:20:33'
+ },
+ ]
+
+ const getBadgeColor = (status) => {
+ switch (status) {
+ case 'Active':
+ return 'success'
+ case 'Inactive':
+ return 'secondary'
+ case 'Suspended':
+ return 'danger'
+ case 'Pending':
+ return 'warning'
+ default:
+ return 'primary'
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ Users
+ Manage system users
+
+
+
+
+
+ Add New User
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Filter
+
+ All Users
+ Active Users
+ Inactive Users
+
+
+
+
+
+
+ Export
+ Import
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+
+
+ Username
+ Name
+ Email
+ Role
+ Status
+ Last Login
+ Actions
+
+
+
+ {users.map((user) => (
+
+ {user.username}
+ {user.name}
+ {user.email}
+ {user.role}
+
+
+ {user.status}
+
+
+ {user.lastLogin}
+
+
+
+
+ View
+
+
+
+
+ Edit
+
+
+
+ Delete
+
+
+
+
+ ))}
+
+
+ )}
+
+
+
+
+ )
+}
+
+export default Users
\ No newline at end of file
diff --git a/src/views/tickets/TicketTypes.js b/src/views/tickets/TicketTypes.js
new file mode 100644
index 000000000..f414fcd63
--- /dev/null
+++ b/src/views/tickets/TicketTypes.js
@@ -0,0 +1,340 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CModal,
+ CModalHeader,
+ CModalTitle,
+ CModalBody,
+ CModalFooter,
+ CForm,
+ CFormInput,
+ CFormLabel,
+ CFormTextarea,
+ CFormCheck,
+ CInputGroup,
+ CInputGroupText,
+ CAlert,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilPlus, cilPencil, cilTrash } from '@coreui/icons'
+import { ticketApi } from 'src/services/api'
+
+const TicketTypes = () => {
+ const [ticketTypes, setTicketTypes] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [showModal, setShowModal] = useState(false)
+ const [currentType, setCurrentType] = useState({
+ name: '',
+ description: '',
+ basePrice: '',
+ isLimited: false,
+ maxPerOrder: '',
+ color: '#28a745',
+ })
+ const [isEditing, setIsEditing] = useState(false)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+
+ // Fetch ticket types on component mount
+ useEffect(() => {
+ fetchTicketTypes()
+ }, [])
+
+ const fetchTicketTypes = async () => {
+ try {
+ setLoading(true)
+ const response = await ticketApi.getTicketTypes()
+ setTicketTypes(response)
+ } catch (error) {
+ setError('Failed to load ticket types. Please try again.')
+ console.error(error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleOpenModal = (ticketType = null) => {
+ if (ticketType) {
+ setCurrentType(ticketType)
+ setIsEditing(true)
+ } else {
+ setCurrentType({
+ name: '',
+ description: '',
+ basePrice: '',
+ isLimited: false,
+ maxPerOrder: '',
+ color: '#28a745',
+ })
+ setIsEditing(false)
+ }
+ setShowModal(true)
+ }
+
+ const handleCloseModal = () => {
+ setShowModal(false)
+ setError(null)
+ setSuccess(null)
+ }
+
+ const handleInputChange = (e) => {
+ const { name, value, type, checked } = e.target
+
+ if (type === 'checkbox') {
+ setCurrentType({ ...currentType, [name]: checked })
+ } else {
+ setCurrentType({ ...currentType, [name]: value })
+ }
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ try {
+ if (isEditing) {
+ await ticketApi.updateTicketType(currentType.id, currentType)
+ setSuccess('Ticket type updated successfully!')
+ } else {
+ await ticketApi.createTicketType(currentType)
+ setSuccess('Ticket type created successfully!')
+ }
+
+ // Refresh ticket types list
+ fetchTicketTypes()
+
+ // Close modal after short delay
+ setTimeout(() => {
+ handleCloseModal()
+ }, 1500)
+ } catch (error) {
+ setError(error.message || 'Failed to save ticket type. Please try again.')
+ console.error(error)
+ }
+ }
+
+ const handleDelete = async (id) => {
+ if (window.confirm('Are you sure you want to delete this ticket type? This may affect existing tickets.')) {
+ try {
+ await ticketApi.deleteTicketType(id)
+ setTicketTypes(ticketTypes.filter(type => type.id !== id))
+ } catch (error) {
+ setError('Failed to delete ticket type. It may be in use by existing tickets.')
+ console.error(error)
+ }
+ }
+ }
+
+ return (
+
+
+
+
+ Ticket Types
+ handleOpenModal()}
+ >
+
+ Add Ticket Type
+
+
+
+ {error && {error} }
+
+ {loading ? (
+ Loading ticket types...
+ ) : (
+
+
+
+ Name
+ Description
+ Base Price
+ Max Per Order
+ Status
+ Actions
+
+
+
+ {ticketTypes.length > 0 ? (
+ ticketTypes.map(type => (
+
+
+
+ {type.name}
+
+
+
+ {type.description || 'No description'}
+
+
+ ${parseFloat(type.basePrice).toFixed(2)}
+
+
+ {type.isLimited ? type.maxPerOrder || 'Limited' : 'Unlimited'}
+
+
+ {type.isActive !== false ? (
+ Active
+ ) : (
+ Inactive
+ )}
+
+
+ handleOpenModal(type)}
+ >
+
+
+ handleDelete(type.id)}
+ >
+
+
+
+
+ ))
+ ) : (
+
+
+ No ticket types found
+
+
+ )}
+
+
+ )}
+
+
+
+
+ {/* Ticket Type Modal */}
+
+
+ {isEditing ? 'Edit Ticket Type' : 'Add New Ticket Type'}
+
+
+ {error && {error} }
+ {success && {success} }
+
+
+
+ Name
+
+
+
+
+ Description
+
+
+
+
+ Base Price
+
+ $
+
+
+
+
+
+
+
+
+ {currentType.isLimited && (
+
+ Maximum Per Order
+
+
+ )}
+
+
+
Display Color
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ {isEditing ? 'Update' : 'Create'}
+
+
+
+
+
+
+ )
+}
+
+export default TicketTypes
\ No newline at end of file
diff --git a/src/views/tickets/TicketsList.js b/src/views/tickets/TicketsList.js
new file mode 100644
index 000000000..108fd3768
--- /dev/null
+++ b/src/views/tickets/TicketsList.js
@@ -0,0 +1,328 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CDropdown,
+ CDropdownToggle,
+ CDropdownMenu,
+ CDropdownItem,
+ CBadge,
+ CFormInput,
+ CForm,
+ CFormSelect,
+ CPagination,
+ CPaginationItem,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilOptions, cilPencil, cilTrash, cilPlus, cilSearch } from '@coreui/icons'
+import { ticketApi, eventApi } from 'src/services/api'
+import { Link, useNavigate } from 'react-router-dom'
+
+const TicketsList = () => {
+ const navigate = useNavigate()
+ const [tickets, setTickets] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [events, setEvents] = useState([])
+ const [ticketTypes, setTicketTypes] = useState([])
+ const [page, setPage] = useState(1)
+ const [totalPages, setTotalPages] = useState(1)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [filterEvent, setFilterEvent] = useState('')
+ const [filterType, setFilterType] = useState('')
+ const [filterStatus, setFilterStatus] = useState('')
+
+ // Fetch tickets on component mount and when filters change
+ useEffect(() => {
+ const fetchTickets = async () => {
+ try {
+ setLoading(true)
+ const filters = {}
+ if (searchTerm) filters.search = searchTerm
+ if (filterEvent) filters.eventId = filterEvent
+ if (filterType) filters.typeId = filterType
+ if (filterStatus) filters.status = filterStatus
+
+ const response = await ticketApi.getTickets(page, 10, filters)
+ setTickets(response.data)
+ setTotalPages(response.totalPages || 1)
+ } catch (error) {
+ console.error('Failed to fetch tickets:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchTickets()
+ }, [page, searchTerm, filterEvent, filterType, filterStatus])
+
+ // Fetch events and ticket types for filter dropdowns
+ useEffect(() => {
+ const fetchFilterData = async () => {
+ try {
+ // Get events for dropdown
+ const eventsResponse = await eventApi.getEvents(1, 100, { status: 'published' })
+ setEvents(eventsResponse.data || [])
+
+ // Get ticket types for dropdown
+ const typesResponse = await ticketApi.getTicketTypes()
+ setTicketTypes(typesResponse || [])
+ } catch (error) {
+ console.error('Failed to fetch filter data:', error)
+ }
+ }
+
+ fetchFilterData()
+ }, [])
+
+ const handleSearch = (e) => {
+ e.preventDefault()
+ setPage(1) // Reset to first page on new search
+ }
+
+ const handleDelete = async (id) => {
+ if (window.confirm('Are you sure you want to delete this ticket?')) {
+ try {
+ await ticketApi.deleteTicket(id)
+ // Refresh tickets after deletion
+ setTickets(tickets.filter(ticket => ticket.id !== id))
+ } catch (error) {
+ console.error('Failed to delete ticket:', error)
+ }
+ }
+ }
+
+ const getStatusBadge = (status) => {
+ const statusMap = {
+ 'available': { color: 'success', label: 'Available' },
+ 'reserved': { color: 'warning', label: 'Reserved' },
+ 'sold': { color: 'info', label: 'Sold' },
+ 'cancelled': { color: 'danger', label: 'Cancelled' },
+ 'checked-in': { color: 'primary', label: 'Checked In' },
+ }
+
+ const statusInfo = statusMap[status] || { color: 'light', label: status }
+
+ return (
+
{statusInfo.label}
+ )
+ }
+
+ return (
+
+
+
+
+ Tickets
+ navigate('/tickets/create')}
+ >
+
+ Create Ticket
+
+
+
+ {/* Search and Filters */}
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+ {
+ setFilterEvent(e.target.value)
+ setPage(1)
+ }}
+ >
+ All Events
+ {events.map(event => (
+
+ {event.name}
+
+ ))}
+
+
+
+ {
+ setFilterType(e.target.value)
+ setPage(1)
+ }}
+ >
+ All Types
+ {ticketTypes.map(type => (
+
+ {type.name}
+
+ ))}
+
+
+
+ {
+ setFilterStatus(e.target.value)
+ setPage(1)
+ }}
+ >
+ All Statuses
+ Available
+ Reserved
+ Sold
+ Cancelled
+ Checked In
+
+
+
+
+
+ Search
+
+
+
+
+
+ {/* Tickets Table */}
+ {loading ? (
+ Loading tickets...
+ ) : (
+ <>
+
+
+
+ Ticket ID
+ Event
+ Type
+ Price
+ Buyer
+ Status
+ Actions
+
+
+
+ {tickets.length > 0 ? (
+ tickets.map(ticket => (
+
+
+ {ticket.ticketCode || ticket.id}
+
+
+ {ticket.event?.name || 'N/A'}
+
+
+ {ticket.ticketType?.name || 'Standard'}
+
+
+ ${parseFloat(ticket.price).toFixed(2)}
+
+
+ {ticket.buyer ? (
+
+
{ticket.buyer.name || ticket.buyer.email}
+
{ticket.purchaseDate && new Date(ticket.purchaseDate).toLocaleDateString()}
+
+ ) : (
+ 'Not sold'
+ )}
+
+
+ {getStatusBadge(ticket.status)}
+
+
+
+
+
+
+
+
+ View Details
+
+
+
+ Edit
+
+ handleDelete(ticket.id)}
+ style={{ color: 'red' }}
+ >
+
+ Delete
+
+
+
+
+
+ ))
+ ) : (
+
+
+ No tickets found
+
+
+ )}
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+ setPage(page - 1)}
+ >
+ Previous
+
+
+ {[...Array(totalPages).keys()].map(number => (
+ setPage(number + 1)}
+ >
+ {number + 1}
+
+ ))}
+
+ setPage(page + 1)}
+ >
+ Next
+
+
+ )}
+ >
+ )}
+
+
+
+
+ )
+}
+
+export default TicketsList
\ No newline at end of file
diff --git a/src/views/users/UserPermissions.js b/src/views/users/UserPermissions.js
new file mode 100644
index 000000000..02766718d
--- /dev/null
+++ b/src/views/users/UserPermissions.js
@@ -0,0 +1,372 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CFormInput,
+ CForm,
+ CFormCheck,
+ CAlert,
+ CFormSelect,
+ CBadge,
+ CAvatar,
+ CSpinner,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilSave, cilSearch } from '@coreui/icons'
+import { userApi } from 'src/services/api'
+
+const defaultAvatar = 'https://via.placeholder.com/40'
+
+const UserPermissions = () => {
+ const [users, setUsers] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [selectedUser, setSelectedUser] = useState(null)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+ const [permissions, setPermissions] = useState([])
+ const [userPermissions, setUserPermissions] = useState({})
+
+ // Fetch users and permissions on component mount
+ useEffect(() => {
+ const fetchInitialData = async () => {
+ try {
+ setLoading(true)
+
+ // Fetch users
+ const usersResponse = await userApi.getUsers(1, 100)
+ setUsers(usersResponse.data || [])
+
+ // Fetch all available permissions
+ const permissionsResponse = await userApi.getPermissions()
+ setPermissions(permissionsResponse || [])
+ } catch (error) {
+ setError('Failed to load data. Please refresh the page.')
+ console.error(error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchInitialData()
+ }, [])
+
+ // Fetch specific user's permissions when a user is selected
+ useEffect(() => {
+ const fetchUserPermissions = async () => {
+ if (!selectedUser) return
+
+ try {
+ setLoading(true)
+
+ // Fetch the full user data including permissions
+ const userData = await userApi.getUser(selectedUser.id)
+
+ // Transform permissions to a more usable format
+ const permissionsMap = {}
+ if (userData.permissions) {
+ userData.permissions.forEach(perm => {
+ permissionsMap[perm.id] = true
+ })
+ }
+
+ setUserPermissions(permissionsMap)
+ } catch (error) {
+ setError('Failed to load user permissions.')
+ console.error(error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchUserPermissions()
+ }, [selectedUser])
+
+ const handleSearch = (e) => {
+ e.preventDefault()
+ // Filter users based on search term
+ if (!searchTerm.trim()) return
+
+ const filteredUsers = users.filter(user =>
+ `${user.firstName} ${user.lastName}`.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ user.email.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+
+ if (filteredUsers.length > 0) {
+ setSelectedUser(filteredUsers[0])
+ } else {
+ setError('No users found matching your search.')
+ }
+ }
+
+ const handleUserChange = (e) => {
+ const userId = e.target.value
+ if (!userId) {
+ setSelectedUser(null)
+ return
+ }
+
+ const user = users.find(u => u.id === userId)
+ setSelectedUser(user || null)
+ }
+
+ const handlePermissionChange = (permissionId, checked) => {
+ setUserPermissions(prev => ({
+ ...prev,
+ [permissionId]: checked
+ }))
+ }
+
+ const handleRoleChange = (role) => {
+ if (!selectedUser) return
+
+ // Reset permissions
+ const defaultPermissions = {}
+
+ // Set default permissions based on role
+ permissions.forEach(permission => {
+ // Admin gets all permissions
+ if (role === 'admin') {
+ defaultPermissions[permission.id] = true
+ }
+ // Staff gets most permissions except sensitive ones
+ else if (role === 'staff') {
+ defaultPermissions[permission.id] = !permission.isAdminOnly
+ }
+ // Organizer gets event-related permissions
+ else if (role === 'organizer') {
+ defaultPermissions[permission.id] = permission.isEventRelated
+ }
+ // Regular users get minimal permissions
+ else {
+ defaultPermissions[permission.id] = permission.isBasic
+ }
+ })
+
+ setUserPermissions(defaultPermissions)
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ if (!selectedUser) {
+ setError('Please select a user first')
+ return
+ }
+
+ try {
+ setSaving(true)
+ setError(null)
+
+ // Transform permissions back to array format for API
+ const permissionsToSave = Object.entries(userPermissions)
+ .filter(([_, isEnabled]) => isEnabled)
+ .map(([permId, _]) => permId)
+
+ await userApi.updatePermission(selectedUser.id, { permissions: permissionsToSave })
+
+ setSuccess('User permissions updated successfully!')
+
+ // Clear success message after delay
+ setTimeout(() => {
+ setSuccess(null)
+ }, 3000)
+ } catch (error) {
+ setError(error.message || 'Failed to update permissions. Please try again.')
+ console.error(error)
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const getRoleBadge = (role) => {
+ const roleMap = {
+ 'admin': { color: 'danger', label: 'Admin' },
+ 'staff': { color: 'warning', label: 'Staff' },
+ 'organizer': { color: 'info', label: 'Organizer' },
+ 'user': { color: 'success', label: 'User' },
+ }
+
+ const roleInfo = roleMap[role] || { color: 'light', label: role }
+
+ return (
+
{roleInfo.label}
+ )
+ }
+
+ // Group permissions by category for better organization
+ const groupedPermissions = permissions.reduce((acc, permission) => {
+ const category = permission.category || 'General'
+ if (!acc[category]) {
+ acc[category] = []
+ }
+ acc[category].push(permission)
+ return acc
+ }, {})
+
+ return (
+
+
+
+
+ User Permissions
+
+
+ {error && {error} }
+ {success && {success} }
+
+ {/* User Selection */}
+
+
+
+ setSearchTerm(e.target.value)}
+ className="me-2"
+ />
+
+
+
+
+
+
+
+ Select User
+ {users.map(user => (
+
+ {user.firstName} {user.lastName} ({user.email})
+
+ ))}
+
+
+
+
+ {/* User Details */}
+ {selectedUser && (
+
+
+
+
+
+
+
{selectedUser.firstName} {selectedUser.lastName}
+
Email: {selectedUser.email}
+
Current Role: {getRoleBadge(selectedUser.role)}
+
+
+
+
+
+ )}
+
+ {/* Role Template Selection */}
+ {selectedUser && (
+
+
+
+
Apply Role Template
+
This will preset permissions based on the selected role.
+
+ handleRoleChange('admin')}>Admin
+ handleRoleChange('staff')}>Staff
+ handleRoleChange('organizer')}>Organizer
+ handleRoleChange('user')}>Basic User
+
+
+
+
+ )}
+
+ {/* Permissions Table */}
+ {selectedUser && !loading ? (
+
+ {Object.entries(groupedPermissions).map(([category, categoryPermissions]) => (
+
+
{category}
+
+
+
+ Enable
+ Permission
+ Description
+
+
+
+ {categoryPermissions.map(permission => (
+
+
+ handlePermissionChange(permission.id, e.target.checked)}
+ />
+
+
+
+ {permission.name}
+
+
+
+ {permission.description}
+
+
+ ))}
+
+
+
+ ))}
+
+
+
+ {saving ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ <>
+
+ Save Permissions
+ >
+ )}
+
+
+
+ ) : selectedUser && loading ? (
+
+
+
Loading user permissions...
+
+ ) : (
+
+
Select a user to manage permissions
+
+ )}
+
+
+
+
+ )
+}
+
+export default UserPermissions
\ No newline at end of file
diff --git a/src/views/users/UsersList.js b/src/views/users/UsersList.js
new file mode 100644
index 000000000..efbd58f13
--- /dev/null
+++ b/src/views/users/UsersList.js
@@ -0,0 +1,295 @@
+import React, { useState, useEffect } from 'react'
+import {
+ CCard,
+ CCardBody,
+ CCardHeader,
+ CCol,
+ CRow,
+ CTable,
+ CTableHead,
+ CTableRow,
+ CTableHeaderCell,
+ CTableBody,
+ CTableDataCell,
+ CButton,
+ CDropdown,
+ CDropdownToggle,
+ CDropdownMenu,
+ CDropdownItem,
+ CBadge,
+ CFormInput,
+ CForm,
+ CFormSelect,
+ CPagination,
+ CPaginationItem,
+ CAvatar,
+} from '@coreui/react'
+import CIcon from '@coreui/icons-react'
+import { cilOptions, cilPencil, cilTrash, cilPlus, cilSearch } from '@coreui/icons'
+import { userApi } from 'src/services/api'
+import { Link, useNavigate } from 'react-router-dom'
+
+// Default avatar placeholder
+const defaultAvatar = 'https://via.placeholder.com/40'
+
+const UsersList = () => {
+ const navigate = useNavigate()
+ const [users, setUsers] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [page, setPage] = useState(1)
+ const [totalPages, setTotalPages] = useState(1)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [filterRole, setFilterRole] = useState('')
+ const [filterStatus, setFilterStatus] = useState('')
+
+ // Fetch users on component mount and when filters change
+ useEffect(() => {
+ const fetchUsers = async () => {
+ try {
+ setLoading(true)
+ const filters = {}
+ if (searchTerm) filters.search = searchTerm
+ if (filterRole) filters.role = filterRole
+ if (filterStatus) filters.status = filterStatus
+
+ const response = await userApi.getUsers(page, 10, filters)
+ setUsers(response.data)
+ setTotalPages(response.totalPages || 1)
+ } catch (error) {
+ console.error('Failed to fetch users:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchUsers()
+ }, [page, searchTerm, filterRole, filterStatus])
+
+ const handleSearch = (e) => {
+ e.preventDefault()
+ setPage(1) // Reset to first page on new search
+ }
+
+ const handleDelete = async (id) => {
+ if (window.confirm('Are you sure you want to delete this user?')) {
+ try {
+ await userApi.deleteUser(id)
+ // Refresh users after deletion
+ setUsers(users.filter(user => user.id !== id))
+ } catch (error) {
+ console.error('Failed to delete user:', error)
+ }
+ }
+ }
+
+ const getRoleBadge = (role) => {
+ const roleMap = {
+ 'admin': { color: 'danger', label: 'Admin' },
+ 'staff': { color: 'warning', label: 'Staff' },
+ 'organizer': { color: 'info', label: 'Organizer' },
+ 'user': { color: 'success', label: 'User' },
+ }
+
+ const roleInfo = roleMap[role] || { color: 'light', label: role }
+
+ return (
+
{roleInfo.label}
+ )
+ }
+
+ const getStatusBadge = (isActive) => {
+ return isActive ?
+
Active :
+
Inactive
+ }
+
+ return (
+
+
+
+
+ Users
+ navigate('/users/create')}
+ >
+
+ Add User
+
+
+
+ {/* Search and Filters */}
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+ {
+ setFilterRole(e.target.value)
+ setPage(1)
+ }}
+ >
+ All Roles
+ Admin
+ Staff
+ Organizer
+ User
+
+
+
+ {
+ setFilterStatus(e.target.value)
+ setPage(1)
+ }}
+ >
+ All Status
+ Active
+ Inactive
+
+
+
+
+
+ Search
+
+
+
+
+
+ {/* Users Table */}
+ {loading ? (
+ Loading users...
+ ) : (
+ <>
+
+
+
+ User
+ Email
+ Phone
+ Role
+ Status
+ Registered
+ Actions
+
+
+
+ {users.length > 0 ? (
+ users.map(user => (
+
+
+
+
+
+
{user.firstName} {user.lastName}
+
ID: {user.id}
+
+
+
+ {user.email}
+ {user.phone || 'N/A'}
+
+ {getRoleBadge(user.role)}
+
+
+ {getStatusBadge(user.isActive)}
+
+
+ {user.registeredDate && new Date(user.registeredDate).toLocaleDateString()}
+
+
+
+
+
+
+
+
+ View Profile
+
+
+
+ Edit
+
+
+ Manage Permissions
+
+ handleDelete(user.id)}
+ style={{ color: 'red' }}
+ >
+
+ Delete
+
+
+
+
+
+ ))
+ ) : (
+
+
+ No users found
+
+
+ )}
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+ setPage(page - 1)}
+ >
+ Previous
+
+
+ {[...Array(totalPages).keys()].map(number => (
+ setPage(number + 1)}
+ >
+ {number + 1}
+
+ ))}
+
+ setPage(page + 1)}
+ >
+ Next
+
+
+ )}
+ >
+ )}
+
+
+
+
+ )
+}
+
+export default UsersList
\ No newline at end of file
diff --git a/src/views/widgets/WidgetsBrand.js b/src/views/widgets/WidgetsBrand.js
deleted file mode 100644
index 03eea83ef..000000000
--- a/src/views/widgets/WidgetsBrand.js
+++ /dev/null
@@ -1,182 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import { CWidgetStatsD, CRow, CCol } from '@coreui/react'
-import CIcon from '@coreui/icons-react'
-import { cibFacebook, cibLinkedin, cibTwitter, cilCalendar } from '@coreui/icons'
-import { CChart } from '@coreui/react-chartjs'
-
-const WidgetsBrand = (props) => {
- const chartOptions = {
- elements: {
- line: {
- tension: 0.4,
- },
- point: {
- radius: 0,
- hitRadius: 10,
- hoverRadius: 4,
- hoverBorderWidth: 3,
- },
- },
- maintainAspectRatio: false,
- plugins: {
- legend: {
- display: false,
- },
- },
- scales: {
- x: {
- display: false,
- },
- y: {
- display: false,
- },
- },
- }
-
- return (
-
-
-
- ),
- })}
- icon={ }
- values={[
- { title: 'friends', value: '89K' },
- { title: 'feeds', value: '459' },
- ]}
- style={{
- '--cui-card-cap-bg': '#3b5998',
- }}
- />
-
-
-
- ),
- })}
- icon={ }
- values={[
- { title: 'followers', value: '973k' },
- { title: 'tweets', value: '1.792' },
- ]}
- style={{
- '--cui-card-cap-bg': '#00aced',
- }}
- />
-
-
-
- ),
- })}
- icon={ }
- values={[
- { title: 'contacts', value: '500' },
- { title: 'feeds', value: '1.292' },
- ]}
- style={{
- '--cui-card-cap-bg': '#4875b4',
- }}
- />
-
-
-
- ),
- })}
- icon={ }
- values={[
- { title: 'events', value: '12+' },
- { title: 'meetings', value: '4' },
- ]}
- />
-
-
- )
-}
-
-WidgetsBrand.propTypes = {
- className: PropTypes.string,
- withCharts: PropTypes.bool,
-}
-
-export default WidgetsBrand
diff --git a/src/views/widgets/WidgetsDropdown.js b/src/views/widgets/WidgetsDropdown.js
deleted file mode 100644
index 85e2fc969..000000000
--- a/src/views/widgets/WidgetsDropdown.js
+++ /dev/null
@@ -1,396 +0,0 @@
-import React, { useEffect, useRef } from 'react'
-import PropTypes from 'prop-types'
-
-import {
- CRow,
- CCol,
- CDropdown,
- CDropdownMenu,
- CDropdownItem,
- CDropdownToggle,
- CWidgetStatsA,
-} from '@coreui/react'
-import { getStyle } from '@coreui/utils'
-import { CChartBar, CChartLine } from '@coreui/react-chartjs'
-import CIcon from '@coreui/icons-react'
-import { cilArrowBottom, cilArrowTop, cilOptions } from '@coreui/icons'
-
-const WidgetsDropdown = (props) => {
- const widgetChartRef1 = useRef(null)
- const widgetChartRef2 = useRef(null)
-
- useEffect(() => {
- document.documentElement.addEventListener('ColorSchemeChange', () => {
- if (widgetChartRef1.current) {
- setTimeout(() => {
- widgetChartRef1.current.data.datasets[0].pointBackgroundColor = getStyle('--cui-primary')
- widgetChartRef1.current.update()
- })
- }
-
- if (widgetChartRef2.current) {
- setTimeout(() => {
- widgetChartRef2.current.data.datasets[0].pointBackgroundColor = getStyle('--cui-info')
- widgetChartRef2.current.update()
- })
- }
- })
- }, [widgetChartRef1, widgetChartRef2])
-
- return (
-
-
-
- 26K{' '}
-
- (-12.4% )
-
- >
- }
- title="Users"
- action={
-
-
-
-
-
- Action
- Another action
- Something else here...
- Disabled action
-
-
- }
- chart={
-
- }
- />
-
-
-
- $6.200{' '}
-
- (40.9% )
-
- >
- }
- title="Income"
- action={
-
-
-
-
-
- Action
- Another action
- Something else here...
- Disabled action
-
-
- }
- chart={
-
- }
- />
-
-
-
- 2.49%{' '}
-
- (84.7% )
-
- >
- }
- title="Conversion Rate"
- action={
-
-
-
-
-
- Action
- Another action
- Something else here...
- Disabled action
-
-
- }
- chart={
-
- }
- />
-
-
-
- 44K{' '}
-
- (-23.6% )
-
- >
- }
- title="Sessions"
- action={
-
-
-
-
-
- Action
- Another action
- Something else here...
- Disabled action
-
-
- }
- chart={
-
- }
- />
-
-
- )
-}
-
-WidgetsDropdown.propTypes = {
- className: PropTypes.string,
- withCharts: PropTypes.bool,
-}
-
-export default WidgetsDropdown