From 4b55cb0e360da1e52fe764527d080f568517d296 Mon Sep 17 00:00:00 2001 From: hungvt Date: Wed, 21 May 2025 08:54:45 +0700 Subject: [PATCH 1/2] [feat] : add business logic --- Docs/admin-api-requirements.docx | Bin 0 -> 12072 bytes package.json | 7 +- src/App.js | 10 +- src/_nav.js | 369 ++-------- src/components/AppContent.js | 1 - src/routes.js | 129 ++-- src/services/api.js | 251 +++++++ src/views/admins/AdminForm.js | 306 ++++++++ src/views/admins/AdminRoles.js | 370 ++++++++++ src/views/admins/AdminsList.js | 337 +++++++++ src/views/dashboard/Dashboard.js | 682 +++++++++--------- src/views/dashboard/MainChart.js | 133 ---- src/views/events/EventCategories.js | 269 +++++++ src/views/events/EventForm.js | 425 +++++++++++ src/views/events/EventReport.js | 378 ++++++++++ src/views/events/EventStatistics.js | 660 +++++++++++++++++ src/views/events/EventsList.js | 292 ++++++++ src/views/gift-baskets/GiftBasketForm.js | 456 ++++++++++++ .../gift-baskets/GiftBasketProbabilities.js | 273 +++++++ src/views/gift-baskets/GiftBasketsList.js | 350 +++++++++ src/views/gift-boxes/GiftBoxForm.js | 295 ++++++++ src/views/gift-boxes/GiftBoxProbabilities.js | 405 +++++++++++ .../gift-boxes/GiftBoxProbabilities.js.bak | 479 ++++++++++++ src/views/gift-boxes/GiftBoxesList.js | 294 ++++++++ src/views/tickets/TicketTypes.js | 340 +++++++++ src/views/tickets/TicketsList.js | 328 +++++++++ src/views/users/UserPermissions.js | 372 ++++++++++ src/views/users/UsersList.js | 295 ++++++++ src/views/widgets/WidgetsBrand.js | 182 ----- src/views/widgets/WidgetsDropdown.js | 396 ---------- 30 files changed, 7611 insertions(+), 1473 deletions(-) create mode 100644 Docs/admin-api-requirements.docx create mode 100644 src/services/api.js create mode 100644 src/views/admins/AdminForm.js create mode 100644 src/views/admins/AdminRoles.js create mode 100644 src/views/admins/AdminsList.js delete mode 100644 src/views/dashboard/MainChart.js create mode 100644 src/views/events/EventCategories.js create mode 100644 src/views/events/EventForm.js create mode 100644 src/views/events/EventReport.js create mode 100644 src/views/events/EventStatistics.js create mode 100644 src/views/events/EventsList.js create mode 100644 src/views/gift-baskets/GiftBasketForm.js create mode 100644 src/views/gift-baskets/GiftBasketProbabilities.js create mode 100644 src/views/gift-baskets/GiftBasketsList.js create mode 100644 src/views/gift-boxes/GiftBoxForm.js create mode 100644 src/views/gift-boxes/GiftBoxProbabilities.js create mode 100644 src/views/gift-boxes/GiftBoxProbabilities.js.bak create mode 100644 src/views/gift-boxes/GiftBoxesList.js create mode 100644 src/views/tickets/TicketTypes.js create mode 100644 src/views/tickets/TicketsList.js create mode 100644 src/views/users/UserPermissions.js create mode 100644 src/views/users/UsersList.js delete mode 100644 src/views/widgets/WidgetsBrand.js delete mode 100644 src/views/widgets/WidgetsDropdown.js diff --git a/Docs/admin-api-requirements.docx b/Docs/admin-api-requirements.docx new file mode 100644 index 0000000000000000000000000000000000000000..7b0843942959c574f86fd14a695a3fb73ce02914 GIT binary patch literal 12072 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 U%&*mj{(L-*J;!>8(*K0~ALsXoX#fBK literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 7e992e447..58c09d504 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,11 @@ "classnames": "^2.5.1", "core-js": "^3.40.0", "prop-types": "^15.8.1", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^18.2.0", + "react-csv": "^2.2.2", + "react-dom": "^18.2.0", "react-redux": "^9.2.0", - "react-router-dom": "^7.1.5", + "react-router-dom": "^6.15.0", "redux": "5.0.1", "simplebar-react": "^3.3.0" }, diff --git a/src/App.js b/src/App.js index f5b22393e..eb5388405 100644 --- a/src/App.js +++ b/src/App.js @@ -45,11 +45,11 @@ const App = () => { } > - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> diff --git a/src/_nav.js b/src/_nav.js index 9f8ca150b..2dfbd3856 100644 --- a/src/_nav.js +++ b/src/_nav.js @@ -13,6 +13,12 @@ import { cilPuzzle, cilSpeedometer, cilStar, + cilCalendar, + cilBookmark, + cilUser, + cilSettings, + cilGift, + cilChart, } from '@coreui/icons' import { CNavGroup, CNavItem, CNavTitle } from '@coreui/react' @@ -29,386 +35,93 @@ const _nav = [ }, { component: CNavTitle, - name: 'Theme', - }, - { - component: CNavItem, - name: 'Colors', - to: '/theme/colors', - icon: , - }, - { - component: CNavItem, - name: 'Typography', - to: '/theme/typography', - icon: , - }, - { - component: CNavTitle, - name: 'Components', + name: 'Admin Management', }, { component: CNavGroup, - name: 'Base', - to: '/base', - icon: , + name: 'Admins', + to: '/admins', + icon: , items: [ { component: CNavItem, - name: 'Accordion', - to: '/base/accordion', - }, - { - component: CNavItem, - name: 'Breadcrumb', - to: '/base/breadcrumbs', - }, - { - component: CNavItem, - name: ( - - {'Calendar'} - - - ), - href: 'https://coreui.io/react/docs/components/calendar/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Cards', - to: '/base/cards', - }, - { - component: CNavItem, - name: 'Carousel', - to: '/base/carousels', - }, - { - component: CNavItem, - name: 'Collapse', - to: '/base/collapses', - }, - { - component: CNavItem, - name: 'List group', - to: '/base/list-groups', - }, - { - component: CNavItem, - name: 'Navs & Tabs', - to: '/base/navs', - }, - { - component: CNavItem, - name: 'Pagination', - to: '/base/paginations', - }, - { - component: CNavItem, - name: 'Placeholders', - to: '/base/placeholders', - }, - { - component: CNavItem, - name: 'Popovers', - to: '/base/popovers', - }, - { - component: CNavItem, - name: 'Progress', - to: '/base/progress', - }, - { - component: CNavItem, - name: 'Smart Pagination', - href: 'https://coreui.io/react/docs/components/smart-pagination/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: ( - - {'Smart Table'} - - - ), - href: 'https://coreui.io/react/docs/components/smart-table/', - badge: { - color: 'danger', - text: 'PRO', - }, + name: 'All Admins', + to: '/admins/list', }, { component: CNavItem, - name: 'Spinners', - to: '/base/spinners', + name: 'Create Admin', + to: '/admins/create', }, { component: CNavItem, - name: 'Tables', - to: '/base/tables', - }, - { - component: CNavItem, - name: 'Tabs', - to: '/base/tabs', - }, - { - component: CNavItem, - name: 'Tooltips', - to: '/base/tooltips', - }, - { - component: CNavItem, - name: ( - - {'Virtual Scroller'} - - - ), - href: 'https://coreui.io/react/docs/components/virtual-scroller/', - badge: { - color: 'danger', - text: 'PRO', - }, + name: 'Admin Roles', + to: '/admins/roles', }, ], }, { - component: CNavGroup, - name: 'Buttons', - to: '/buttons', - icon: , - items: [ - { - component: CNavItem, - name: 'Buttons', - to: '/buttons/buttons', - }, - { - component: CNavItem, - name: 'Buttons groups', - to: '/buttons/button-groups', - }, - { - component: CNavItem, - name: 'Dropdowns', - to: '/buttons/dropdowns', - }, - { - component: CNavItem, - name: ( - - {'Loading Button'} - - - ), - href: 'https://coreui.io/react/docs/components/loading-button/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - ], + component: CNavTitle, + name: 'Event Management', }, { component: CNavGroup, - name: 'Forms', - icon: , + name: 'Events', + to: '/events', + icon: , items: [ { component: CNavItem, - name: 'Form Control', - to: '/forms/form-control', - }, - { - component: CNavItem, - name: 'Select', - to: '/forms/select', - }, - { - component: CNavItem, - name: ( - - {'Multi Select'} - - - ), - href: 'https://coreui.io/react/docs/forms/multi-select/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Checks & Radios', - to: '/forms/checks-radios', - }, - { - component: CNavItem, - name: 'Range', - to: '/forms/range', - }, - { - component: CNavItem, - name: ( - - {'Range Slider'} - - - ), - href: 'https://coreui.io/react/docs/forms/range-slider/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: ( - - {'Rating'} - - - ), - href: 'https://coreui.io/react/docs/forms/rating/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Input Group', - to: '/forms/input-group', - }, - { - component: CNavItem, - name: 'Floating Labels', - to: '/forms/floating-labels', - }, - { - component: CNavItem, - name: ( - - {'Date Picker'} - - - ), - href: 'https://coreui.io/react/docs/forms/date-picker/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Date Range Picker', - href: 'https://coreui.io/react/docs/forms/date-range-picker/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: ( - - {'Time Picker'} - - - ), - href: 'https://coreui.io/react/docs/forms/time-picker/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Layout', - to: '/forms/layout', + name: 'Statistics', + to: '/events/statistics', }, { component: CNavItem, - name: 'Validation', - to: '/forms/validation', + name: 'Download Report', + to: '/events/report', }, ], }, { - component: CNavItem, - name: 'Charts', - to: '/charts', - icon: , + component: CNavTitle, + name: 'Gift Box Probability Management', }, { component: CNavGroup, - name: 'Icons', - icon: , + name: 'Gift Boxes', + to: '/gift-boxes', + icon: , items: [ { component: CNavItem, - name: 'CoreUI Free', - to: '/icons/coreui-icons', + name: 'All Boxes', + to: '/gift-boxes/list', }, { component: CNavItem, - name: 'CoreUI Flags', - to: '/icons/flags', - }, - { - component: CNavItem, - name: 'CoreUI Brands', - to: '/icons/brands', + name: 'Probabilities', + to: '/gift-boxes/probabilities', }, ], }, { component: CNavGroup, - name: 'Notifications', - icon: , + name: 'Users', + to: '/users', + icon: , items: [ { component: CNavItem, - name: 'Alerts', - to: '/notifications/alerts', - }, - { - component: CNavItem, - name: 'Badges', - to: '/notifications/badges', - }, - { - component: CNavItem, - name: 'Modal', - to: '/notifications/modals', + name: 'All Users', + to: '/users/list', }, { component: CNavItem, - name: 'Toasts', - to: '/notifications/toasts', + name: 'Permissions', + to: '/users/permissions', }, ], }, - { - component: CNavItem, - name: 'Widgets', - to: '/widgets', - icon: , - badge: { - color: 'info', - text: 'NEW', - }, - }, { component: CNavTitle, name: 'Extras', diff --git a/src/components/AppContent.js b/src/components/AppContent.js index b9a39ef50..0be4b79f6 100644 --- a/src/components/AppContent.js +++ b/src/components/AppContent.js @@ -17,7 +17,6 @@ const AppContent = () => { key={idx} path={route.path} exact={route.exact} - name={route.name} element={} /> ) diff --git a/src/routes.js b/src/routes.js index d2e9d6479..9c3160c18 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,102 +1,55 @@ import React from 'react' const Dashboard = React.lazy(() => import('./views/dashboard/Dashboard')) -const Colors = React.lazy(() => import('./views/theme/colors/Colors')) -const Typography = React.lazy(() => import('./views/theme/typography/Typography')) -// Base -const Accordion = React.lazy(() => import('./views/base/accordion/Accordion')) -const Breadcrumbs = React.lazy(() => import('./views/base/breadcrumbs/Breadcrumbs')) -const Cards = React.lazy(() => import('./views/base/cards/Cards')) -const Carousels = React.lazy(() => import('./views/base/carousels/Carousels')) -const Collapses = React.lazy(() => import('./views/base/collapses/Collapses')) -const ListGroups = React.lazy(() => import('./views/base/list-groups/ListGroups')) -const Navs = React.lazy(() => import('./views/base/navs/Navs')) -const Paginations = React.lazy(() => import('./views/base/paginations/Paginations')) -const Placeholders = React.lazy(() => import('./views/base/placeholders/Placeholders')) -const Popovers = React.lazy(() => import('./views/base/popovers/Popovers')) -const Progress = React.lazy(() => import('./views/base/progress/Progress')) -const Spinners = React.lazy(() => import('./views/base/spinners/Spinners')) -const Tabs = React.lazy(() => import('./views/base/tabs/Tabs')) -const Tables = React.lazy(() => import('./views/base/tables/Tables')) -const Tooltips = React.lazy(() => import('./views/base/tooltips/Tooltips')) +// Admin Management +const AdminsList = React.lazy(() => import('./views/admins/AdminsList')) +const AdminForm = React.lazy(() => import('./views/admins/AdminForm')) +const AdminRoles = React.lazy(() => import('./views/admins/AdminRoles')) -// Buttons -const Buttons = React.lazy(() => import('./views/buttons/buttons/Buttons')) -const ButtonGroups = React.lazy(() => import('./views/buttons/button-groups/ButtonGroups')) -const Dropdowns = React.lazy(() => import('./views/buttons/dropdowns/Dropdowns')) +// Event Management +const EventsList = React.lazy(() => import('./views/events/EventsList')) +const EventForm = React.lazy(() => import('./views/events/EventForm')) +const EventCategories = React.lazy(() => import('./views/events/EventCategories')) +const EventStatistics = React.lazy(() => import('./views/events/EventStatistics')) +const EventReport = React.lazy(() => import('./views/events/EventReport')) -//Forms -const ChecksRadios = React.lazy(() => import('./views/forms/checks-radios/ChecksRadios')) -const FloatingLabels = React.lazy(() => import('./views/forms/floating-labels/FloatingLabels')) -const FormControl = React.lazy(() => import('./views/forms/form-control/FormControl')) -const InputGroup = React.lazy(() => import('./views/forms/input-group/InputGroup')) -const Layout = React.lazy(() => import('./views/forms/layout/Layout')) -const Range = React.lazy(() => import('./views/forms/range/Range')) -const Select = React.lazy(() => import('./views/forms/select/Select')) -const Validation = React.lazy(() => import('./views/forms/validation/Validation')) +// Gift Box Management +const GiftBoxesList = React.lazy(() => import('./views/gift-boxes/GiftBoxesList')) +const GiftBoxForm = React.lazy(() => import('./views/gift-boxes/GiftBoxForm')) +const GiftBoxProbabilities = React.lazy(() => import('./views/gift-boxes/GiftBoxProbabilities')) -const Charts = React.lazy(() => import('./views/charts/Charts')) - -// Icons -const CoreUIIcons = React.lazy(() => import('./views/icons/coreui-icons/CoreUIIcons')) -const Flags = React.lazy(() => import('./views/icons/flags/Flags')) -const Brands = React.lazy(() => import('./views/icons/brands/Brands')) - -// Notifications -const Alerts = React.lazy(() => import('./views/notifications/alerts/Alerts')) -const Badges = React.lazy(() => import('./views/notifications/badges/Badges')) -const Modals = React.lazy(() => import('./views/notifications/modals/Modals')) -const Toasts = React.lazy(() => import('./views/notifications/toasts/Toasts')) - -const Widgets = React.lazy(() => import('./views/widgets/Widgets')) +// User Management +const UsersList = React.lazy(() => import('./views/users/UsersList')) +const UserPermissions = React.lazy(() => import('./views/users/UserPermissions')) const routes = [ { path: '/', exact: true, name: 'Home' }, { path: '/dashboard', name: 'Dashboard', element: Dashboard }, - { path: '/theme', name: 'Theme', element: Colors, exact: true }, - { path: '/theme/colors', name: 'Colors', element: Colors }, - { path: '/theme/typography', name: 'Typography', element: Typography }, - { path: '/base', name: 'Base', element: Cards, exact: true }, - { path: '/base/accordion', name: 'Accordion', element: Accordion }, - { path: '/base/breadcrumbs', name: 'Breadcrumbs', element: Breadcrumbs }, - { path: '/base/cards', name: 'Cards', element: Cards }, - { path: '/base/carousels', name: 'Carousel', element: Carousels }, - { path: '/base/collapses', name: 'Collapse', element: Collapses }, - { path: '/base/list-groups', name: 'List Groups', element: ListGroups }, - { path: '/base/navs', name: 'Navs', element: Navs }, - { path: '/base/paginations', name: 'Paginations', element: Paginations }, - { path: '/base/placeholders', name: 'Placeholders', element: Placeholders }, - { path: '/base/popovers', name: 'Popovers', element: Popovers }, - { path: '/base/progress', name: 'Progress', element: Progress }, - { path: '/base/spinners', name: 'Spinners', element: Spinners }, - { path: '/base/tabs', name: 'Tabs', element: Tabs }, - { path: '/base/tables', name: 'Tables', element: Tables }, - { path: '/base/tooltips', name: 'Tooltips', element: Tooltips }, - { path: '/buttons', name: 'Buttons', element: Buttons, exact: true }, - { path: '/buttons/buttons', name: 'Buttons', element: Buttons }, - { path: '/buttons/dropdowns', name: 'Dropdowns', element: Dropdowns }, - { path: '/buttons/button-groups', name: 'Button Groups', element: ButtonGroups }, - { path: '/charts', name: 'Charts', element: Charts }, - { path: '/forms', name: 'Forms', element: FormControl, exact: true }, - { path: '/forms/form-control', name: 'Form Control', element: FormControl }, - { path: '/forms/select', name: 'Select', element: Select }, - { path: '/forms/checks-radios', name: 'Checks & Radios', element: ChecksRadios }, - { path: '/forms/range', name: 'Range', element: Range }, - { path: '/forms/input-group', name: 'Input Group', element: InputGroup }, - { path: '/forms/floating-labels', name: 'Floating Labels', element: FloatingLabels }, - { path: '/forms/layout', name: 'Layout', element: Layout }, - { path: '/forms/validation', name: 'Validation', element: Validation }, - { path: '/icons', exact: true, name: 'Icons', element: CoreUIIcons }, - { path: '/icons/coreui-icons', name: 'CoreUI Icons', element: CoreUIIcons }, - { path: '/icons/flags', name: 'Flags', element: Flags }, - { path: '/icons/brands', name: 'Brands', element: Brands }, - { path: '/notifications', name: 'Notifications', element: Alerts, exact: true }, - { path: '/notifications/alerts', name: 'Alerts', element: Alerts }, - { path: '/notifications/badges', name: 'Badges', element: Badges }, - { path: '/notifications/modals', name: 'Modals', element: Modals }, - { path: '/notifications/toasts', name: 'Toasts', element: Toasts }, - { path: '/widgets', name: 'Widgets', element: Widgets }, + + // Admin Management + { path: '/admins', name: 'Admins', element: AdminsList, exact: true }, + { path: '/admins/list', name: 'Admins List', element: AdminsList }, + { path: '/admins/create', name: 'Create Admin', element: AdminForm }, + { path: '/admins/edit/:id', name: 'Edit Admin', element: AdminForm }, + { path: '/admins/roles', name: 'Admin Roles', element: AdminRoles }, + + // Event Management + { path: '/events', name: 'Events', element: EventsList, exact: true }, + { path: '/events/statistics', name: 'Event Statistics', element: EventStatistics }, + { path: '/events/report', name: 'Event Report', element: EventReport }, + + // Gift Box Management + { path: '/gift-boxes', name: 'Gift Boxes', element: GiftBoxesList, exact: true }, + { path: '/gift-boxes/list', name: 'Gift Boxes List', element: GiftBoxesList }, + { path: '/gift-boxes/edit/:boxType/:scenario/:index', name: 'Edit Gift Box Item', element: GiftBoxForm }, + { path: '/gift-boxes/probabilities', name: 'Gift Box Probabilities', element: GiftBoxProbabilities }, + + // User Management + { path: '/users', name: 'Users', element: UsersList, exact: true }, + { path: '/users/list', name: 'Users List', element: UsersList }, + { path: '/users/permissions', name: 'User Permissions', element: UserPermissions }, + { path: '/users/permissions/:id', name: 'Edit User Permissions', element: UserPermissions }, ] export default routes diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 000000000..f6819ad54 --- /dev/null +++ b/src/services/api.js @@ -0,0 +1,251 @@ +// API base URL +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api'; + +// Helper function for HTTP requests +const fetchWithAuth = async (endpoint, options = {}) => { + // Get the token from localStorage + const token = localStorage.getItem('token'); + + // Set default headers + const headers = { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + ...options.headers, + }; + + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + ...options, + headers, + }); + + // Handle unauthorized error + if (response.status === 401) { + localStorage.removeItem('token'); + window.location.href = '/login'; + throw new Error('Unauthorized'); + } + + // Parse JSON response + const data = await response.json(); + + // Handle API errors + if (!response.ok) { + throw new Error(data.message || 'API request failed'); + } + + return data; +}; + +// Event API endpoints +export const eventApi = { + // Get all events with pagination + getEvents: (page = 1, limit = 10, filters = {}) => { + const queryParams = new URLSearchParams({ + page, + limit, + ...filters, + }); + return fetchWithAuth(`/events?${queryParams}`); + }, + + // Get a single event by ID + getEvent: (id) => fetchWithAuth(`/events/${id}`), + + // Create a new event + createEvent: (eventData) => fetchWithAuth('/events', { + method: 'POST', + body: JSON.stringify(eventData), + }), + + // Update an event + updateEvent: (id, eventData) => fetchWithAuth(`/events/${id}`, { + method: 'PUT', + body: JSON.stringify(eventData), + }), + + // Delete an event + deleteEvent: (id) => fetchWithAuth(`/events/${id}`, { + method: 'DELETE', + }), + + // Get event categories + getCategories: () => fetchWithAuth('/events/categories'), + + // Create a new category + createCategory: (categoryData) => fetchWithAuth('/events/categories', { + method: 'POST', + body: JSON.stringify(categoryData), + }), + + // Update a category + updateCategory: (id, categoryData) => fetchWithAuth(`/events/categories/${id}`, { + method: 'PUT', + body: JSON.stringify(categoryData), + }), + + // Delete a category + deleteCategory: (id) => fetchWithAuth(`/events/categories/${id}`, { + method: 'DELETE', + }), + + // Get event statistics + getEventStatistics: (eventId, startDate, endDate) => { + const queryParams = new URLSearchParams(); + if (startDate) queryParams.append('startDate', startDate); + if (endDate) queryParams.append('endDate', endDate); + + return fetchWithAuth(`/events/${eventId}/statistics?${queryParams}`); + }, + + // Export event statistics as CSV + exportEventStatistics: (eventId, format = 'csv', startDate, endDate) => { + const queryParams = new URLSearchParams({ + format, + }); + if (startDate) queryParams.append('startDate', startDate); + if (endDate) queryParams.append('endDate', endDate); + + return fetchWithAuth(`/events/${eventId}/export?${queryParams}`); + }, + + // Get event participation statistics + getEventParticipationStatistics: (page = 1, searchTerm = '', filters = {}) => { + const queryParams = new URLSearchParams({ + page, + limit: 10 + }); + if (searchTerm) queryParams.append('search', searchTerm); + + // Add any other filters + for (const [key, value] of Object.entries(filters)) { + if (value) queryParams.append(key, value); + } + + return fetchWithAuth(`/events/participation/statistics?${queryParams}`); + }, +}; + +// Ticket API endpoints +export const ticketApi = { + // Get all tickets with pagination + getTickets: (page = 1, limit = 10, filters = {}) => { + const queryParams = new URLSearchParams({ + page, + limit, + ...filters, + }); + return fetchWithAuth(`/tickets?${queryParams}`); + }, + + // Get a single ticket by ID + getTicket: (id) => fetchWithAuth(`/tickets/${id}`), + + // Create a new ticket + createTicket: (ticketData) => fetchWithAuth('/tickets', { + method: 'POST', + body: JSON.stringify(ticketData), + }), + + // Update a ticket + updateTicket: (id, ticketData) => fetchWithAuth(`/tickets/${id}`, { + method: 'PUT', + body: JSON.stringify(ticketData), + }), + + // Delete a ticket + deleteTicket: (id) => fetchWithAuth(`/tickets/${id}`, { + method: 'DELETE', + }), + + // Get ticket types + getTicketTypes: () => fetchWithAuth('/tickets/types'), + + // Create a new ticket type + createTicketType: (typeData) => fetchWithAuth('/tickets/types', { + method: 'POST', + body: JSON.stringify(typeData), + }), + + // Update a ticket type + updateTicketType: (id, typeData) => fetchWithAuth(`/tickets/types/${id}`, { + method: 'PUT', + body: JSON.stringify(typeData), + }), + + // Delete a ticket type + deleteTicketType: (id) => fetchWithAuth(`/tickets/types/${id}`, { + method: 'DELETE', + }), +}; + +// User API endpoints +export const userApi = { + // Get all users with pagination + getUsers: (page = 1, limit = 10, filters = {}) => { + const queryParams = new URLSearchParams({ + page, + limit, + ...filters, + }); + return fetchWithAuth(`/users?${queryParams}`); + }, + + // Get a single user by ID + getUser: (id) => fetchWithAuth(`/users/${id}`), + + // Create a new user + createUser: (userData) => fetchWithAuth('/users', { + method: 'POST', + body: JSON.stringify(userData), + }), + + // Update a user + updateUser: (id, userData) => fetchWithAuth(`/users/${id}`, { + method: 'PUT', + body: JSON.stringify(userData), + }), + + // Delete a user + deleteUser: (id) => fetchWithAuth(`/users/${id}`, { + method: 'DELETE', + }), + + // Get user roles/permissions + getPermissions: () => fetchWithAuth('/users/permissions'), + + // Update user role/permission + updatePermission: (userId, permissionData) => fetchWithAuth(`/users/${userId}/permissions`, { + method: 'PUT', + body: JSON.stringify(permissionData), + }), +}; + +// Authentication API endpoints +export const authApi = { + // Login + login: (credentials) => fetchWithAuth('/auth/login', { + method: 'POST', + body: JSON.stringify(credentials), + }), + + // Register + register: (userData) => fetchWithAuth('/auth/register', { + method: 'POST', + body: JSON.stringify(userData), + }), + + // Logout + logout: () => { + localStorage.removeItem('token'); + }, + + // Check if user is authenticated + isAuthenticated: () => !!localStorage.getItem('token'), +}; + +export default { + event: eventApi, + ticket: ticketApi, + user: userApi, + auth: authApi, +}; \ No newline at end of file diff --git a/src/views/admins/AdminForm.js b/src/views/admins/AdminForm.js new file mode 100644 index 000000000..731ec18ce --- /dev/null +++ b/src/views/admins/AdminForm.js @@ -0,0 +1,306 @@ +import React, { useState, useEffect } from 'react' +import { + CCard, + CCardBody, + CCardHeader, + CCol, + CRow, + CForm, + CFormInput, + CFormLabel, + CFormSelect, + CFormCheck, + CButton, + CAlert, + CInputGroup, + CInputGroupText, +} from '@coreui/react' +import CIcon from '@coreui/icons-react' +import { cilSave, cilX } from '@coreui/icons' +import { useNavigate, useParams } from 'react-router-dom' + +const AdminForm = () => { + const navigate = useNavigate() + const { id } = useParams() + const isEditMode = !!id + + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + email: '', + phone: '', + password: '', + confirmPassword: '', + role: 'admin', + isActive: true, + }) + + 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 admin data + useEffect(() => { + if (isEditMode) { + // Mock data for demonstration + const mockAdmins = [ + { + id: '1', + firstName: 'John', + lastName: 'Doe', + email: 'admin@example.com', + phone: '+1234567890', + role: 'super_admin', + isActive: true, + }, + { + id: '2', + firstName: 'Jane', + lastName: 'Smith', + email: 'jane.smith@example.com', + phone: '+0987654321', + role: 'admin', + isActive: true, + }, + { + id: '3', + firstName: 'Mike', + lastName: 'Johnson', + email: 'mike.j@example.com', + phone: '+1122334455', + role: 'editor', + isActive: false, + }, + ] + + // Find admin by ID + const admin = mockAdmins.find(admin => admin.id === id) + + if (admin) { + // Remove password fields for edit mode + const { password, confirmPassword, ...adminData } = admin + setFormData(adminData) + } else { + setError('Admin not found') + } + + setLoading(false) + } + }, [id, isEditMode]) + + const handleInputChange = (e) => { + const { name, value, type, checked } = e.target + + if (type === 'checkbox') { + setFormData({ ...formData, [name]: checked }) + } else { + setFormData({ ...formData, [name]: value }) + } + } + + const validateForm = () => { + // Reset error + setError(null) + + // Basic validation + if (!formData.firstName || !formData.lastName) { + setError('First name and last name are required') + return false + } + + if (!formData.email || !/\S+@\S+\.\S+/.test(formData.email)) { + setError('Valid email is required') + return false + } + + // Password validation for new admins + if (!isEditMode) { + if (!formData.password) { + setError('Password is required for new admins') + return false + } + + if (formData.password.length < 8) { + setError('Password must be at least 8 characters long') + return false + } + + if (formData.password !== formData.confirmPassword) { + setError('Passwords do not match') + 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 ? 'Admin updated successfully!' : 'Admin created successfully!') + + // Redirect after successful save + setTimeout(() => { + navigate('/admins/list') + }, 1500) + } catch (error) { + setError('Failed to save admin. Please try again.') + } finally { + setSaving(false) + } + } + + if (loading) { + return
Loading admin data...
+ } + + return ( + + + + + {isEditMode ? 'Edit Admin' : 'Create New Admin'} + + + {error && {error}} + {success && {success}} + + + + + First Name + + + + Last Name + + + + + + + Email + + + + Phone + + + + + {!isEditMode && ( + + + Password + + + + Confirm Password + + + + )} + + + + Role + + + + + + + + + + + + +
+ navigate('/admins/list')} + > + + Cancel + + + + {saving ? 'Saving...' : 'Save Admin'} + +
+
+
+
+
+
+ ) +} + +export default AdminForm \ No newline at end of file diff --git a/src/views/admins/AdminRoles.js b/src/views/admins/AdminRoles.js new file mode 100644 index 000000000..e3d833495 --- /dev/null +++ b/src/views/admins/AdminRoles.js @@ -0,0 +1,370 @@ +import React, { useState, useEffect } from 'react' +import { + CCard, + CCardBody, + CCardHeader, + CCol, + CRow, + CTable, + CTableHead, + CTableRow, + CTableHeaderCell, + CTableBody, + CTableDataCell, + CButton, + CFormCheck, + CAlert, + CForm, + CBadge, + CModal, + CModalHeader, + CModalTitle, + CModalBody, + CModalFooter, + CFormInput, + CFormLabel, + CFormTextarea, +} from '@coreui/react' +import CIcon from '@coreui/icons-react' +import { cilPlus, cilPencil, cilTrash, cilSave } from '@coreui/icons' + +// Mock data for roles +const initialRoles = [ + { + id: 1, + name: 'Super Admin', + description: 'Full access to all features and settings', + permissions: ['users_manage', 'admins_manage', 'roles_manage', 'system_settings', 'logs_view', 'gift_baskets_manage'], + isSystem: true, + }, + { + id: 2, + name: 'Admin', + description: 'Access to most administration features', + permissions: ['users_manage', 'gift_baskets_manage', 'logs_view'], + isSystem: false, + }, + { + id: 3, + name: 'Editor', + description: 'Can edit content but not access sensitive settings', + permissions: ['gift_baskets_manage'], + isSystem: false, + }, + { + id: 4, + name: 'Viewer', + description: 'View-only access to dashboard and reports', + permissions: [], + isSystem: false, + }, +] + +// Mock data for available permissions +const availablePermissions = [ + { id: 'users_manage', name: 'Manage Users', description: 'Create, edit, and delete users' }, + { id: 'admins_manage', name: 'Manage Admins', description: 'Create, edit, and delete admins' }, + { id: 'roles_manage', name: 'Manage Roles', description: 'Create, edit, and delete roles' }, + { id: 'system_settings', name: 'System Settings', description: 'Access to system configuration' }, + { id: 'logs_view', name: 'View Logs', description: 'Access to system and user logs' }, + { id: 'gift_boxes_manage', name: 'Manage Gift Boxes', description: 'Create, edit, and delete gift boxes' }, +] + +const AdminRoles = () => { + const [roles, setRoles] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + const [showModal, setShowModal] = useState(false) + const [currentRole, setCurrentRole] = useState({ + name: '', + description: '', + permissions: [], + }) + const [isEditing, setIsEditing] = useState(false) + + // Load mock data + useEffect(() => { + // Simulate API call + setTimeout(() => { + setRoles(initialRoles) + setLoading(false) + }, 500) + }, []) + + const handleOpenModal = (role = null) => { + if (role) { + setCurrentRole({ ...role }) + setIsEditing(true) + } else { + setCurrentRole({ + name: '', + description: '', + permissions: [], + }) + setIsEditing(false) + } + setShowModal(true) + } + + const handleCloseModal = () => { + setShowModal(false) + setError(null) + } + + const handleInputChange = (e) => { + const { name, value } = e.target + setCurrentRole({ ...currentRole, [name]: value }) + } + + const handlePermissionChange = (permissionId) => { + const permissions = [...currentRole.permissions] + + if (permissions.includes(permissionId)) { + // Remove permission + const index = permissions.indexOf(permissionId) + permissions.splice(index, 1) + } else { + // Add permission + permissions.push(permissionId) + } + + setCurrentRole({ ...currentRole, permissions }) + } + + const handleSubmit = async (e) => { + e.preventDefault() + + // Validate form + if (!currentRole.name) { + setError('Role name is required') + return + } + + try { + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 800)) + + if (isEditing) { + // Update existing role + setRoles(roles.map(role => + role.id === currentRole.id ? currentRole : role + )) + setSuccess('Role updated successfully!') + } else { + // Create new role + const newRole = { + ...currentRole, + id: Math.max(...roles.map(role => role.id)) + 1, + isSystem: false, + } + setRoles([...roles, newRole]) + setSuccess('Role created successfully!') + } + + // Close modal after short delay + setTimeout(() => { + handleCloseModal() + setSuccess(null) + }, 1500) + } catch (error) { + setError('Failed to save role. Please try again.') + } + } + + const handleDelete = async (id) => { + // Check if it's a system role + const role = roles.find(role => role.id === id) + + if (role && role.isSystem) { + setError('System roles cannot be deleted') + setTimeout(() => setError(null), 3000) + return + } + + if (window.confirm('Are you sure you want to delete this role?')) { + try { + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 500)) + + // Remove role + setRoles(roles.filter(role => role.id !== id)) + setSuccess('Role deleted successfully!') + + // Clear success message after delay + setTimeout(() => { + setSuccess(null) + }, 3000) + } catch (error) { + setError('Failed to delete role. Please try again.') + setTimeout(() => setError(null), 3000) + } + } + } + + return ( + + + + + Admin Roles + handleOpenModal()} + > + + Create Role + + + + {error && {error}} + {success && {success}} + + {loading ? ( +
Loading roles...
+ ) : ( + + + + Role Name + Description + Permissions + Type + Actions + + + + {roles.map(role => ( + + {role.name} + {role.description} + + {role.permissions.length > 0 ? ( +
+ {role.permissions.map(permId => { + const permission = availablePermissions.find(p => p.id === permId) + return ( + + {permission ? permission.name : permId} + + ) + })} +
+ ) : ( + No permissions + )} +
+ + {role.isSystem ? ( + System + ) : ( + Custom + )} + + + handleOpenModal(role)} + title="Edit" + > + + + handleDelete(role.id)} + disabled={role.isSystem} + title={role.isSystem ? 'System roles cannot be deleted' : 'Delete'} + > + + + +
+ ))} +
+
+ )} +
+
+
+ + {/* Role Modal */} + + + {isEditing ? 'Edit Role' : 'Create New Role'} + + + {error && {error}} + + +
+ Role Name + +
+ +
+ Description + +
+ +
+ Permissions + {availablePermissions.map(permission => ( +
+ handlePermissionChange(permission.id)} + disabled={isEditing && currentRole.isSystem} + /> +
{permission.description}
+
+ ))} +
+ + {isEditing && currentRole.isSystem && ( + + System roles can be viewed but not modified. + + )} + + + + Cancel + + + + {isEditing ? 'Update Role' : 'Create Role'} + + +
+
+
+
+ ) +} + +export default AdminRoles \ No newline at end of file diff --git a/src/views/admins/AdminsList.js b/src/views/admins/AdminsList.js new file mode 100644 index 000000000..405e2f998 --- /dev/null +++ b/src/views/admins/AdminsList.js @@ -0,0 +1,337 @@ +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 { Link, useNavigate } from 'react-router-dom' + +// Default avatar placeholder +const defaultAvatar = 'https://via.placeholder.com/40' + +// Mock data for admins +const mockAdmins = [ + { + id: 1, + firstName: 'John', + lastName: 'Doe', + email: 'admin@example.com', + phone: '+1234567890', + role: 'super_admin', + isActive: true, + avatar: null, + lastLogin: '2023-09-15T10:30:00Z', + createdAt: '2023-01-10T08:00:00Z', + }, + { + id: 2, + firstName: 'Jane', + lastName: 'Smith', + email: 'jane.smith@example.com', + phone: '+0987654321', + role: 'admin', + isActive: true, + avatar: null, + lastLogin: '2023-09-14T14:20:00Z', + createdAt: '2023-02-15T09:30:00Z', + }, + { + id: 3, + firstName: 'Mike', + lastName: 'Johnson', + email: 'mike.j@example.com', + phone: '+1122334455', + role: 'editor', + isActive: false, + avatar: null, + lastLogin: '2023-08-30T11:45:00Z', + createdAt: '2023-03-20T10:15:00Z', + }, +] + +const AdminsList = () => { + const navigate = useNavigate() + const [admins, setAdmins] = 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('') + + // Load mock data on component mount + useEffect(() => { + const fetchAdmins = () => { + setLoading(true) + + // Filter mock data based on filters + let filtered = [...mockAdmins] + + if (searchTerm) { + const term = searchTerm.toLowerCase() + filtered = filtered.filter(admin => + `${admin.firstName} ${admin.lastName}`.toLowerCase().includes(term) || + admin.email.toLowerCase().includes(term) || + (admin.phone && admin.phone.includes(term)) + ) + } + + if (filterRole) { + filtered = filtered.filter(admin => admin.role === filterRole) + } + + if (filterStatus) { + const isActive = filterStatus === 'active' + filtered = filtered.filter(admin => admin.isActive === isActive) + } + + setAdmins(filtered) + setTotalPages(Math.max(1, Math.ceil(filtered.length / 10))) + setLoading(false) + } + + fetchAdmins() + }, [searchTerm, filterRole, filterStatus]) + + 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 admin?')) { + // For mock data, just filter out the deleted admin + setAdmins(admins.filter(admin => admin.id !== id)) + } + } + + const getRoleBadge = (role) => { + const roleMap = { + 'super_admin': { color: 'danger', label: 'Super Admin' }, + 'admin': { color: 'warning', label: 'Admin' }, + 'editor': { color: 'info', label: 'Editor' }, + 'viewer': { color: 'success', label: 'Viewer' }, + } + + const roleInfo = roleMap[role] || { color: 'light', label: role } + + return ( + {roleInfo.label} + ) + } + + const getStatusBadge = (isActive) => { + return isActive ? + Active : + Inactive + } + + // Get paginated admins + const paginatedAdmins = admins.slice((page - 1) * 10, page * 10) + + return ( + + + + + Admins + navigate('/admins/create')} + > + + Add Admin + + + + {/* Search and Filters */} + + + + setSearchTerm(e.target.value)} + /> + + + { + setFilterRole(e.target.value) + setPage(1) + }} + > + + + + + + + + + { + setFilterStatus(e.target.value) + setPage(1) + }} + > + + + + + + + + + Search + + + + + + {/* Admins Table */} + {loading ? ( +
Loading admins...
+ ) : ( + <> + + + + Admin + Email + Phone + Role + Status + Last Login + Actions + + + + {paginatedAdmins.length > 0 ? ( + paginatedAdmins.map(admin => ( + + +
+ +
+
{admin.firstName} {admin.lastName}
+ ID: {admin.id} +
+
+
+ {admin.email} + {admin.phone || 'N/A'} + + {getRoleBadge(admin.role)} + + + {getStatusBadge(admin.isActive)} + + + {admin.lastLogin && new Date(admin.lastLogin).toLocaleDateString()} + + + + + + + + + View Details + + + + Edit + + handleDelete(admin.id)} + style={{ color: 'red' }} + > + + Delete + + + + +
+ )) + ) : ( + + + No admins found + + + )} +
+
+ + {/* Pagination */} + {totalPages > 1 && ( + + setPage(page - 1)} + > + Previous + + + {[...Array(totalPages).keys()].map(number => ( + setPage(number + 1)} + > + {number + 1} + + ))} + + setPage(page + 1)} + > + Next + + + )} + + )} +
+
+
+
+ ) +} + +export default AdminsList \ No newline at end of file diff --git a/src/views/dashboard/Dashboard.js b/src/views/dashboard/Dashboard.js index 57a55290d..e78ed276f 100644 --- a/src/views/dashboard/Dashboard.js +++ b/src/views/dashboard/Dashboard.js @@ -1,385 +1,393 @@ -import React from 'react' -import classNames from 'classnames' - +import React, { useState, useEffect } from 'react' import { - CAvatar, CButton, - CButtonGroup, CCard, CCardBody, - CCardFooter, CCardHeader, CCol, CProgress, CRow, - CTable, - CTableBody, - CTableDataCell, - CTableHead, - CTableHeaderCell, - CTableRow, + CWidgetStatsA, + CWidgetStatsB, + CWidgetStatsC, + CWidgetStatsF, + CBadge, + CAlert, } from '@coreui/react' import CIcon from '@coreui/icons-react' import { - cibCcAmex, - cibCcApplePay, - cibCcMastercard, - cibCcPaypal, - cibCcStripe, - cibCcVisa, - cibGoogle, - cibFacebook, - cibLinkedin, - cifBr, - cifEs, - cifFr, - cifIn, - cifPl, - cifUs, - cibTwitter, - cilCloudDownload, cilPeople, cilUser, - cilUserFemale, + cilGift, + cilMoney, + cilArrowTop, + cilArrowBottom, + cilCalendar, + cilClock, } from '@coreui/icons' - -import avatar1 from 'src/assets/images/avatars/1.jpg' -import avatar2 from 'src/assets/images/avatars/2.jpg' -import avatar3 from 'src/assets/images/avatars/3.jpg' -import avatar4 from 'src/assets/images/avatars/4.jpg' -import avatar5 from 'src/assets/images/avatars/5.jpg' -import avatar6 from 'src/assets/images/avatars/6.jpg' - -import WidgetsBrand from '../widgets/WidgetsBrand' -import WidgetsDropdown from '../widgets/WidgetsDropdown' -import MainChart from './MainChart' +import { CChart } from '@coreui/react-chartjs' const Dashboard = () => { - const progressExample = [ - { title: 'Visits', value: '29.703 Users', percent: 40, color: 'success' }, - { title: 'Unique', value: '24.093 Users', percent: 20, color: 'info' }, - { title: 'Pageviews', value: '78.706 Views', percent: 60, color: 'warning' }, - { title: 'New Users', value: '22.123 Users', percent: 80, color: 'danger' }, - { title: 'Bounce Rate', value: 'Average Rate', percent: 40.15, color: 'primary' }, - ] - - const progressGroupExample1 = [ - { title: 'Monday', value1: 34, value2: 78 }, - { title: 'Tuesday', value1: 56, value2: 94 }, - { title: 'Wednesday', value1: 12, value2: 67 }, - { title: 'Thursday', value1: 43, value2: 91 }, - { title: 'Friday', value1: 22, value2: 73 }, - { title: 'Saturday', value1: 53, value2: 82 }, - { title: 'Sunday', value1: 9, value2: 69 }, - ] + const [dashboardData, setDashboardData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) - const progressGroupExample2 = [ - { title: 'Male', icon: cilUser, value: 53 }, - { title: 'Female', icon: cilUserFemale, value: 43 }, - ] - - const progressGroupExample3 = [ - { title: 'Organic Search', icon: cibGoogle, percent: 56, value: '191,235' }, - { title: 'Facebook', icon: cibFacebook, percent: 15, value: '51,223' }, - { title: 'Twitter', icon: cibTwitter, percent: 11, value: '37,564' }, - { title: 'LinkedIn', icon: cibLinkedin, percent: 8, value: '27,319' }, - ] - - const tableExample = [ - { - avatar: { src: avatar1, status: 'success' }, - user: { - name: 'Yiorgos Avraamu', - new: true, - registered: 'Jan 1, 2023', - }, - country: { name: 'USA', flag: cifUs }, - usage: { - value: 50, - period: 'Jun 11, 2023 - Jul 10, 2023', - color: 'success', - }, - payment: { name: 'Mastercard', icon: cibCcMastercard }, - activity: '10 sec ago', - }, - { - avatar: { src: avatar2, status: 'danger' }, - user: { - name: 'Avram Tarasios', - new: false, - registered: 'Jan 1, 2023', - }, - country: { name: 'Brazil', flag: cifBr }, - usage: { - value: 22, - period: 'Jun 11, 2023 - Jul 10, 2023', - color: 'info', - }, - payment: { name: 'Visa', icon: cibCcVisa }, - activity: '5 minutes ago', - }, - { - avatar: { src: avatar3, status: 'warning' }, - user: { name: 'Quintin Ed', new: true, registered: 'Jan 1, 2023' }, - country: { name: 'India', flag: cifIn }, - usage: { - value: 74, - period: 'Jun 11, 2023 - Jul 10, 2023', - color: 'warning', - }, - payment: { name: 'Stripe', icon: cibCcStripe }, - activity: '1 hour ago', - }, - { - avatar: { src: avatar4, status: 'secondary' }, - user: { name: 'Enéas Kwadwo', new: true, registered: 'Jan 1, 2023' }, - country: { name: 'France', flag: cifFr }, - usage: { - value: 98, - period: 'Jun 11, 2023 - Jul 10, 2023', - color: 'danger', - }, - payment: { name: 'PayPal', icon: cibCcPaypal }, - activity: 'Last month', - }, - { - avatar: { src: avatar5, status: 'success' }, - user: { - name: 'Agapetus Tadeáš', - new: true, - registered: 'Jan 1, 2023', - }, - country: { name: 'Spain', flag: cifEs }, - usage: { - value: 22, - period: 'Jun 11, 2023 - Jul 10, 2023', - color: 'primary', - }, - payment: { name: 'Google Wallet', icon: cibCcApplePay }, - activity: 'Last week', - }, - { - avatar: { src: avatar6, status: 'danger' }, - user: { - name: 'Friderik Dávid', - new: true, - registered: 'Jan 1, 2023', + useEffect(() => { + // In a real app, replace with actual API call + // const fetchData = async () => { + // try { + // const response = await fetch('/api/dashboard') + // const data = await response.json() + // setDashboardData(data) + // } catch (err) { + // setError('Failed to load dashboard data') + // console.error(err) + // } finally { + // setLoading(false) + // } + // } + + // Mock data for development + const mockData = { + totalUsers: 1250, + activeUsers: 980, + totalGifts: 4500, + openedGifts: 3200, + totalPoints: 1234500000, + claimedPoints: 987600000, + btcStatus: { + btcGivenToday: true, + lastRecipient: "user123", + timestamp: "2023-10-21T14:30:22Z" }, - country: { name: 'Poland', flag: cifPl }, - usage: { - value: 43, - period: 'Jun 11, 2023 - Jul 10, 2023', - color: 'success', + todayStats: { + newUsers: 45, + activeUsers: 320, + giftsOpened: 189, + pointsClaimed: 3450000 }, - payment: { name: 'Amex', icon: cibCcAmex }, - activity: 'Last week', - }, - ] + weeklyTrends: { + users: [120, 95, 110, 85, 130, 150, 45], + gifts: [410, 380, 420, 310, 350, 390, 189], + points: [12500000, 9800000, 11200000, 8500000, 10200000, 9500000, 3450000] + } + } + + // Simulate API call + setTimeout(() => { + setDashboardData(mockData) + setLoading(false) + }, 800) + + // fetchData() + }, []) + + // Format numbers for easier reading + const formatNumber = (num) => { + if (num >= 1000000000) { + return (num / 1000000000).toFixed(1) + 'B' + } else if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M' + } else if (num >= 1000) { + return (num / 1000).toFixed(1) + 'K' + } + return num + } + + // Format points in Vietnamese Dong + const formatVND = (points) => { + return formatNumber(points) + ' VND' + } + + // Format date + const formatDate = (dateString) => { + const date = new Date(dateString) + return date.toLocaleString() + } + + // Generate week labels (last 7 days) + const generateWeekLabels = () => { + const days = [] + for (let i = 6; i >= 0; i--) { + const date = new Date() + date.setDate(date.getDate() - i) + days.push(date.toLocaleDateString('en-US', { weekday: 'short' })) + } + return days + } + + if (loading) { + return
+ } + + if (error) { + return {error} + } return ( <> - - - - - -

- Traffic -

-
January - July 2023
+ {/* User and Gift Summary Widgets */} + + + } + color="primary" + title="Total Users" + value={formatNumber(dashboardData.totalUsers)} + progress={{ value: Math.round((dashboardData.activeUsers / dashboardData.totalUsers) * 100) }} + text={`${formatNumber(dashboardData.activeUsers)} active users (${Math.round((dashboardData.activeUsers / dashboardData.totalUsers) * 100)}%)`} + /> + + + } + color="info" + title="Today's Active Users" + value={formatNumber(dashboardData.todayStats.activeUsers)} + progress={{ value: Math.round((dashboardData.todayStats.activeUsers / dashboardData.activeUsers) * 100) }} + text={`${formatNumber(dashboardData.todayStats.newUsers)} new users today`} + /> + + + } + color="warning" + title="Total Gift Boxes" + value={formatNumber(dashboardData.totalGifts)} + progress={{ value: Math.round((dashboardData.openedGifts / dashboardData.totalGifts) * 100) }} + text={`${formatNumber(dashboardData.openedGifts)} boxes opened (${Math.round((dashboardData.openedGifts / dashboardData.totalGifts) * 100)}%)`} + /> - - - - - - {['Day', 'Month', 'Year'].map((value) => ( - - {value} - - ))} - + + } + color="success" + title="Total Points" + value={formatVND(dashboardData.totalPoints)} + progress={{ value: Math.round((dashboardData.claimedPoints / dashboardData.totalPoints) * 100) }} + text={`${formatVND(dashboardData.claimedPoints)} points claimed (${Math.round((dashboardData.claimedPoints / dashboardData.totalPoints) * 100)}%)`} + /> - + + {/* BTC Status and Today's Stats */} + + + + + BTC Status + + +
+

+ Today's BTC: +

+ + {dashboardData.btcStatus.btcGivenToday ? "Given" : "Not Given"} + +
+ {dashboardData.btcStatus.btcGivenToday && ( + <> +
+ Last Recipient: {dashboardData.btcStatus.lastRecipient} +
+
+ Time: {formatDate(dashboardData.btcStatus.timestamp)} +
+ + )}
- - - {progressExample.map((item, index, items) => ( - -
{item.title}
-
- {item.value} ({item.percent}%) -
- +
- ))} -
- -
- - - + + - Traffic {' & '} Sales + + Today's Statistics + - - - -
-
New Clients
-
9,123
+ +
+
New Users
+
{formatNumber(dashboardData.todayStats.newUsers)}
- -
-
- Recurring Clients -
-
22,643
-
-
- -
- {progressGroupExample1.map((item, index) => ( -
-
- {item.title} -
-
- - -
+ + +
+
Active Users
+
{formatNumber(dashboardData.todayStats.activeUsers)}
- ))}
- - - + +
-
Pageviews
-
78,623
+
Gifts Opened
+
{formatNumber(dashboardData.todayStats.giftsOpened)}
- + +
-
Organic
-
49,123
-
-
-
- -
- - {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 + +
+ +
+ Color +
+ + +
+
+ + + + 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 + + + + + + + + + + + + Description + + + + + + + Start Date + + + + End Date + + + + Time + + + + + + + Venue + + + + Location + + + + + + + Address + + + + + + + City + + + + Country + + + + + + + Capacity + + + + Price + + $ + + + + + + + + Category + + + {categories.map(category => ( + + ))} + + + + Organizer + + + {organizers.map(organizer => ( + + ))} + + + + + + + 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" + > + + + + + handleFilterChange('device', e.target.value)} + className="w-auto" + > + + + + + + handleFilterChange('hasBtc', e.target.value)} + className="w-auto" + > + + + + + + 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) + }} + > + + {categories.map(category => ( + + ))} + + + + { + setFilterStatus(e.target.value) + setPage(1) + }} + > + + + + + + + + + + + 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 + + + + + + + +
+ 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) + }} + > + + + + + + + + + { + setFilterStatus(e.target.value) + setPage(1) + }} + > + + + + + + + + + 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} + > + + + + + + Scenario + navigate(`/gift-boxes/${isEditMode ? 'edit' : 'create'}/${boxType || 'firstBox'}/${e.target.value}${isEditMode ? `/${index}` : ''}`)} + disabled={isEditMode} + > + + + + + + + + + Reward Type + + + + + + + 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)} + > + + + +
+ + {/* 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)} + > + + + +
+ + {/* 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)} + > + + + +
+ + {/* 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/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) + }} + > + + {events.map(event => ( + + ))} + + + + { + setFilterType(e.target.value) + setPage(1) + }} + > + + {ticketTypes.map(type => ( + + ))} + + + + { + setFilterStatus(e.target.value) + setPage(1) + }} + > + + + + + + + + + + + + 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" + /> + + + + + + + + + {users.map(user => ( + + ))} + + + + + {/* 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.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) + }} + > + + + + + + + + + { + setFilterStatus(e.target.value) + setPage(1) + }} + > + + + + + + + + + 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 From b8b03d2786fba58a8f0c0b00b3c0ee9adf070476 Mon Sep 17 00:00:00 2001 From: "Mike (Hung)" Date: Wed, 21 May 2025 16:43:06 +0700 Subject: [PATCH 2/2] [feat] : makes new dashboard --- .env | 5 + .gitignore | 1 + Docs/admin-FE.docx | Bin 0 -> 12072 bytes src/_nav.js | 10 - src/views/dashboard/Dashboard.js | 123 +++++++--- src/views/management/users/UserDetail.js | 271 +++++++++++++++++++++++ src/views/management/users/Users.js | 198 +++++++++++++++++ 7 files changed, 571 insertions(+), 37 deletions(-) create mode 100644 .env create mode 100644 Docs/admin-FE.docx create mode 100644 src/views/management/users/UserDetail.js create mode 100644 src/views/management/users/Users.js diff --git a/.env b/.env new file mode 100644 index 000000000..6df086aaa --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +#prod +VITE_API_URL=https://b3uyvi86dj.execute-api.ap-northeast-1.amazonaws.com + +#local +# REACT_APP_API_URL=http://localhost:4000 \ No newline at end of file diff --git a/.gitignore b/.gitignore index e080cd5fb..414de915e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ yarn.lock npm-debug.log* yarn-debug.log* yarn-error.log* +REACT_APP_API_URL=https://b3uyvi86dj.execute-api.ap-northeast-1.amazonaws.com/api \ No newline at end of file diff --git a/Docs/admin-FE.docx b/Docs/admin-FE.docx new file mode 100644 index 0000000000000000000000000000000000000000..7b0843942959c574f86fd14a695a3fb73ce02914 GIT binary patch literal 12072 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 U%&*mj{(L-*J;!>8(*K0~ALsXoX#fBK literal 0 HcmV?d00001 diff --git a/src/_nav.js b/src/_nav.js index 2dfbd3856..e9f8b4507 100644 --- a/src/_nav.js +++ b/src/_nav.js @@ -141,16 +141,6 @@ const _nav = [ name: 'Register', to: '/register', }, - { - component: CNavItem, - name: 'Error 404', - to: '/404', - }, - { - component: CNavItem, - name: 'Error 500', - to: '/500', - }, ], }, { diff --git a/src/views/dashboard/Dashboard.js b/src/views/dashboard/Dashboard.js index e78ed276f..f650a278c 100644 --- a/src/views/dashboard/Dashboard.js +++ b/src/views/dashboard/Dashboard.js @@ -13,6 +13,7 @@ import { CWidgetStatsF, CBadge, CAlert, + CSpinner, } from '@coreui/react' import CIcon from '@coreui/icons-react' import { @@ -27,28 +28,66 @@ import { } from '@coreui/icons' import { CChart } from '@coreui/react-chartjs' +// URL của API - sử dụng import.meta.env thay vì process.env cho Vite +// console.log("import",import.meta.env.VITE_API_URL) +const API_URL = import.meta.env.VITE_API_URL || 'https://api.example.com' + const Dashboard = () => { const [dashboardData, setDashboardData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { - // In a real app, replace with actual API call - // const fetchData = async () => { - // try { - // const response = await fetch('/api/dashboard') - // const data = await response.json() - // setDashboardData(data) - // } catch (err) { - // setError('Failed to load dashboard data') - // console.error(err) - // } finally { - // setLoading(false) - // } - // } + const fetchDashboardData = async () => { + try { + setLoading(true) + + // Gọi API thực tế + const response = await fetch(`${API_URL}/api/dashboard`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` // Nếu có yêu cầu xác thực + } + }) + + // Kiểm tra response status + if (!response.ok) { + throw new Error(`API error: ${response.status}`) + } + + // Parse dữ liệu JSON + const data = await response.json() + + // Cập nhật state với dữ liệu từ API + setDashboardData(data) + setError(null) + } catch (err) { + console.error('Failed to fetch dashboard data:', err) + setError('Không thể tải dữ liệu dashboard. Vui lòng thử lại sau.') + + // Sử dụng dữ liệu giả lập trong trường hợp API lỗi (chỉ dùng cho development) + if (process.env.NODE_ENV === 'development') { + setDashboardData(mockDashboardData()) + } + } finally { + setLoading(false) + } + } + + // Gọi function fetch data + fetchDashboardData() + + // Thiết lập interval để cập nhật dữ liệu mỗi 5 phút + const intervalId = setInterval(fetchDashboardData, 5 * 60 * 1000) - // Mock data for development - const mockData = { + // Cleanup interval khi component unmount + return () => clearInterval(intervalId) + }, []) + + // Dữ liệu giả lập cho development + const mockDashboardData = () => { + return { totalUsers: 1250, activeUsers: 980, totalGifts: 4500, @@ -72,15 +111,7 @@ const Dashboard = () => { points: [12500000, 9800000, 11200000, 8500000, 10200000, 9500000, 3450000] } } - - // Simulate API call - setTimeout(() => { - setDashboardData(mockData) - setLoading(false) - }, 800) - - // fetchData() - }, []) + } // Format numbers for easier reading const formatNumber = (num) => { @@ -116,16 +147,54 @@ const Dashboard = () => { return days } + // Hiển thị loading spinner if (loading) { - return
+ return ( +
+ + Đang tải dữ liệu... +
+ ) + } + + // Hiển thị thông báo lỗi + if (error && !dashboardData) { + return ( + + Lỗi! {error} +
+ window.location.reload()} + > + Thử lại + +
+
+ ) } - if (error) { - return {error} + // Hiển thị cảnh báo khi có lỗi nhưng vẫn có dữ liệu + const errorAlert = error && ( + + {error} + + ) + + // Nếu không có dữ liệu và không đang loading + if (!dashboardData) { + return ( + + Không có dữ liệu nào khả dụng. + + ) } return ( <> + {errorAlert} + {/* User and Gift Summary Widgets */} 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 ? ( +
+ +

Loading users...

+
+ ) : ( + + + + 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