feat: add guest role to user management and update statistics display
This commit is contained in:
parent
60057b6c94
commit
adfe136d74
@ -14,7 +14,7 @@ import useAuthStore from '../../store/authStore'
|
||||
|
||||
type UserType = 'personal' | 'company'
|
||||
type UserStatus = 'active' | 'pending' | 'disabled' | 'inactive' | 'suspended' | 'archived'
|
||||
type UserRole = 'user' | 'admin'
|
||||
type UserRole = 'user' | 'admin' | 'guest'
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
@ -32,7 +32,7 @@ interface User {
|
||||
|
||||
const STATUSES: UserStatus[] = ['active','pending','disabled','inactive']
|
||||
const TYPES: UserType[] = ['personal','company']
|
||||
const ROLES: UserRole[] = ['user','admin']
|
||||
const ROLES: UserRole[] = ['user','admin','guest']
|
||||
|
||||
export default function AdminUserManagementPage() {
|
||||
const { isAdmin } = useAdminUsers()
|
||||
@ -122,6 +122,7 @@ export default function AdminUserManagementPage() {
|
||||
const stats = useMemo(() => ({
|
||||
total: allUsers.length,
|
||||
admins: allUsers.filter(u => u.role === 'admin').length,
|
||||
guests: allUsers.filter(u => u.role === 'guest').length,
|
||||
personal: allUsers.filter(u => u.user_type === 'personal').length,
|
||||
company: allUsers.filter(u => u.user_type === 'company').length,
|
||||
active: allUsers.filter(u => u.status === 'active').length,
|
||||
@ -232,7 +233,7 @@ export default function AdminUserManagementPage() {
|
||||
t==='personal' ? badge('Personal','blue') : badge('Company','purple')
|
||||
|
||||
const roleBadge = (r: UserRole) =>
|
||||
r==='admin' ? badge('Admin','indigo') : badge('User','gray')
|
||||
r==='admin' ? badge('Admin','indigo') : r==='guest' ? badge('Guest','amber') : badge('User','gray')
|
||||
|
||||
// Action handler for opening edit modal
|
||||
const onEdit = (id: string) => {
|
||||
@ -256,7 +257,7 @@ export default function AdminUserManagementPage() {
|
||||
|
||||
{/* Statistic Section + Verify Button */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center gap-6">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-6 flex-1">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-7 gap-6 flex-1">
|
||||
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
|
||||
<div className="text-xs text-gray-500">Total Users</div>
|
||||
<div className="text-xl font-semibold text-blue-900">{stats.total}</div>
|
||||
@ -265,6 +266,10 @@ export default function AdminUserManagementPage() {
|
||||
<div className="text-xs text-gray-500">Admins</div>
|
||||
<div className="text-xl font-semibold text-indigo-700">{stats.admins}</div>
|
||||
</div>
|
||||
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
|
||||
<div className="text-xs text-gray-500">Guests</div>
|
||||
<div className="text-xl font-semibold text-amber-700">{stats.guests}</div>
|
||||
</div>
|
||||
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
|
||||
<div className="text-xs text-gray-500">Personal</div>
|
||||
<div className="text-xl font-semibold text-blue-700">{stats.personal}</div>
|
||||
|
||||
@ -327,7 +327,31 @@ export default function SummaryPage() {
|
||||
>
|
||||
Fill fields with logged in data
|
||||
</button>
|
||||
{/* "For someone else" is disabled for now — only self-subscriptions */}
|
||||
{/* Toggle: For myself / For someone else */}
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsForSelf(true)}
|
||||
className={`flex-1 rounded-md px-3 py-2 text-sm font-medium transition ${
|
||||
isForSelf
|
||||
? 'bg-[#1C2B4A] text-white shadow'
|
||||
: 'border border-[#1C2B4A] text-[#1C2B4A] hover:bg-[#1C2B4A]/5'
|
||||
}`}
|
||||
>
|
||||
For myself
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsForSelf(false)}
|
||||
className={`flex-1 rounded-md px-3 py-2 text-sm font-medium transition ${
|
||||
!isForSelf
|
||||
? 'bg-[#1C2B4A] text-white shadow'
|
||||
: 'border border-[#1C2B4A] text-[#1C2B4A] hover:bg-[#1C2B4A]/5'
|
||||
}`}
|
||||
>
|
||||
For someone else
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{/* inputs translated */}
|
||||
<div>
|
||||
|
||||
@ -504,18 +504,18 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
||||
Personal Matrix
|
||||
</button>
|
||||
)}
|
||||
|
||||
{DISPLAY_ABONEMENTS && hasSubscribePerm && (
|
||||
<button
|
||||
onClick={() => router.push('/coffee-abonnements')}
|
||||
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||
>
|
||||
Coffee Abonnements
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{userPresent && DISPLAY_ABONEMENTS && hasSubscribePerm && (
|
||||
<button
|
||||
onClick={() => router.push('/coffee-abonnements')}
|
||||
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||
>
|
||||
Coffee Abonnements
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Information dropdown already removed here */}
|
||||
</PopoverGroup>
|
||||
|
||||
@ -737,16 +737,16 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
||||
Personal Matrix
|
||||
</button>
|
||||
)}
|
||||
{DISPLAY_ABONEMENTS && hasSubscribePerm && (
|
||||
<button
|
||||
onClick={() => { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
|
||||
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
|
||||
>
|
||||
Coffee Abonnements
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{DISPLAY_ABONEMENTS && hasSubscribePerm && (
|
||||
<button
|
||||
onClick={() => { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
|
||||
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
|
||||
>
|
||||
Coffee Abonnements
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Admin navigation – LAST */}
|
||||
{isAdmin && (
|
||||
|
||||
@ -23,8 +23,14 @@ export default function MediaSection({ documents }: { documents: any[] }) {
|
||||
<td className="px-4 py-2 text-gray-600">{doc.type}</td>
|
||||
<td className="px-4 py-2 text-gray-600">{doc.uploaded}</td>
|
||||
<td className="px-4 py-2 flex gap-2">
|
||||
<button className="px-3 py-1 text-xs bg-[#8D6B1D] text-white rounded hover:bg-[#7A5E1A] transition">Download</button>
|
||||
<button className="px-3 py-1 text-xs bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition">Preview</button>
|
||||
{doc.signedUrl ? (
|
||||
<>
|
||||
<a href={doc.signedUrl} download className="px-3 py-1 text-xs bg-[#8D6B1D] text-white rounded hover:bg-[#7A5E1A] transition">Download</a>
|
||||
<a href={doc.signedUrl} target="_blank" rel="noopener noreferrer" className="px-3 py-1 text-xs bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition">Preview</a>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 italic">No file</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@ -184,7 +184,28 @@ export default function ProfilePage() {
|
||||
}
|
||||
}, [profileDataApi, user, progressPercent])
|
||||
|
||||
const documents = Array.isArray(mediaData?.documents) ? mediaData.documents : []
|
||||
const documents = React.useMemo(() => {
|
||||
const contracts = Array.isArray(mediaData?.contracts) ? mediaData.contracts : []
|
||||
const idDocs = Array.isArray(mediaData?.idDocuments) ? mediaData.idDocuments : []
|
||||
|
||||
const contractItems = contracts.map((doc: any) => ({
|
||||
id: `contract-${doc.id}`,
|
||||
name: doc.original_filename || 'Contract',
|
||||
type: 'Contract',
|
||||
uploaded: doc.created_at ? new Date(doc.created_at).toLocaleDateString() : '-',
|
||||
signedUrl: doc.signedUrl,
|
||||
}))
|
||||
|
||||
const idDocItems = idDocs.filter((d: any) => d.object_storage_id).map((doc: any) => ({
|
||||
id: `id-${doc.user_id_document_id}-${doc.side}`,
|
||||
name: doc.original_filename || `ID Document (${doc.side})`,
|
||||
type: `${doc.id_type || 'ID'} – ${doc.side}`,
|
||||
uploaded: doc.expiry_date ? new Date(doc.expiry_date).toLocaleDateString() : '-',
|
||||
signedUrl: doc.signedUrl,
|
||||
}))
|
||||
|
||||
return [...contractItems, ...idDocItems]
|
||||
}, [mediaData])
|
||||
|
||||
useEffect(() => {
|
||||
if (showRefreshing && !profileLoading && !mediaLoading && !completionLoading) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user