admin/helpers/listResponse.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

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
}