Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
228 lines
7.1 KiB
TypeScript
228 lines
7.1 KiB
TypeScript
import type { FollowResponseDto } from '@/api/generated/models'
|
|
import {
|
|
createServiceError,
|
|
errorResult,
|
|
handleServiceError,
|
|
type ServiceCallOptions,
|
|
type ServiceResult,
|
|
shouldBubbleErrorToParent,
|
|
successResult,
|
|
} from '@/services/errorHandler'
|
|
import { unwrapApiData } from '@/services/apiResponse'
|
|
import { getOrganizerFollows } from '@/api/generated/organizer-follows/organizer-follows'
|
|
import { parseRemittanceList } from '@/helpers/listResponse'
|
|
import { texts } from '@/texts'
|
|
|
|
export interface FollowUserSummary {
|
|
/** Omitted by public follower-list responses. */
|
|
mobile?: string
|
|
firstName: string | null
|
|
lastName: string | null
|
|
avatarUrl?: string | null
|
|
gender?: 'male' | 'female' | 'other' | null
|
|
}
|
|
|
|
export type FollowedOrganizer = Omit<FollowResponseDto, 'user'> & {
|
|
user?: FollowUserSummary
|
|
}
|
|
|
|
const organizerFollowsApi = getOrganizerFollows()
|
|
|
|
/**
|
|
* `GET /organizers/:id/is-following` — a lightweight boolean check, no
|
|
* `notifyNewEvents` in the response. Per docs/workflows/fa/organizer-follow.md,
|
|
* a freshly-created (or reactivated) follow always starts with
|
|
* notifyNewEvents=true on the backend, so callers show the bell "on" by
|
|
* default; the real value only matters after the user has actually
|
|
* toggled it in this same mounted session (handled client-side, not
|
|
* re-fetched here — see docs/workflows/fa/organizer-follow.md).
|
|
*/
|
|
export const IS_FOLLOWING_ORGANIZER = async (
|
|
organizerId: string,
|
|
options?: ServiceCallOptions
|
|
): Promise<ServiceResult<{ isFollowing: boolean }>> => {
|
|
try {
|
|
const res = await organizerFollowsApi.organizerFollowsControllerIsFollowing(organizerId)
|
|
const raw = res.data
|
|
|
|
if ('success' in raw && !raw.success) {
|
|
throw createServiceError(raw.message || texts.profile.fetchFollowStatusFailed)
|
|
}
|
|
|
|
// This endpoint isn't wrapped in the usual { success, data } envelope
|
|
// (see organizer-follows.controller.ts), but tolerate either shape.
|
|
const isFollowing = 'isFollowing' in raw ? Boolean(raw.isFollowing) : Boolean(unwrapApiData(raw)?.isFollowing)
|
|
|
|
return successResult({ isFollowing })
|
|
} catch (error) {
|
|
const normalizedError = handleServiceError(error, options)
|
|
|
|
if (shouldBubbleErrorToParent(options)) {
|
|
throw normalizedError
|
|
}
|
|
|
|
return errorResult(normalizedError)
|
|
}
|
|
}
|
|
|
|
export const FOLLOW_ORGANIZER = async (organizerId: string, options?: ServiceCallOptions): Promise<ServiceResult<FollowedOrganizer>> => {
|
|
try {
|
|
const res = await organizerFollowsApi.organizerFollowsControllerFollow(organizerId)
|
|
const payload = res.data
|
|
|
|
if (!payload.success) {
|
|
throw createServiceError(payload.message || texts.profile.followFailed)
|
|
}
|
|
|
|
const follow = unwrapApiData(payload)
|
|
|
|
if (!follow) {
|
|
throw createServiceError(texts.common.invalidServerResponse)
|
|
}
|
|
|
|
return successResult(follow)
|
|
} catch (error) {
|
|
const normalizedError = handleServiceError(error, options)
|
|
|
|
if (shouldBubbleErrorToParent(options)) {
|
|
throw normalizedError
|
|
}
|
|
|
|
return errorResult(normalizedError)
|
|
}
|
|
}
|
|
|
|
export const UNFOLLOW_ORGANIZER = async (
|
|
organizerId: string,
|
|
options?: ServiceCallOptions
|
|
): Promise<ServiceResult<{ success: boolean }>> => {
|
|
try {
|
|
const res = await organizerFollowsApi.organizerFollowsControllerUnfollow(organizerId)
|
|
const payload = res.data
|
|
|
|
if (!payload.success) {
|
|
throw createServiceError(payload.message || texts.profile.unfollowFailed)
|
|
}
|
|
|
|
return successResult({ success: true })
|
|
} catch (error) {
|
|
const normalizedError = handleServiceError(error, options)
|
|
|
|
if (shouldBubbleErrorToParent(options)) {
|
|
throw normalizedError
|
|
}
|
|
|
|
return errorResult(normalizedError)
|
|
}
|
|
}
|
|
|
|
export const UPDATE_FOLLOW_SETTINGS = async (
|
|
organizerId: string,
|
|
notifyNewEvents: boolean,
|
|
options?: ServiceCallOptions
|
|
): Promise<ServiceResult<FollowedOrganizer>> => {
|
|
try {
|
|
const res = await organizerFollowsApi.organizerFollowsControllerUpdateFollowSettings(organizerId, {
|
|
notifyNewEvents,
|
|
})
|
|
const payload = res.data
|
|
|
|
if (!payload.success) {
|
|
throw createServiceError(payload.message || texts.profile.updateNotifyFailed)
|
|
}
|
|
|
|
const follow = unwrapApiData(payload)
|
|
|
|
if (!follow) {
|
|
throw createServiceError(texts.common.invalidServerResponse)
|
|
}
|
|
|
|
return successResult(follow)
|
|
} catch (error) {
|
|
const normalizedError = handleServiceError(error, options)
|
|
|
|
if (shouldBubbleErrorToParent(options)) {
|
|
throw normalizedError
|
|
}
|
|
|
|
return errorResult(normalizedError)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `GET /organizers/me/following` — paginated. Used both for the
|
|
* `/profile/following` list (page-by-page via `page`) and for the
|
|
* MenuRow badge count (pageSize=1, only `pagination.totalItemsCount` is
|
|
* read — see GET_FOLLOWING_COUNT below).
|
|
*/
|
|
export const GET_FOLLOWING = async (
|
|
page: number,
|
|
pageSize: number,
|
|
options?: ServiceCallOptions
|
|
): Promise<ServiceResult<{ items: FollowedOrganizer[]; totalItemsCount: number }>> => {
|
|
try {
|
|
const res = await organizerFollowsApi.organizerFollowMeControllerGetFollowing({ page, pageSize, sort: '-createdAt' })
|
|
const payload = res.data
|
|
|
|
if (!payload.success) {
|
|
throw createServiceError(payload.message || texts.profile.fetchFollowingFailed)
|
|
}
|
|
|
|
const { items, pagination } = parseRemittanceList<FollowedOrganizer>(unwrapApiData(payload) ?? {}, 'items', page, pageSize)
|
|
|
|
return successResult({ items, totalItemsCount: pagination.totalItemsCount })
|
|
} catch (error) {
|
|
const normalizedError = handleServiceError(error, options)
|
|
|
|
if (shouldBubbleErrorToParent(options)) {
|
|
throw normalizedError
|
|
}
|
|
|
|
return errorResult(normalizedError)
|
|
}
|
|
}
|
|
|
|
/** Cheap count-only call for the profile MenuRow badge — pageSize=1, discards items. */
|
|
export const GET_FOLLOWING_COUNT = async (options?: ServiceCallOptions): Promise<ServiceResult<number>> => {
|
|
const result = await GET_FOLLOWING(1, 1, options)
|
|
|
|
if (!result.ok) {
|
|
return result
|
|
}
|
|
|
|
return successResult(result.data.totalItemsCount)
|
|
}
|
|
|
|
/**
|
|
* `GET /organizers/me/followers` — users following the current user as an
|
|
* organizer/host. Only meaningful for verified hosts (see
|
|
* `HostCapabilityService.isVerifiedHost` — non-hosts simply have no
|
|
* followers, so this returns an empty list rather than erroring).
|
|
*/
|
|
export const GET_FOLLOWERS = async (
|
|
page: number,
|
|
pageSize: number,
|
|
options?: ServiceCallOptions
|
|
): Promise<ServiceResult<{ items: FollowedOrganizer[]; totalItemsCount: number }>> => {
|
|
try {
|
|
const res = await organizerFollowsApi.organizerFollowMeControllerGetFollowers({ page, pageSize, sort: '-createdAt' })
|
|
const payload = res.data
|
|
|
|
if (!payload.success) {
|
|
throw createServiceError(payload.message || texts.profile.fetchFollowersFailed)
|
|
}
|
|
|
|
const { items, pagination } = parseRemittanceList<FollowedOrganizer>(unwrapApiData(payload) ?? {}, 'items', page, pageSize)
|
|
|
|
return successResult({ items, totalItemsCount: pagination.totalItemsCount })
|
|
} catch (error) {
|
|
const normalizedError = handleServiceError(error, options)
|
|
|
|
if (shouldBubbleErrorToParent(options)) {
|
|
throw normalizedError
|
|
}
|
|
|
|
return errorResult(normalizedError)
|
|
}
|
|
}
|