Skip to content

Commit

Permalink
Merge pull request #33 from atlp-rwanda/187788139-ft-admin-should-con…
Browse files Browse the repository at this point in the history
…troll-all-sellers

[Finishes #187788139] Control sellers on admin dashboard
  • Loading branch information
niyontwali authored Jul 14, 2024
2 parents f38f190 + d803952 commit c54df12
Show file tree
Hide file tree
Showing 13 changed files with 779 additions and 5 deletions.
10 changes: 10 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import Category from './pages/admin/Category';
import Sellers from './pages/admin/Sellers';
import Buyers from './pages/admin/Buyers';
import Messages from './pages/admin/Messages';
import UserManagement from './pages/admin/UserManagement';
import NotFoundPage from './pages/NotFoundPage';
import Settings from './pages/admin/Settings';

import CategoriesPage from './pages/CategoriesPage';
Expand Down Expand Up @@ -125,6 +127,10 @@ const App = () => {
path: 'messages',
element: <Messages />,
},
{
path: 'users',
element: <UserManagement />,
},
{
path: 'settings',
element: <Settings />,
Expand All @@ -135,6 +141,10 @@ const App = () => {
path: 'search',
element: <Searchpage />,
},
{
path: '*',
element: <NotFoundPage />,
},
]);
return (
<>
Expand Down
Binary file added src/assets/404_page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions src/components/dashboard/ConfirmDisableModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';

interface ConfirmDisableModalProps {
sellerName: string;
onClose: () => void;
onConfirm: () => void;
}

const ConfirmDisableModal: React.FC<ConfirmDisableModalProps> = ({ sellerName, onClose, onConfirm }) => {
const handleBackgroundClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
onClose();
}
};

return (
<div
className='fixed inset-0 z-50 bg-[black] bg-opacity-50 overflow-y-auto flex justify-center items-center'
onClick={handleBackgroundClick}
>
<div className='bg-whiteColor p-5 rounded-lg shadow-lg w-96'>
<h2 className='text-xl font-bold mb-4'>Confirm Disable</h2>
<p className='mb-4'>
Are you sure you want to disable the seller account for <b>{sellerName}</b>? <br />
<br />
This action will change their role to buyer.
</p>
<div className='flex justify-end'>
<button onClick={onClose} className='mr-2 px-4 py-2 bg-grayColor rounded'>
Cancel
</button>
<button onClick={onConfirm} className='px-4 py-2 bg-overlay text-whiteColor rounded'>
Confirm
</button>
</div>
</div>
</div>
);
};

export default ConfirmDisableModal;
69 changes: 69 additions & 0 deletions src/components/dashboard/RoleChangeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useState } from 'react';

interface Role {
id: string;
name: string;
}

interface RoleChangeModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (roleId: string) => void;
user: any;
roles: Role[];
}

const RoleChangeModal: React.FC<RoleChangeModalProps> = ({ isOpen, onClose, onConfirm, user, roles }) => {
const [selectedRoleId, setSelectedRoleId] = useState<string>('');

if (!isOpen || !user) return null;

const handleConfirm = () => {
if (selectedRoleId) {
onConfirm(selectedRoleId);
}
onClose();
};

return (
<div className='fixed inset-0 bg-blackColor bg-opacity-50 z-50 flex items-center justify-center'>
<div className='bg-whiteColor p-6 rounded-lg'>
<h2 className='text-xl font-bold mb-4'>
Change Role for{' '}
<span className='p-1.5 text-xs font-[800] uppercase tracking-wider text-[#166534] bg-[#BBF7D0] rounded-lg bg-opacity-50'>
{user.firstName} {user.lastName}
</span>
</h2>
<p>Current Role: {user.Role.name}</p>
<div className='mt-4'>
<label className='block mb-2'>New Role:</label>
<select
className='w-full p-2 border rounded'
onChange={e => setSelectedRoleId(e.target.value)}
value={selectedRoleId || user.Role.id}
>
{roles.map(role => (
<option key={role.id} value={role.id}>
{role.name}
</option>
))}
</select>
</div>
<div className='mt-6 flex justify-end'>
<button onClick={onClose} className='px-4 py-2 bg-[#D1D5DB] rounded mr-2 hover:bg-[#9CA3AF]'>
Cancel
</button>
<button
onClick={handleConfirm}
className='px-4 py-2 bg-[#3B82F6] text-whiteColor rounded hover:bg-[#2563EB]'
disabled={!selectedRoleId || selectedRoleId === user.Role.id}
>
Confirm
</button>
</div>
</div>
</div>
);
};

