profit-planet-frontend/src/app/admin/user-management/components/UserManagementTable.tsx
DeathKaioken 646c293bc1 .
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 06:22:10 +02:00

195 lines
8.3 KiB
TypeScript

'use client'
import { PencilSquareIcon } from '@heroicons/react/24/outline'
import type { UserRole, UserRow, UserStatus, UserType } from '../hooks/useUserManagementPageState'
import {
getUserStatusBadgeClass,
getUserStatusLabel,
getUserTypeBadgeClass,
getUserTypeLabel,
getUserRoleBadgeClass,
getUserRoleLabel,
type ManagedUserStatus,
type ManagedUserType,
type ManagedUserRole,
} from '../constants/userStatusPresentation'
type TFunction = (key: string) => string
type Props = {
t: TFunction
loading: boolean
users: UserRow[]
totalFiltered: number
page: number
totalPages: number
onPageChange: (nextPage: number) => void
onEdit: (id: string) => void
normalizeStatus: (status: string) => UserStatus
}
function badge(text: string, color: 'blue' | 'amber' | 'green' | 'gray' | 'rose' | 'indigo' | 'violet') {
const base = 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-wide'
const colorMap: Record<string, string> = {
blue: 'bg-sky-100 text-sky-700',
amber: 'bg-amber-100 text-amber-700',
green: 'bg-green-100 text-green-700',
gray: 'bg-slate-100 text-slate-700',
rose: 'bg-rose-100 text-rose-700',
indigo: 'bg-indigo-100 text-indigo-700',
violet: 'bg-violet-100 text-violet-700',
}
return <span className={`${base} ${colorMap[color]}`}>{text}</span>
}
function statusBadge(status: UserStatus, t: TFunction) {
if (status === 'disabled') return badge(t('autofix.k2fc06d90'), 'gray')
const managedStatus = status as ManagedUserStatus
return (
<span
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-wide border ${getUserStatusBadgeClass(managedStatus)}`}
>
{getUserStatusLabel(t, managedStatus)}
</span>
)
}
function typeBadge(type: UserType, t: TFunction) {
const managedType = type as ManagedUserType
return (
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-wide ${getUserTypeBadgeClass(managedType)}`}>
{getUserTypeLabel(t, managedType)}
</span>
)
}
function roleBadge(role: UserRole, t: TFunction) {
const managedRole = role as ManagedUserRole
return (
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-wide ${getUserRoleBadgeClass(managedRole)}`}>
{getUserRoleLabel(t, managedRole)}
</span>
)
}
export default function UserManagementTable({
t,
loading,
users,
totalFiltered,
page,
totalPages,
onPageChange,
onEdit,
normalizeStatus,
}: Props) {
return (
<section className="rounded-[28px] border border-white/80 bg-white/90 p-8 shadow-[0_24px_70px_-40px_rgba(15,23,42,0.3)] backdrop-blur space-y-4">
<div className="flex items-center justify-between gap-3">
<h2 className="text-lg font-semibold text-slate-900">{t('autofix.k10ccb626')}</h2>
<div className="text-xs text-slate-500">
{t('autofix.k2e41c8dc').replace('{current}', String(users.length)).replace('{total}', String(totalFiltered))}
</div>
</div>
<div className="overflow-x-auto rounded-2xl border border-slate-200/70 bg-white/70 p-1">
<table className="min-w-full text-sm rounded-xl overflow-hidden">
<thead>
<tr className="bg-slate-50 text-left text-slate-900">
<th className="px-4 py-3 font-semibold">{t('autofix.k91f49568')}</th>
<th className="px-4 py-3 font-semibold">{t('autofix.kec4fe9ef')}</th>
<th className="px-4 py-3 font-semibold">{t('autofix.k81c0b74b')}</th>
<th className="px-4 py-3 font-semibold">{t('autofix.ked760737')}</th>
<th className="px-4 py-3 font-semibold">{t('autofix.kf123704b')}</th>
<th className="px-4 py-3 font-semibold">{t('autofix.kb24782ec')}</th>
<th className="px-4 py-3 font-semibold">{t('autofix.k0afbbac4')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{loading ? (
<tr>
<td colSpan={7} className="px-4 py-10 text-center">
<div className="flex items-center justify-center gap-2 text-slate-700">
<div className="h-4 w-4 rounded-full border-2 border-slate-900 border-b-transparent animate-spin" />
<span>{t('autofix.k7fa2c4af')}</span>
</div>
</td>
</tr>
) : users.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-10 text-center text-sm text-slate-600">{t('autofix.k748bf541')}</td>
</tr>
) : (
users.map((user) => {
const displayName = user.user_type === 'company'
? user.company_name || t('autofix.k835f1c86')
: `${user.first_name || t('autofix.k76870ea8')} ${user.last_name || t('autofix.k2bf5e6ec')}`
const initials = user.user_type === 'company'
? (user.company_name?.[0] || 'C').toUpperCase()
: `${user.first_name?.[0] || 'U'}${user.last_name?.[0] || 'U'}`.toUpperCase()
const createdDate = new Date(user.created_at).toLocaleDateString()
const lastLoginDate = user.last_login_at ? new Date(user.last_login_at).toLocaleDateString() : t('autofix.k768f3f4a')
const normalizedStatus = normalizeStatus(user.status)
return (
<tr key={user.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-4">
<div className="flex items-center gap-3">
<div className="h-9 w-9 flex items-center justify-center rounded-full bg-gradient-to-br from-slate-900 to-slate-700 text-white text-xs font-semibold shadow">
{initials}
</div>
<div>
<div className="font-medium text-slate-900 leading-tight">{displayName}</div>
<div className="text-[11px] text-slate-600">{user.email}</div>
</div>
</div>
</td>
<td className="px-4 py-4">{typeBadge(user.user_type, t)}</td>
<td className="px-4 py-4">{statusBadge(normalizedStatus, t)}</td>
<td className="px-4 py-4">{roleBadge(user.role, t)}</td>
<td className="px-4 py-4 text-slate-900">{createdDate}</td>
<td className="px-4 py-4 text-slate-600 italic">{lastLoginDate}</td>
<td className="px-4 py-4">
<button
onClick={() => onEdit(user.id.toString())}
className="inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-slate-50 hover:bg-slate-100 text-slate-900 px-3 py-2 text-xs font-medium transition"
>
<PencilSquareIcon className="h-4 w-4" />
{t('autofix.k9f8d7a4f')}
</button>
</td>
</tr>
)
})
)}
</tbody>
</table>
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
<div className="text-xs text-slate-600">
{t('autofix.kf03c39b7').replace('{page}', String(page)).replace('{pages}', String(totalPages)).replace('{total}', String(totalFiltered))}
</div>
<div className="flex gap-2">
<button
disabled={page === 1}
onClick={() => onPageChange(Math.max(1, page - 1))}
className="px-4 py-2 text-xs font-medium rounded-lg border border-slate-200 bg-white hover:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed"
>
{t('autofix.kdb27a82d')}
</button>
<button
disabled={page === totalPages}
onClick={() => onPageChange(Math.min(totalPages, page + 1))}
className="px-4 py-2 text-xs font-medium rounded-lg border border-slate-200 bg-white hover:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed"
>
{t('autofix.ka8ea17b8')}
</button>
</div>
</div>
</section>
)
}