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) => (
- {column} |
- ))}
-
-
-
- {profiles.map(profile => (
- routeChange(profile.id)}>
- {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