feat: add guest role to user management and update statistics display

This commit is contained in:
Seazn 2026-03-15 14:18:27 +01:00
parent 60057b6c94
commit adfe136d74
5 changed files with 81 additions and 25 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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 && (

View File

@ -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>
))}

View File

@ -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) {