Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
343 lines
16 KiB
TypeScript
343 lines
16 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 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 },
|
|
}
|
|
|
|
/** 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_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 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' }
|
|
}
|