diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesTable.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesTable.jsx index a19c5a6ca..7182992cd 100644 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesTable.jsx +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesTable.jsx @@ -1,87 +1,446 @@ -import { useState } from 'react'; -import css from './ProfilesTable.module.css'; -import { useNavigate } from 'react-router-dom'; -import PaginationButtons from './PaginationButtons'; +import {useEffect, useState} from 'react'; +import {useLocation, useNavigate} from 'react-router-dom'; +import css from './ProfilesTable.module.scss'; import axios from 'axios'; -import useSWR from 'swr'; -import { DEFAULT_PAGE_SIZE } from '../constants'; +import useSWR, {mutate} from 'swr'; +import {Button, Input, Pagination, Space, Table, Tag} from 'antd'; +import {CaretDownOutlined, CaretUpOutlined, SearchOutlined} from '@ant-design/icons'; +import Highlighter from 'react-highlight-words'; +import UserActionsProfiles from './UserActionsProfiles'; +import { DatePicker } from 'antd'; -const COLUMN_NAMES = [ - 'ID', 'Person', 'Position', 'Company', 'Region ID', 'Phone', 'EDRPOU', 'Adress', 'IsDeleted', 'IsApproved' -]; + +const DEFAULT_PAGE_SIZE = 10; function ProfilesTable() { + const location = useLocation(); const navigate = useNavigate(); - const routeChange = (id) => { - const path = `../../customadmin/profile/${id}`; - navigate(path); - }; - const [currentPage, setCurrentPage] = useState(1); + const queryParams = new URLSearchParams(location.search); + const pageNumber = Number(queryParams.get('page')) || 1; + const [currentPage, setCurrentPage] = useState(pageNumber); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); - const handlePageSizeChange = (size) => { - setPageSize(size); - setCurrentPage(1); - }; - const handlePageChange = (page) => { - setCurrentPage(page); - }; + const [sortInfo, setSortInfo] = useState({ field: null, order: null }); + const [statusFilters, setStatusFilters] = useState([]); + const [searchParams, setSearchParams] = useState({}); - const url = `${process.env.REACT_APP_BASE_API_URL}/api/admin/profiles/?page=${currentPage}&page_size=${pageSize}`; + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + const updatedPageNumber = Number(queryParams.get('page')) || 1; + setCurrentPage(updatedPageNumber); + }, [location.search]); + const ordering = sortInfo.field + ? `&ordering=${sortInfo.order === 'ascend' ? sortInfo.field : '-' + sortInfo.field}` + : ''; + const filtering = statusFilters ? statusFilters.map((filter) => `&${filter}=true`).join('') : ''; + const query = new URLSearchParams(searchParams).toString(); + const url = `${process.env.REACT_APP_BASE_API_URL}/api/admin/profiles?page=${currentPage}&page_size=${pageSize}${ordering}${filtering}&${query}`; async function fetcher(url) { const response = await axios.get(url); return response.data; } - const { data, error, isValidating: loading } = useSWR(url, fetcher); - + const { RangePicker } = DatePicker; + const { data, isValidating: loading } = useSWR(url, fetcher); const profiles = data ? data.results : []; + const totalItems = data ? data.total_items : 0; + + const updateQueryParams = (newPage) => { + queryParams.set('page', newPage); + navigate(`?${queryParams.toString()}`); + }; + + const handlePageChange = (page, size) => { + setCurrentPage(page); + setPageSize(size); + updateQueryParams(page); + }; + + const handleTableChange = (pagination, filters, sorter) => { + if (sorter.field) { + const newSortInfo = + sorter.order === null || sorter.order === undefined + ? { field: null, order: null } + : { field: sorter.field, order: sorter.order }; + + setSortInfo(newSortInfo); + } else { + setSortInfo({ field: null, order: null }); + } + + setStatusFilters(filters.status); + setCurrentPage(1); + updateQueryParams(1); + }; + const getSortIcon = (sortOrder) => { + if (!sortOrder) return ; + return sortOrder === 'ascend' ? ( + + ) : ( + + ); + }; + + const handleSearch = (selectedKeys, confirm, dataIndex) => { + confirm(); + + if (selectedKeys[0]) { + setSearchParams((prev) => ({ ...prev, [dataIndex]: selectedKeys[0] })); + } else { + setSearchParams((prev) => { + const updatedParams = { ...prev }; + delete updatedParams[dataIndex]; + return updatedParams; + }); + } + }; + const handleReset = (clearFilters, confirm, dataIndex) => { + clearFilters(); + setSearchParams((prev) => { + const updatedParams = { ...prev }; + delete updatedParams[dataIndex]; + return updatedParams; + }); + confirm(); + }; + + const getColumnSearchProps = (dataIndex) => ({ + filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}) => ( +
+ setSelectedKeys(e.target.value ? [e.target.value]: [])} + onPressEnter={() => handleSearch(selectedKeys, confirm, dataIndex)} + className={css['antInput']} + > + + + + +
+ ), + filterIcon: (filtered) => , + onFilter: (value, record) => + record[dataIndex]?.toString().toLowerCase().includes(value.toLowerCase()), + render: (text) => { + const searchValue = searchParams[dataIndex] || ''; + return searchValue? ( + + ) : ( + text + ); + } + }); + const getDateRangeColumnProps = (dataIndex) => ({ + filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => ( +
+ setSelectedKeys([dateStrings])} + className={css['antInput']} + /> + + + + +
+ ), + filterIcon: (filtered) => , + onFilter: (value, record) => { + const [start, end] = value; + const recordDate = new Date(record[dataIndex]); + return (!start || recordDate >= new Date(start)) && (!end || recordDate <= new Date(end)); + }, +}); + const renderCompanyTypeTags = (type) => { + const tags = []; + + if (type === 'Компанія') { + tags.push(Компанія); + } + if (type === 'Стартап') { + tags.push(Стартап); + } + if (type === 'Компанія і стартап') { + tags.push(Компанія і стартап); + } + + return <>{tags}; + }; + + const renderStatusTags = (status) => { + const tags = []; + if (status === 'undefined') { + tags.push(Не визначена); + } + if (status === 'pending') { + tags.push(На модерації); + } + if (status === 'blocked') { + tags.push(Заблокована); + } + if (status === 'active') { + tags.push(Активна); + } + if (status === 'approved') { + tags.push(Підтверджена); + } + if (status === 'auto_approved') { + tags.push(Підтверджена); + } + + return <>{tags}; + }; + + const renderActivityTags = (activities) => { + const tags = []; + activities.forEach((activity) => { + if (activity.name === 'Виробник') { + tags.push(Виробник); + } + if (activity.name === 'Імпортер') { + tags.push(Імпортер); + } + if (activity.name === 'Роздрібна мережа') { + tags.push(Роздрібна мережа); + } + if (activity.name === 'Інші послуги') { + tags.push(Інші послуги); + } + }); + return <>{tags}; + }; + const renderBusinessEntityTags = (business_entity) => { + const tags = []; + if (business_entity === 'Юридична особа') { + tags.push(Юридична особа); + } + if (business_entity === 'ФОП') { + tags.push(ФОП); + } + + return <>{tags}; + }; + + const columns = [ + { + title: (
+ Назва компанії +
), + dataIndex: 'name', + key: 'name', + sorter: true, + sortOrder: sortInfo.field === 'name' ? sortInfo.order : null, + sortIcon: ({ sortOrder }) => getSortIcon(sortOrder), + ...getColumnSearchProps('name'), + width: 140, + render: (text, profile) => ( + { + mutate(url); + }} + /> + ), + }, + { + title: (
+ Тип компанії +
), + dataIndex: 'company_type', + key: 'company_type', + render: (company_type) => renderCompanyTypeTags(company_type), + filters: [ + { text: 'Компанія', value: 'Компанія' }, + { text: 'Стартап', value: 'Стартап' }, + { text: 'Компанія і стартап', value: 'Компанія і стартап' }, + ], + onFilter: (value, record) => record.company_type === value, + width: 140 + }, + { + title: (
+ Вид діяльності +
), + dataIndex: 'activities', + key: 'activities', + render: (activity) => renderActivityTags(activity), + filters: [ + { text: 'Імпортер', value: 'Імпортер' }, + { text: 'Виробник', value: 'Виробник' }, + { text: 'Роздрібна мережа', value: 'Роздрібна мережа' }, + { text: 'Інші послуги', value: 'Інші послуги' }, + ], + onFilter: (value, record) => + record.activities && record.activities.some((activity) => activity.name === value), + width: 160 + }, + { + title: (
+ Суб'єкт
господарювання +
), + dataIndex: 'business_entity', + key: 'business_entity', + render: (entity) => renderBusinessEntityTags(entity), + filters: [ + { text: 'ФОП', value: 'ФОП' }, + { text: 'Юридична особа', value: 'Юридична особа' }, + ], + onFilter: (value, record) => record.business_entity === value, + width: 180 + }, + { + title: (
+ Дата реєстрації +
), + dataIndex: 'created_at', + key: 'created_at', + sorter: true, + sortOrder: sortInfo.field === 'created_at' ? sortInfo.order : null, + sortIcon: ({sortOrder}) => getSortIcon(sortOrder), + ...getDateRangeColumnProps('created_at'), + width: 170 + }, + { + title: (
+ Дата оновлення +
), + dataIndex: 'updated_at', + key: 'updated_at', + sorter: true, + sortOrder: sortInfo.field === 'updated_at' ? sortInfo.order : null, + sortIcon: ({sortOrder}) => getSortIcon(sortOrder), + ...getDateRangeColumnProps('updated_at'), + width: 150 + }, + { + title: (
+ Статус +
), + dataIndex: 'status', + key: 'status', + render: (status) => renderStatusTags(status), + filters: [ + { text: 'Не визначена', value: 'undefined' }, + { text: 'На модерації', value: 'pending' }, + { text: 'Заблокована', value: 'blocked' }, + { text: 'Активна', value: 'active' }, + { text: 'Підтверджена', value: ['approved', 'auto_approved']}, + ], + onFilter: (value, record) => { + if (Array.isArray(value)) { + return value.includes(record.status); + } + return record.status === value; + }, + width: 120 + }, + { + title: (
+ Представник +
), + dataIndex: 'representative', + key: 'representative', + sorter: true, + sortOrder: sortInfo.field === 'representative' ? sortInfo.order : null, + ...getColumnSearchProps('representative'), + width: 170, + }, + { + title: (
+ Контакти +
), + dataIndex: 'phone', + key: 'phone', + sorter: true, + sortOrder: sortInfo.field === 'phone' ? sortInfo.order : null, + ...getColumnSearchProps('phone'), + width: 130, + }, + { + title: (
+ Адреса +
), + dataIndex: 'address', + key: 'address', + sorter: true, + sortOrder: sortInfo.field === 'address' ? sortInfo.order : null, + ...getColumnSearchProps('address'), + width: 130 + }, + ]; return ( -
- + + + -
    - {loading &&
  • Завантаження ...
  • } - {error &&
  • Виникла помилка: {error}
  • } -
-
- - - {COLUMN_NAMES.map((column) => ( - - ))} - - - - {profiles.map(profile => ( - routeChange(profile.id)}> - - - - - - - - - - - - ))} - -
{column}
{profile.id} - {profile.person.name} - {profile.person.surname} - {profile.person_position} {profile.official_name} -
    - {profile.regions.map(region => ( -
  • {region.name_ukr}
  • - ))} -
