admin/types/status.ts
alisaza 280eb14ee6 feat(users): expand admin users table with city, gender, wallet, and roles
Show city, gender, wallet balance, and guest/host/admin userType with
matching filters while keeping identity status for host verification.
2026-09-07 11:29:29 +03:30

369 lines
17 KiB
TypeScript

import type { IdentityVerificationStatus, UserRole, UserStatus } from '@/api/generated/models'
import { texts } from '@/texts'
export type ChipColor = 'success' | 'warning' | 'danger' | 'default'
interface StatusMeta {
label: string
chipColor: ChipColor
filterable?: boolean
}
export interface StatusPresentation {
label: string
chipColor: ChipColor
}
function toFilterItems(registry: Record<string, { label: string; filterable?: boolean }>) {
return Object.entries(registry)
.filter(([, meta]) => meta.filterable)
.map(([code, meta]) => ({ code, name: meta.label }))
}
function toStatusPresentation(
registry: Record<string, StatusMeta>,
status?: string,
fallback: StatusPresentation = { label: texts.status.empty, chipColor: 'default' }
): StatusPresentation {
if (!status) return fallback
const meta = registry[status]
if (!meta) return fallback
return { label: meta.label, chipColor: meta.chipColor }
}
/** User gender — label and table filter options. */
const USER_GENDER: Record<'male' | 'female' | 'other', StatusMeta> = {
male: { label: texts.common.genderMale, chipColor: 'default', filterable: true },
female: { label: texts.common.genderFemale, chipColor: 'default', filterable: true },
other: { label: texts.profile.genderOther, chipColor: 'default', filterable: true },
}
/** User role — label, chip color, and table filter options. */
const USER_ROLE: Record<UserRole, StatusMeta> = {
user: { label: texts.status.userRole.user, chipColor: 'default', filterable: true },
admin: { label: texts.status.userRole.admin, chipColor: 'warning', filterable: true },
}
/** Admin list derived type — guest / verified host / staff admin. */
export type AdminUserType = 'guest' | 'host' | 'admin'
const USER_TYPE: Record<AdminUserType, StatusMeta> = {
guest: { label: texts.status.userType.guest, chipColor: 'default', filterable: true },
host: { label: texts.status.userType.host, chipColor: 'success', filterable: true },
admin: { label: texts.status.userType.admin, chipColor: 'warning', filterable: true },
}
/** User account status — label, chip color, and table filter options. */
const USER_ACCOUNT_STATUS: Record<UserStatus, StatusMeta> = {
active: { label: texts.status.userAccount.active, chipColor: 'success', filterable: true },
pending: { label: texts.status.userAccount.pending, chipColor: 'warning' },
suspended: { label: texts.status.userAccount.suspended, chipColor: 'warning', filterable: true },
deleted: { label: texts.status.userAccount.deleted, chipColor: 'danger', filterable: true },
}
/** Identity verification status — label, chip color, and table filter options. */
const IDENTITY_STATUS: Record<IdentityVerificationStatus, StatusMeta> = {
none: { label: texts.status.identity.none, chipColor: 'default', filterable: true },
pending: { label: texts.status.identity.pending, chipColor: 'warning', filterable: true },
verified: { label: texts.status.identity.verified, chipColor: 'success', filterable: true },
rejected: { label: texts.status.identity.rejected, chipColor: 'danger', filterable: true },
}
/**
* Review moderation status — label, chip color, and table filter options.
* Not imported from `@/api/generated/models` like the other registries here:
* the admin reviews endpoints are new and haven't gone through `pnpm generate:api`
* yet (see docs/workflows/reviews.md, Implementation gap). Once that's run,
* this local type should be replaced with the generated `ReviewStatus` type.
*/
export type ReviewStatus = 'published' | 'hidden' | 'deleted'
const REVIEW_STATUS: Record<ReviewStatus, StatusMeta> = {
published: { label: texts.status.review.published, chipColor: 'success', filterable: true },
hidden: { label: texts.status.review.hidden, chipColor: 'warning', filterable: true },
deleted: { label: texts.status.review.deleted, chipColor: 'danger', filterable: true },
}
/**
* User report review status — label, chip color, and table filter options.
* Mirrors the Prisma `report_status` enum (`pending`, `reviewed`,
* `dismissed`) from the admin user-reports review queue.
*/
export type ReportStatus = 'pending' | 'reviewed' | 'dismissed'
const REPORT_STATUS: Record<ReportStatus, StatusMeta> = {
pending: { label: texts.status.report.pending, chipColor: 'warning', filterable: true },
reviewed: { label: texts.status.report.reviewed, chipColor: 'success', filterable: true },
dismissed: { label: texts.status.report.dismissed, chipColor: 'danger', filterable: true },
}
export const USER_ROLE_FILTER_ITEMS = toFilterItems(USER_ROLE)
export const USER_TYPE_FILTER_ITEMS = toFilterItems(USER_TYPE)
export const USER_GENDER_FILTER_ITEMS = toFilterItems(USER_GENDER)
export const USER_ACCOUNT_STATUS_FILTER_ITEMS = toFilterItems(USER_ACCOUNT_STATUS)
export const IDENTITY_STATUS_FILTER_ITEMS = toFilterItems(IDENTITY_STATUS)
/** Admin identity verification queue — excludes `none` (never submitted). */
export const IDENTITY_VERIFICATION_QUEUE_FILTER_ITEMS = IDENTITY_STATUS_FILTER_ITEMS.filter((item) => item.code !== 'none')
export const REVIEW_STATUS_FILTER_ITEMS = toFilterItems(REVIEW_STATUS)
export const REPORT_STATUS_FILTER_ITEMS = toFilterItems(REPORT_STATUS)
/** Event lifecycle status — label, chip color, and table filter options. */
export type EventStatus = 'draft' | 'pending_review' | 'published' | 'full' | 'rejected' | 'cancelled' | 'completed'
const EVENT_STATUS: Record<EventStatus, StatusMeta> = {
draft: { label: texts.status.event.draft, chipColor: 'default', filterable: true },
pending_review: { label: texts.status.event.pending_review, chipColor: 'warning', filterable: true },
published: { label: texts.status.event.published, chipColor: 'success', filterable: true },
full: { label: texts.status.event.full, chipColor: 'warning', filterable: true },
rejected: { label: texts.status.event.rejected, chipColor: 'danger', filterable: true },
cancelled: { label: texts.status.event.cancelled, chipColor: 'danger', filterable: true },
completed: { label: texts.status.event.completed, chipColor: 'default', filterable: true },
}
export const EVENT_STATUS_FILTER_ITEMS = toFilterItems(EVENT_STATUS)
export function getEventStatus(status?: EventStatus | string): StatusPresentation {
return toStatusPresentation(EVENT_STATUS, status)
}
export function getReviewStatus(status?: ReviewStatus | string): StatusPresentation {
return toStatusPresentation(REVIEW_STATUS, status)
}
export function getReportStatus(status?: ReportStatus | string): StatusPresentation {
return toStatusPresentation(REPORT_STATUS, status)
}
export function getUserRole(role?: UserRole | string): StatusPresentation {
return toStatusPresentation(USER_ROLE, role)
}
export function getUserRoleLabel(role?: UserRole | string): string {
return getUserRole(role).label
}
export function getUserType(userType?: AdminUserType | string | null): StatusPresentation {
return toStatusPresentation(USER_TYPE, userType ?? undefined)
}
export function getUserGender(gender?: string | null): StatusPresentation {
return toStatusPresentation(USER_GENDER, gender ?? undefined)
}
export function getUserAccountStatus(status?: UserStatus | string): StatusPresentation {
return toStatusPresentation(USER_ACCOUNT_STATUS, status)
}
export function getIdentityStatus(status?: IdentityVerificationStatus | string): StatusPresentation {
return toStatusPresentation(IDENTITY_STATUS, status)
}
/**
* Booking status — label and chip color.
* Not imported from `@/api/generated/models`: the admin user-detail endpoints
* are new and haven't gone through `pnpm generate:api` yet (see
* docs/workflows/admin-user-detail.md, Implementation status). Once that's
* run, this local type should be replaced with the generated `BookingStatus` type.
*/
export type BookingStatus = 'pending_payment' | 'confirmed' | 'cancelled' | 'expired' | 'refunded' | 'no_show'
const BOOKING_STATUS: Record<BookingStatus, StatusMeta> = {
pending_payment: { label: texts.status.booking.pending_payment, chipColor: 'warning', filterable: true },
confirmed: { label: texts.status.booking.confirmed, chipColor: 'success', filterable: true },
cancelled: { label: texts.status.booking.cancelled, chipColor: 'danger', filterable: true },
expired: { label: texts.status.booking.expired, chipColor: 'default', filterable: true },
refunded: { label: texts.status.booking.refunded, chipColor: 'default', filterable: true },
no_show: { label: texts.status.booking.no_show, chipColor: 'danger', filterable: true },
}
export function getBookingStatus(status?: BookingStatus | string): StatusPresentation {
return toStatusPresentation(BOOKING_STATUS, status)
}
/** Admin platform-wide bookings list filter — see GET /admin/bookings `status` filter (ADMIN_FILTER_KEYS). */
export const BOOKING_STATUS_FILTER_ITEMS = toFilterItems(BOOKING_STATUS)
/** Payment status — label and chip color. See BookingStatus comment above re: not-yet-generated. */
export type PaymentStatus = 'pending' | 'processing' | 'succeeded' | 'failed' | 'cancelled'
const PAYMENT_STATUS: Record<PaymentStatus, StatusMeta> = {
pending: { label: texts.status.payment.pending, chipColor: 'warning', filterable: true },
processing: { label: texts.status.payment.processing, chipColor: 'warning', filterable: true },
succeeded: { label: texts.status.payment.succeeded, chipColor: 'success', filterable: true },
failed: { label: texts.status.payment.failed, chipColor: 'danger', filterable: true },
cancelled: { label: texts.status.payment.cancelled, chipColor: 'default', filterable: true },
}
export const PAYMENT_STATUS_FILTER_ITEMS = toFilterItems(PAYMENT_STATUS)
export function getPaymentStatus(status?: PaymentStatus | string): StatusPresentation {
return toStatusPresentation(PAYMENT_STATUS, status)
}
/** Withdrawal request status — label and chip color. See BookingStatus comment above re: not-yet-generated. */
export type WithdrawalRequestStatus = 'pending' | 'processing' | 'completed' | 'rejected'
const WITHDRAWAL_REQUEST_STATUS: Record<WithdrawalRequestStatus, StatusMeta> = {
pending: { label: texts.status.withdrawal.pending, chipColor: 'warning', filterable: true },
processing: { label: texts.status.withdrawal.processing, chipColor: 'warning', filterable: true },
completed: { label: texts.status.withdrawal.completed, chipColor: 'success', filterable: true },
rejected: { label: texts.status.withdrawal.rejected, chipColor: 'danger', filterable: true },
}
export const WITHDRAWAL_REQUEST_STATUS_FILTER_ITEMS = toFilterItems(WITHDRAWAL_REQUEST_STATUS)
export function getWithdrawalRequestStatus(status?: WithdrawalRequestStatus | string): StatusPresentation {
return toStatusPresentation(WITHDRAWAL_REQUEST_STATUS, status)
}
/** Payment method — label, chip color, and table filter options. */
export type PaymentMethod = 'gateway' | 'wallet' | 'mixed'
const PAYMENT_METHOD: Record<PaymentMethod, StatusMeta> = {
gateway: { label: texts.status.paymentMethod.gateway, chipColor: 'default', filterable: true },
wallet: { label: texts.status.paymentMethod.wallet, chipColor: 'success', filterable: true },
mixed: { label: texts.status.paymentMethod.mixed, chipColor: 'warning', filterable: true },
}
export const PAYMENT_METHOD_FILTER_ITEMS = toFilterItems(PAYMENT_METHOD)
export function getPaymentMethod(method?: PaymentMethod | string): StatusPresentation {
return toStatusPresentation(PAYMENT_METHOD, method)
}
export function getPaymentMethodLabel(method?: PaymentMethod | string): string {
return getPaymentMethod(method).label
}
/**
* Settlement batch status — label and chip color. Matches the Prisma
* `settlement_batch_status` enum (backend/prisma/schema.prisma). Not
* imported from `@/api/generated/models`: same not-yet-generated
* situation as BookingStatus/PaymentStatus above.
*/
export type SettlementStatus = 'pending' | 'processing' | 'completed' | 'failed'
const SETTLEMENT_STATUS: Record<SettlementStatus, StatusMeta> = {
pending: { label: texts.status.settlement.pending, chipColor: 'warning', filterable: true },
processing: { label: texts.status.settlement.processing, chipColor: 'warning', filterable: true },
completed: { label: texts.status.settlement.completed, chipColor: 'success', filterable: true },
failed: { label: texts.status.settlement.failed, chipColor: 'danger', filterable: true },
}
export const SETTLEMENT_STATUS_FILTER_ITEMS = toFilterItems(SETTLEMENT_STATUS)
export function getSettlementStatus(status?: SettlementStatus | string): StatusPresentation {
return toStatusPresentation(SETTLEMENT_STATUS, status)
}
export type BankAccountVerificationStatus = 'pending_review' | 'approved' | 'rejected'
const BANK_ACCOUNT_VERIFICATION_STATUS: Record<BankAccountVerificationStatus, StatusMeta> = {
pending_review: { label: texts.status.bankAccount.pending_review, chipColor: 'warning', filterable: true },
approved: { label: texts.status.bankAccount.approved, chipColor: 'success', filterable: true },
rejected: { label: texts.status.bankAccount.rejected, chipColor: 'danger', filterable: true },
}
export const BANK_ACCOUNT_VERIFICATION_STATUS_FILTER_ITEMS = toFilterItems(BANK_ACCOUNT_VERIFICATION_STATUS)
export function getBankAccountVerificationStatus(status?: BankAccountVerificationStatus | string): StatusPresentation {
return toStatusPresentation(BANK_ACCOUNT_VERIFICATION_STATUS, status)
}
/**
* SMS delivery status — label, chip color, and table filter options.
* Not imported from `@/api/generated/models`: matches the `sms_status`
* Prisma enum in `backend/src/modules/notifications/dto/sms-message-response.dto.ts`
* (pending, sent, delivered, failed).
*/
export type SmsStatus = 'pending' | 'sent' | 'delivered' | 'failed'
const SMS_STATUS: Record<SmsStatus, StatusMeta> = {
pending: { label: texts.status.sms.pending, chipColor: 'warning', filterable: true },
sent: { label: texts.status.sms.sent, chipColor: 'default', filterable: true },
delivered: { label: texts.status.sms.delivered, chipColor: 'success', filterable: true },
failed: { label: texts.status.sms.failed, chipColor: 'danger', filterable: true },
}
export const SMS_STATUS_FILTER_ITEMS = toFilterItems(SMS_STATUS)
export function getSmsStatus(status?: SmsStatus | string): StatusPresentation {
return toStatusPresentation(SMS_STATUS, status)
}
/**
* Admin audit log HTTP method — label, chip color, and table filter options.
* Matches ALLOWED_FILTER_KEYS/method whitelist in
* backend/src/modules/admin-audit-logs/admin-audit-logs.service.ts
* (only mutating verbs are ever logged; GET is never captured).
*/
export type AuditLogMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'WS'
const AUDIT_LOG_METHOD: Record<AuditLogMethod, StatusMeta> = {
GET: { label: 'GET', chipColor: 'default', filterable: true },
WS: { label: 'WS', chipColor: 'default', filterable: true },
POST: { label: 'POST', chipColor: 'success', filterable: true },
PATCH: { label: 'PATCH', chipColor: 'warning', filterable: true },
PUT: { label: 'PUT', chipColor: 'warning', filterable: true },
DELETE: { label: 'DELETE', chipColor: 'danger', filterable: true },
}
export const AUDIT_LOG_METHOD_FILTER_ITEMS = toFilterItems(AUDIT_LOG_METHOD)
export function getAuditLogMethod(method?: string): StatusPresentation {
return toStatusPresentation(AUDIT_LOG_METHOD, method, { label: method ?? texts.status.empty, chipColor: 'default' })
}
/** HTTP response status code badge for admin audit logs. */
export function getHttpStatusCode(statusCode?: number | null): StatusPresentation {
if (statusCode == null || !Number.isFinite(statusCode) || statusCode <= 0) {
return { label: texts.status.empty, chipColor: 'default' }
}
const code = Math.trunc(statusCode)
let chipColor: ChipColor = 'default'
if (code >= 500) chipColor = 'danger'
else if (code >= 400) chipColor = 'warning'
else if (code >= 200) chipColor = 'success'
return { label: String(code), chipColor }
}
/** Boolean active / inactive badge (tags, categories, switches). */
export function getActiveStatus(isActive?: boolean | null): StatusPresentation {
if (isActive == null) return { label: texts.status.empty, chipColor: 'default' }
return isActive ? { label: texts.status.active, chipColor: 'success' } : { label: texts.status.inactive, chipColor: 'default' }
}
/** Boolean publish status (blog articles). */
export function getPublishStatus(isPublished?: boolean | null, scheduledAt?: string | null): StatusPresentation {
if (isPublished == null) return { label: texts.status.empty, chipColor: 'default' }
if (!isPublished && scheduledAt) return { label: texts.status.scheduled, chipColor: 'warning' }
return isPublished ? { label: texts.status.published, chipColor: 'success' } : { label: texts.status.draft, chipColor: 'default' }
}
/** Featured flag badge; returns null when not featured so callers can render "—". */
export function getFeaturedStatus(isFeatured?: boolean | null): StatusPresentation | null {
if (!isFeatured) return null
return { label: texts.status.featured, chipColor: 'warning' }
}
/** Yes / no boolean badge (e.g. isFree, isDiscoverable, isPublic). */
export function getBooleanStatus(
value?: boolean | null,
labels: { true: string; false: string } = { true: texts.status.yes, false: texts.status.no }
): StatusPresentation {
if (value == null) return { label: texts.status.empty, chipColor: 'default' }
return value ? { label: labels.true, chipColor: 'success' } : { label: labels.false, chipColor: 'default' }
}