Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
import type { PaginatedListMeta, ParsedRemittanceList } from '@/types'
|
|
|
|
type UnknownRecord = Record<string, unknown>
|
|
|
|
function isRecord(value: unknown): value is UnknownRecord {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
}
|
|
|
|
/**
|
|
* Unwraps the Ghabilee API envelope `{ success, data }` when present.
|
|
*
|
|
* `data` is usually a record (the paginated `{ items, response }` shape),
|
|
* but some endpoints (e.g. geography's flat provinces/cities lists)
|
|
* return a bare array — `isRecord` alone would reject that and this
|
|
* function would fall through to returning the *wrapper* itself, which
|
|
* no caller actually wants. Callers expecting a flat list (like
|
|
* services/geography.ts) already narrow with `Array.isArray(...)`
|
|
* themselves.
|
|
*/
|
|
// Typed unwrap is the public API for service callers; the param appears only in the return position by design.
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- intentional typed unwrap helper
|
|
export function unwrapApiPayload<T = UnknownRecord>(raw: unknown): T {
|
|
if (!isRecord(raw)) {
|
|
return {} as T
|
|
}
|
|
|
|
if (raw.success === true && (isRecord(raw.data) || Array.isArray(raw.data))) {
|
|
// Arrays are returned as-is for list endpoints; callers narrow with Array.isArray.
|
|
return raw.data as T
|
|
}
|
|
|
|
return raw as T
|
|
}
|
|
|
|
/** Unwrap API envelope to unknown (record or array). Prefer for typed narrowing. */
|
|
export function unwrapApiDataUnknown(raw: unknown): unknown {
|
|
if (!isRecord(raw)) return raw
|
|
if (raw.success === true && 'data' in raw) return raw.data
|
|
|
|
return raw
|
|
}
|
|
|
|
function formatSortValue(value: unknown): string {
|
|
if (typeof value === 'string') return value
|
|
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value)
|
|
|
|
return ''
|
|
}
|
|
|
|
function toNumber(value: unknown, fallback: number): number {
|
|
const parsed = Number(value)
|
|
|
|
return Number.isFinite(parsed) ? parsed : fallback
|
|
}
|
|
|
|
function extractItems(payload: UnknownRecord, itemsKey: string): unknown[] {
|
|
const keyed = payload[itemsKey]
|
|
|
|
if (Array.isArray(keyed)) {
|
|
return keyed
|
|
}
|
|
|
|
if (Array.isArray(payload.items)) {
|
|
return payload.items
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
function extractListMeta(payload: UnknownRecord, fallbackPage: number, fallbackPageSize: number): PaginatedListMeta {
|
|
const response = isRecord(payload.response) ? payload.response : {}
|
|
const meta = isRecord(payload.meta) ? payload.meta : {}
|
|
|
|
const page = toNumber(response.page ?? meta.page, fallbackPage)
|
|
const pageSize = toNumber(response.pageSize ?? meta.limit ?? meta.pageSize, fallbackPageSize)
|
|
const totalItemsCount = toNumber(response.totalItemsCount ?? meta.total ?? meta.totalItemsCount, 0)
|
|
const totalPages = toNumber(response.totalPages ?? meta.total_page ?? meta.totalPages, Math.ceil(totalItemsCount / pageSize) || 0)
|
|
|
|
const filtersSource = response.filters ?? meta.filters
|
|
const filters = isRecord(filtersSource)
|
|
? Object.fromEntries(Object.entries(filtersSource).map(([key, value]) => [key, String(value)]))
|
|
: {}
|
|
|
|
return {
|
|
page,
|
|
pageSize,
|
|
totalItemsCount,
|
|
totalPages,
|
|
sort: formatSortValue(response.sort ?? meta.sort),
|
|
filters,
|
|
...(response.resultType != null ? { resultType: toNumber(response.resultType, 0) } : {}),
|
|
...(typeof response.fileData === 'string' ? { fileData: response.fileData } : {}),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses a paginated list response from the Ghabilee API (or legacy raw shape).
|
|
*/
|
|
export function parseRemittanceList<T = unknown>(
|
|
raw: unknown,
|
|
itemsKey = 'items',
|
|
fallbackPage = 1,
|
|
fallbackPageSize = 20
|
|
): ParsedRemittanceList<T> {
|
|
const payload = unwrapApiPayload(raw)
|
|
|
|
return {
|
|
items: extractItems(payload, itemsKey) as T[],
|
|
pagination: extractListMeta(payload, fallbackPage, fallbackPageSize),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Derives itemsKey from the last non-empty URL segment (e.g. .../users → users).
|
|
*/
|
|
export function getItemsKeyFromUrl(url: string): string {
|
|
const path = url.split('?')[0]?.replace(/\/+$/, '') ?? ''
|
|
const segments = path.split('/').filter(Boolean)
|
|
const last = segments[segments.length - 1]
|
|
|
|
return last || 'items'
|
|
}
|
|
|
|
/**
|
|
* Extracts base64 Excel payload from a list export response.
|
|
*/
|
|
export function extractExportFileData(raw: unknown): string | undefined {
|
|
const payload = unwrapApiPayload(raw)
|
|
const response = isRecord(payload.response) ? payload.response : undefined
|
|
const fileData = response?.fileData
|
|
|
|
return typeof fileData === 'string' && fileData.length > 0 ? fileData : undefined
|
|
}
|