-
{profile.phone} {profile.edrpou} {profile.address} {profile.is_deleted ? 'True' : 'False'} {profile.is_registered ? 'True' : 'False'}
-
+ ); } -export default ProfilesTable; \ No newline at end of file +export default ProfilesTable; diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesTable.module.css b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesTable.module.css deleted file mode 100644 index b716ba4e9..000000000 --- a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesTable.module.css +++ /dev/null @@ -1,62 +0,0 @@ -.table-profiles { - width: var(--table-size__admin-panel); -} - -.table-section { - margin-top: 20px; - padding: 10px; - width: var(--table-size__admin-panel); -} - -.table-header { - text-align: left; - background: rgb(214, 243, 234); - font-family: var(--font__admin-panel); - backdrop-filter: blur(4px); -} - -.table-header__text, -.table-element__text { - padding: 10px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: 1.5; - text-align: center; -} - -.table-element { - transition: transform 0.2s ease; - display: table-row; - font-family: var(--font__admin-panel); -} - -.table-element:hover { - transform: scale(1.03); -} - -.table-element:nth-child(odd) { - background: var(--blue-0, #eef8ff); -} - -.table-element:nth-child(even) { - background: var(--blue-0, #ecefff); -} - -.table-element td { - vertical-align: middle; - display: table-cell; - text-align: center; -} - -.log-section { - list-style: none; - padding: 0; - margin: 0; - height: 20px; - overflow-y: auto; - } - - .log { - margin-bottom: 10px; - } \ No newline at end of file diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesTable.module.scss b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesTable.module.scss new file mode 100644 index 000000000..0cae0135a --- /dev/null +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/ProfilesTable.module.scss @@ -0,0 +1,49 @@ +.table-container { + overflow-x: auto; +} + +.icon { + font-size: 12px; +} + +.empty-icon { + width: 12px; + height: 12px; +} + +.dropdownMenu { + padding: 8px; +} + +.antBtn { + width: 90px; +} + +.antInput { + margin-bottom: 8px; + display: block; +} + +.pagination { + margin-top: 16px; + text-align: center; + justify-content: center; + margin-bottom: 16px; +} + +.icon { + font-size: 16px; + font-weight: normal; + color: inherit; +} + +.filteredIcon { + font-size: 25px; + font-weight: bold; + color: #1f9a7c; +} + +.TableSubject { + text-align: center; + white-space: normal; +} \ No newline at end of file diff --git a/FrontEnd/src/pages/AdminPage/UserProfilesTable/UserActionsProfiles.jsx b/FrontEnd/src/pages/AdminPage/UserProfilesTable/UserActionsProfiles.jsx new file mode 100644 index 000000000..a91ddd8a9 --- /dev/null +++ b/FrontEnd/src/pages/AdminPage/UserProfilesTable/UserActionsProfiles.jsx @@ -0,0 +1,32 @@ +import { Tooltip} from 'antd'; +import { toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; +import PropTypes from 'prop-types'; +import { useNavigate } from 'react-router-dom'; + +function UserActionsProfiles({ text, profile }) { + const navigate = useNavigate(); + const viewProfile = () => { + try { + navigate(`/customadmin/profile/${profile.id}`); + } catch (error) { + toast.error('Не вдалося переглянути профіль. Спробуйте оновити сторінку.'); + } + }; + + return ( + + + {text} + + ); +} + +UserActionsProfiles.propTypes = { + profile: PropTypes.shape({ + id: PropTypes.number.isRequired, + }).isRequired, + onActionComplete: PropTypes.func, +}; + +export default UserActionsProfiles; diff --git a/FrontEnd/src/pages/CustomThemes/customAdminTheme.js b/FrontEnd/src/pages/CustomThemes/customAdminTheme.js index 630dca75f..13138f066 100644 --- a/FrontEnd/src/pages/CustomThemes/customAdminTheme.js +++ b/FrontEnd/src/pages/CustomThemes/customAdminTheme.js @@ -38,4 +38,4 @@ const customAdminTheme = { } }; -export default customAdminTheme; +export default customAdminTheme; \ No newline at end of file