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) { return Object.entries(registry) .filter(([, meta]) => meta.filterable) .map(([code, meta]) => ({ code, name: meta.label })) } function toStatusPresentation( registry: Record, 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 role — label, chip color, and table filter options. */ const USER_ROLE: Record = { user: { label: texts.status.userRole.user, chipColor: 'default', filterable: true }, admin: { label: texts.status.userRole.admin, chipColor: 'warning', filterable: true }, } /** User account status — label, chip color, and table filter options. */ const USER_ACCOUNT_STATUS: Record = { 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 = { 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 = { 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 = { 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_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 = { 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 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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' } }