195 lines
8.3 KiB
TypeScript
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>
|
|
)
|
|
}
|