Introduce pic_sort_order to persist custom user ordering Replace data table with a custom draggable grid layout Add API endpoint to handle bulk order updates
112 lines
2.5 KiB
TypeScript
112 lines
2.5 KiB
TypeScript
export const DEFAULT_USER_PASSWORD = '123456'
|
|
export const MIN_PASSWORD_LENGTH = 8
|
|
export const MIN_FULL_NAME_LENGTH = 2
|
|
export const USERNAME_PATTERN = /^[a-z0-9._-]{3,32}$/
|
|
export const PHONE_NUMBER_PATTERN = /^\+?\d{8,15}$/
|
|
export const DEFAULT_PHONE_COUNTRY_CODE = '+60'
|
|
|
|
export type UserRole = 'super_admin' | 'staff'
|
|
|
|
export function normalizeUsername(value: string) {
|
|
return value.trim().toLowerCase()
|
|
}
|
|
|
|
export function normalizeFullName(value: string) {
|
|
return value.trim()
|
|
}
|
|
|
|
export function normalizePhoneNumber(value: string) {
|
|
const trimmed = value.trim()
|
|
|
|
if (!trimmed) {
|
|
return ''
|
|
}
|
|
|
|
const hasPlusPrefix = trimmed.startsWith('+')
|
|
const digitsOnly = trimmed.replace(/\D/g, '')
|
|
|
|
if (!digitsOnly) {
|
|
return ''
|
|
}
|
|
|
|
if (hasPlusPrefix) {
|
|
return `+${digitsOnly}`
|
|
}
|
|
|
|
const defaultCountryDigits = DEFAULT_PHONE_COUNTRY_CODE.replace(/\D/g, '')
|
|
|
|
if (digitsOnly.startsWith(defaultCountryDigits)) {
|
|
return `+${digitsOnly}`
|
|
}
|
|
|
|
return `+${defaultCountryDigits}${digitsOnly.replace(/^0+/, '')}`
|
|
}
|
|
|
|
export function isValidPhoneNumber(value: string) {
|
|
return PHONE_NUMBER_PATTERN.test(normalizePhoneNumber(value))
|
|
}
|
|
|
|
export function isValidUsername(value: string) {
|
|
return USERNAME_PATTERN.test(normalizeUsername(value))
|
|
}
|
|
|
|
export function hasValidFullName(value: string) {
|
|
return normalizeFullName(value).length >= MIN_FULL_NAME_LENGTH
|
|
}
|
|
|
|
export function isUserRole(value: string | null | undefined): value is UserRole {
|
|
return value === 'super_admin' || value === 'staff'
|
|
}
|
|
|
|
export interface AuthUser {
|
|
id: string
|
|
username: string
|
|
fullName: string
|
|
phoneNumber: string | null
|
|
role: UserRole
|
|
picSortOrder: number
|
|
isActive: boolean
|
|
mustChangePassword: boolean
|
|
needsPasskeySetup: boolean
|
|
passkeyCount: number
|
|
createdAt: string
|
|
lastLoginAt: string | null
|
|
}
|
|
|
|
export interface ManagedUser extends AuthUser {
|
|
createdBy: string | null
|
|
}
|
|
|
|
export interface PublicContact {
|
|
id: string
|
|
fullName: string
|
|
phoneNumber: string
|
|
role: UserRole
|
|
picSortOrder: number
|
|
}
|
|
|
|
export interface PasskeySummary {
|
|
id: string
|
|
label: string
|
|
createdAt: string
|
|
lastUsedAt: string | null
|
|
deviceType: 'singleDevice' | 'multiDevice'
|
|
backedUp: boolean
|
|
}
|
|
|
|
export function needsUserOnboarding(
|
|
user: Pick<AuthUser, 'mustChangePassword'> | null | undefined
|
|
) {
|
|
return Boolean(user?.mustChangePassword)
|
|
}
|
|
|
|
export function getDefaultAuthenticatedPath(
|
|
user: Pick<AuthUser, 'role' | 'mustChangePassword'>
|
|
) {
|
|
if (needsUserOnboarding(user)) {
|
|
return '/security'
|
|
}
|
|
|
|
return user.role === 'super_admin' ? '/management/users' : '/bookings'
|
|
}
|