export default RoleChangeModal;
15 changes: 14 additions & 1 deletion src/components/dashboard/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FaRegListAlt, FaUserFriends, FaUserTie, FaRegEnvelope, FaCog } from 'react-icons/fa';
import { FaRegListAlt, FaUserFriends, FaUserTie, FaRegEnvelope, FaCog, FaUsers } from 'react-icons/fa';
import { FiLogOut } from 'react-icons/fi';
import { RxDashboard } from 'react-icons/rx';
import logo from '../../assets/Rectangle 2487.png';
Expand Down Expand Up @@ -97,6 +97,19 @@ export default function Sidebar({ isOpen, toggleSidebar }: { isOpen: boolean; to
Messages
</NavLink>
</li>
<li>
<NavLink
to={'/admin/users'}
className={({ isActive }) =>
`flex items-center py-2.5 px-4 rounded transition duration-200 hover:bg-[#E5E7EB] ${
isActive ? 'bg-skyBlue text-skyBlueText hover:bg-skyBlue' : 'text-[#8F8183]'
}`
}
>
<FaUsers className='mr-3' />
Users
</NavLink>
</li>
<li>
<NavLink
to={'/admin/settings'}
Expand Down
159 changes: 159 additions & 0 deletions src/components/dashboard/UsersTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React, { useState, useMemo } from 'react';

interface Column {
key: string;
label: string;
isImage?: boolean;
render?: (item: any) => React.ReactNode;
}

interface TableProps {
data: any[];
columns: Column[];
itemsPerPage: number;
}

const getInitials = (firstName: string, lastName: string) =>
`${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();

const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};

const Table: React.FC<TableProps> = ({ data, columns, itemsPerPage }) => {
const [currentPage, setCurrentPage] = useState(1);
const [sortColumn, setSortColumn] = useState<string | null>(null);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [searchTerm, setSearchTerm] = useState('');

const filteredData = useMemo(
() =>
data.filter(item =>
columns.some(column => String(item[column.key]).toLowerCase().includes(searchTerm.toLowerCase()))
),
[data, columns, searchTerm]
);

const sortedData = useMemo(() => {
if (!sortColumn) return filteredData;
return [...filteredData].sort((a, b) => {
if (a[sortColumn] < b[sortColumn]) return sortDirection === 'asc' ? -1 : 1;
if (a[sortColumn] > b[sortColumn]) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
}, [filteredData, sortColumn, sortDirection]);

const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
return sortedData.slice(startIndex, startIndex + itemsPerPage);
}, [sortedData, currentPage, itemsPerPage]);

const totalPages = Math.ceil(sortedData.length / itemsPerPage);

const handleSort = (column: string) => {
if (column === sortColumn) {
setSortDirection(prev => (prev === 'asc' ? 'desc' : 'asc'));
} else {
setSortColumn(column);
setSortDirection('asc');
}
};

const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
};

const renderCell = (item: any, column: Column) => {
if (column.isImage) {
return item[column.key] ? (
<img
src={item[column.key]}
alt={`${item.firstName} ${item.lastName}`}
className='ml-2 w-10 h-10 rounded-full object-cover'
/>
) : (
<div
className='ml-2 w-10 h-10 flex items-center justify-center text-whiteColor font-bold text-lg rounded-full uppercase'
style={{ backgroundColor: getRandomColor() }}
>
{getInitials(item.firstName, item.lastName)}
</div>
);
}
if (column.render) return column.render(item);
return column.key.includes('.')
? column.key.split('.').reduce((obj, key) => obj && obj[key], item)
: item[column.key];
};

return (
<div className='overflow-x-auto pl-4'>
<div className='mb-2'>
<input
type='text'
placeholder='Search user...'
value={searchTerm}
onChange={handleSearch}
className='px-2 py-1 border rounded outline-none'
/>
</div>
<table className='min-w-full bg-whiteColor px-10'>
<thead className='bg-[#F3F4F6]'>
<tr>
{columns.map(column => (
<th
key={column.key}
onClick={() => !column.isImage && handleSort(column.key)}
className={`pl-4 py-4 text-left text-sm font-bold text-[#6B7280] uppercase tracking-wider ${!column.isImage ? 'cursor-pointer' : ''}`}
>
{column.label}
{!column.isImage && sortColumn === column.key && <span>{sortDirection === 'asc' ? ' ▲' : ' ▼'}</span>}
</th>
))}
</tr>
</thead>
<tbody>
{paginatedData.map((item, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-[#F9FAFB]' : 'bg-whiteColor'}>
{columns.map(column => (
<td key={column.key} className='px-2 py-2 whitespace-nowrap text-sm'>
{renderCell(item, column)}
</td>
))}
</tr>
))}
</tbody>
</table>
<div className='mt-4 flex flex-col sm:flex-row justify-between items-center'>
<div className='mb-2 sm:mb-0 text-sm'>
Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, sortedData.length)} of{' '}
{sortedData.length} entries
</div>
<div className='flex justify-center'>
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className='px-3 py-1 border rounded mr-2 text-sm'
>
Previous
</button>
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className='px-3 py-1 border rounded text-sm'
>
Next
</button>
</div>
</div>
</div>
);
};

export default Table;
Loading

0 comments on commit c54df12

Please sign in to comment.