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

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)
}
}