-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33 from atlp-rwanda/187788139-ft-admin-should-con…
…troll-all-sellers [Finishes #187788139] Control sellers on admin dashboard
- Loading branch information
Showing
13 changed files
with
779 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.