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 UserType = 'personal' | 'company'
|
||||||
type UserStatus = 'active' | 'pending' | 'disabled' | 'inactive' | 'suspended' | 'archived'
|
type UserStatus = 'active' | 'pending' | 'disabled' | 'inactive' | 'suspended' | 'archived'
|
||||||
type UserRole = 'user' | 'admin'
|
type UserRole = 'user' | 'admin' | 'guest'
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number
|
id: number
|
||||||
@ -32,7 +32,7 @@ interface User {
|
|||||||
|
|
||||||
const STATUSES: UserStatus[] = ['active','pending','disabled','inactive']
|
const STATUSES: UserStatus[] = ['active','pending','disabled','inactive']
|
||||||
const TYPES: UserType[] = ['personal','company']
|
const TYPES: UserType[] = ['personal','company']
|
||||||
const ROLES: UserRole[] = ['user','admin']
|
const ROLES: UserRole[] = ['user','admin','guest']
|
||||||
|
|
||||||
export default function AdminUserManagementPage() {
|
export default function AdminUserManagementPage() {
|
||||||
const { isAdmin } = useAdminUsers()
|
const { isAdmin } = useAdminUsers()
|
||||||
@ -122,6 +122,7 @@ export default function AdminUserManagementPage() {
|
|||||||
const stats = useMemo(() => ({
|
const stats = useMemo(() => ({
|
||||||
total: allUsers.length,
|
total: allUsers.length,
|
||||||
admins: allUsers.filter(u => u.role === 'admin').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,
|
personal: allUsers.filter(u => u.user_type === 'personal').length,
|
||||||
company: allUsers.filter(u => u.user_type === 'company').length,
|
company: allUsers.filter(u => u.user_type === 'company').length,
|
||||||
active: allUsers.filter(u => u.status === 'active').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')
|
t==='personal' ? badge('Personal','blue') : badge('Company','purple')
|
||||||
|
|
||||||
const roleBadge = (r: UserRole) =>
|
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
|
// Action handler for opening edit modal
|
||||||
const onEdit = (id: string) => {
|
const onEdit = (id: string) => {
|
||||||
@ -256,7 +257,7 @@ export default function AdminUserManagementPage() {
|
|||||||
|
|
||||||
{/* Statistic Section + Verify Button */}
|
{/* Statistic Section + Verify Button */}
|
||||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center gap-6">
|
<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="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-xs text-gray-500">Total Users</div>
|
||||||
<div className="text-xl font-semibold text-blue-900">{stats.total}</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-xs text-gray-500">Admins</div>
|
||||||
<div className="text-xl font-semibold text-indigo-700">{stats.admins}</div>
|
<div className="text-xl font-semibold text-indigo-700">{stats.admins}</div>
|
||||||
</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="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-xs text-gray-500">Personal</div>
|
||||||
<div className="text-xl font-semibold text-blue-700">{stats.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
|
Fill fields with logged in data
|
||||||
</button>
|
</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">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
{/* inputs translated */}
|
{/* inputs translated */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -504,18 +504,18 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
Personal Matrix
|
Personal Matrix
|
||||||
</button>
|
</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 */}
|
{/* Information dropdown already removed here */}
|
||||||
</PopoverGroup>
|
</PopoverGroup>
|
||||||
|
|
||||||
@ -737,16 +737,16 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
Personal Matrix
|
Personal Matrix
|
||||||
</button>
|
</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 */}
|
{/* Admin navigation – LAST */}
|
||||||
{isAdmin && (
|
{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.type}</td>
|
||||||
<td className="px-4 py-2 text-gray-600">{doc.uploaded}</td>
|
<td className="px-4 py-2 text-gray-600">{doc.uploaded}</td>
|
||||||
<td className="px-4 py-2 flex gap-2">
|
<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>
|
{doc.signedUrl ? (
|
||||||
<button className="px-3 py-1 text-xs bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition">Preview</button>
|
<>
|
||||||
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -184,7 +184,28 @@ export default function ProfilePage() {
|
|||||||
}
|
}
|
||||||
}, [profileDataApi, user, progressPercent])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (showRefreshing && !profileLoading && !mediaLoading && !completionLoading) {
|
if (showRefreshing && !profileLoading && !mediaLoading && !completionLoading) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user