admin/services/userReports.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

132 lines
3.8 KiB
TypeScript

import type { CreateUserReportDto, ReportReasonResponseDto, UserReportResponseDto } from '@/api/generated/models'
import {
createServiceError,
errorResult,
handleServiceError,
type ServiceCallOptions,
type ServiceResult,
shouldBubbleErrorToParent,
successResult,
} from '@/services/errorHandler'
import { unwrapApiData } from '@/services/apiResponse'
import { getUserReports } from '@/api/generated/user-reports/user-reports'
import { texts } from '@/texts'
// docs/workflows/fa/user-blocking-reporting.md, Flow B — purely
// informational: creating a report never blocks, hides, or notifies
// anyone by itself. It only creates a `pending` row an admin reviews later.
export type ReportReason = ReportReasonResponseDto
export type UserReport = UserReportResponseDto
const userReportsApi = getUserReports()
/**
* Public, unauthenticated — safe to fetch ahead of time (e.g. when the
* report menu item is rendered) so the reason list is ready by the time
* the user opens the report form.
*/
export const LIST_REPORT_REASONS = async (options?: ServiceCallOptions): Promise<ServiceResult<ReportReason[]>> => {
try {
const res = await userReportsApi.userReportsControllerListReportReasons()
const payload = res.data
if (!payload.success) {
throw createServiceError(payload.message || texts.chats.listReportReasonsFailed)
}
return successResult(unwrapApiData(payload) ?? [])
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}
/**
* At least one of `reasonId` / `description` is required — mirrors the
* backend's `CreateUserReportDto` cross-field rule
* (`user_reports_content_chk`).
*/
export const REPORT_USER = async (
reportedId: string,
payload: { reasonId?: number; description?: string },
options?: ServiceCallOptions
): Promise<ServiceResult<UserReport>> => {
try {
const res = await userReportsApi.userReportsControllerCreate({
reportedId,
...payload,
})
const responsePayload = res.data
if (!responsePayload.success) {
throw createServiceError(responsePayload.message || texts.chats.submitReportFailed, responsePayload.code)
}
const report = unwrapApiData(responsePayload)
if (!report?.id) {
throw createServiceError(texts.common.invalidServerResponse)
}
return successResult(report)
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}
/**
* Reports one specific chat message. The server derives and persists the
* conversation, sender, and immutable message snapshot; the client cannot
* forge those moderation fields.
*/
export const REPORT_MESSAGE = async (
reportedId: string,
messageId: string,
payload: { reasonId?: number; description?: string },
options?: ServiceCallOptions
): Promise<ServiceResult<UserReport>> => {
try {
const request: CreateUserReportDto = {
reportedId,
messageId,
...payload,
}
const res = await userReportsApi.userReportsControllerCreate(request)
const responsePayload = res.data
if (!responsePayload.success) {
throw createServiceError(responsePayload.message || texts.chats.submitReportFailed, responsePayload.code)
}
const report = unwrapApiData(responsePayload)
if (!report?.id) {
throw createServiceError(texts.common.invalidServerResponse)
}
return successResult(report)
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}