import { api, BackendError, clearSessionToken, getSessionToken } from '@/services/backend'; export type AdminStats = { totalUsers: number; guestUsers: number; registeredUsers: number; pretestCompletedUsers: number; posttestCompletedUsers: number; trainingInProgressUsers: number; trainingCompletedUsers: number; courseCompletedUsers: number; totalAssessmentAttempts: number; totalChatSessions: number; totalChatMessages: number; programSessionCount: number; }; export type AdminUserListItem = { id: string; email: string | null; role: 'user' | 'admin'; isGuest: boolean; profileComplete: boolean; name: string | null; age: number | null; createdAt: string; lastActivityAt: string | null; currentStep: string | null; pretestCompleted: boolean; posttestCompleted: boolean; trainingCompleted: boolean; courseCompleted: boolean; completedSessionCount: number; programSessionCount: number; }; export type AdminUserListResult = { items: AdminUserListItem[]; pagination: { page: number; pageSize: number; total: number; totalPages: number; }; }; export type AdminUsersQuery = { q?: string; page?: number; pageSize?: number; sortBy?: 'createdAt' | 'role' | 'externalId' | 'lastActivityAt'; sortDir?: 'asc' | 'desc'; }; function toQueryString(params: AdminUsersQuery): string { const search = new URLSearchParams(); if (params.q) search.set('q', params.q); if (params.page) search.set('page', String(params.page)); if (params.pageSize) search.set('pageSize', String(params.pageSize)); if (params.sortBy) search.set('sortBy', params.sortBy); if (params.sortDir) search.set('sortDir', params.sortDir); const qs = search.toString(); return qs ? `?${qs}` : ''; } export function fetchAdminStats(): Promise { return api.get('/admin/stats'); } export function fetchAdminUsers(query: AdminUsersQuery = {}): Promise { return api.get(`/admin/users${toQueryString(query)}`); } export type AdminAssignmentSummary = { total: number; notStarted: number; opened: number; inProgress: number; completed: number; }; export type AdminUserDetail = { id: string; email: string | null; role: 'user' | 'admin'; isGuest: boolean; profileComplete: boolean; name: string | null; age: number | null; createdAt: string; lastActivityAt: string | null; currentStep: string | null; permissions: { canTakePreTest: boolean; canOpenTraining: boolean; canTakePostTest: boolean; isCompleted: boolean; } | null; pretestCompleted: boolean; posttestCompleted: boolean; trainingStarted: boolean; trainingCompleted: boolean; courseCompleted: boolean; completedSessionCount: number; programSessionCount: number; preTestCompletedAt: string | null; trainingStartedAt: string | null; trainingCompletedAt: string | null; postTestCompletedAt: string | null; hasPretestAttempt: boolean; hasPosttestAttempt: boolean; chatSessionCount: number; assignments: AdminAssignmentSummary; }; export function fetchAdminUserDetail(externalId: string): Promise { return api.get(`/admin/users/${encodeURIComponent(externalId)}`); } export type AdminUserOverview = { user: { id: string; email: string | null }; assignments: { total: number; completed: number; inProgress: number; notStarted: number; reported: number; opened: number; progressPercent: number | null; lastActivityAt: string | null; }; learningProgress: { currentStep: string | null; trainingStarted: boolean; trainingCompleted: boolean; courseCompleted: boolean; totalLessons: number; completedLessons: number; progressPercent: number | null; lastActivityAt: string | null; }; assessments: { completedCount: number; expectedCount: number; remainingCount: number; hasPretest: boolean; hasPosttest: boolean; pretestCompletedAt: string | null; posttestCompletedAt: string | null; lastAssessmentAt: string | null; overallStatus: 'none' | 'partial' | 'complete'; }; recentActivities: Array<{ type: | 'session_completed' | 'assignment_activated' | 'assignment_completed' | 'assessment_submitted' | 'chat_started'; title: string; occurredAt: string; relatedId: string | null; }>; }; export function fetchAdminUserOverview(externalId: string): Promise { return api.get( `/admin/users/${encodeURIComponent(externalId)}/overview`, ); } export type AdminAssessmentScores = { total: number | null; efficacy: number | null; hope: number | null; resilience: number | null; optimism: number | null; }; export type AdminAssessmentAnswer = { questionId: string; questionNumber: number; rawValue: number; points: number; dimension: string; isReverseScored: boolean; }; export type AdminAssessmentPhaseView = { exists: boolean; phase: 'pre' | 'post'; attemptId: string | null; status: string | null; submittedAt: string | null; scoredAt: string | null; scores: AdminAssessmentScores | null; answerCount: number; expectedAnswerCount: number; answers: AdminAssessmentAnswer[]; }; export type AdminUserAssessments = { user: { id: string; email: string | null }; pretest: AdminAssessmentPhaseView; posttest: AdminAssessmentPhaseView; }; export function fetchAdminUserAssessments(externalId: string): Promise { return api.get( `/admin/users/${encodeURIComponent(externalId)}/assessments`, ); } export type AdminChatSessionListItem = { id: string; sessionOrder: number; title: string; status: 'open' | 'ended'; startedAt: string; endedAt: string | null; createdAt: string; updatedAt: string; lastActivityAt: string; messageCount: number; }; export type AdminChatSessionsListResult = { user: { id: string; email: string | null }; items: AdminChatSessionListItem[]; pagination: { page: number; pageSize: number; total: number; totalPages: number; }; }; export type AdminChatMessageView = { id: string; role: 'user' | 'assistant' | 'system'; content: string; createdAt: string; sequence: number; }; export type AdminChatSessionDetail = { user: { id: string; email: string | null }; session: AdminChatSessionListItem; messages: AdminChatMessageView[]; }; export type AdminChatSessionsQuery = { page?: number; pageSize?: number; sortBy?: 'startedAt' | 'updatedAt' | 'sessionOrder' | 'createdAt'; sortDir?: 'asc' | 'desc'; }; function chatSessionsQueryString(params: AdminChatSessionsQuery): string { const search = new URLSearchParams(); if (params.page) search.set('page', String(params.page)); if (params.pageSize) search.set('pageSize', String(params.pageSize)); if (params.sortBy) search.set('sortBy', params.sortBy); if (params.sortDir) search.set('sortDir', params.sortDir); const qs = search.toString(); return qs ? `?${qs}` : ''; } export function fetchAdminUserChatSessions( externalId: string, query: AdminChatSessionsQuery = {}, ): Promise { return api.get( `/admin/users/${encodeURIComponent(externalId)}/chat-sessions${chatSessionsQueryString(query)}`, ); } export function fetchAdminUserChatSessionDetail( externalId: string, sessionId: string, ): Promise { return api.get( `/admin/users/${encodeURIComponent(externalId)}/chat-sessions/${encodeURIComponent(sessionId)}`, ); } export type AdminAssignmentListItem = { assignmentId: string; definitionId: string; definitionKey: string; definitionTitle: string; assignmentType: string; programId: null; programTitle: null; sessionId: string | null; sessionOrder: number; sessionTitle: string | null; status: string; assignedAt: string; startedAt: string | null; completedAt: string | null; lastActivityAt: string | null; doneDayCount: number; expectedDurationDays: number | null; progressPercent: number | null; }; export type AdminAssignmentsSummary = { total: number; notStarted: number; opened: number; inProgress: number; completed: number; pending: number; progressPercent: number | null; }; export type AdminLearningProgress = { currentStep: string | null; trainingStarted: boolean; trainingCompleted: boolean; courseCompleted: boolean; totalLessons: number; completedLessons: number; progressPercent: number | null; }; export type AdminUserAssignmentsResult = { user: { id: string; email: string | null }; learningProgress: AdminLearningProgress; summary: AdminAssignmentsSummary; items: AdminAssignmentListItem[]; }; export type AdminAssignmentsQuery = { sortBy?: 'activatedAt' | 'completedAt' | 'status' | 'sessionOrder'; sortDir?: 'asc' | 'desc'; status?: 'NOT_STARTED' | 'ACTIVE' | 'IN_PROGRESS' | 'REPORTED' | 'COMPLETED'; assignmentType?: 'ONE_TIME' | 'DAILY' | 'ONGOING'; search?: string; }; function assignmentsQueryString(params: AdminAssignmentsQuery): string { const search = new URLSearchParams(); if (params.sortBy) search.set('sortBy', params.sortBy); if (params.sortDir) search.set('sortDir', params.sortDir); if (params.status) search.set('status', params.status); if (params.assignmentType) search.set('assignmentType', params.assignmentType); if (params.search) search.set('search', params.search); const qs = search.toString(); return qs ? `?${qs}` : ''; } export function fetchAdminUserAssignments( externalId: string, query: AdminAssignmentsQuery = {}, ): Promise { return api.get( `/admin/users/${encodeURIComponent(externalId)}/assignments${assignmentsQueryString(query)}`, ); } export type AdminAssignmentDetail = { user: { id: string; email: string | null }; assignment: { id: string; definitionId: string; definitionKey: string; definitionTitle: string; assignmentType: string; status: string; assignedAt: string; startedAt: string | null; completedAt: string | null; lastActivityAt: string | null; doneDayCount: number; expectedDurationDays: number | null; progressPercent: number | null; }; session: { id: string | null; order: number; title: string; }; learningProgress: { currentStep: string | null; totalLessons: number; completedLessons: number; progressPercent: number | null; }; activity: { firstActivityAt: string | null; lastActivityAt: string | null; completedActivityCount: number; } | null; }; export function fetchAdminUserAssignmentDetail( externalId: string, assignmentId: string, ): Promise { return api.get( `/admin/users/${encodeURIComponent(externalId)}/assignments/${encodeURIComponent(assignmentId)}`, ); } function parseContentDispositionFilename(header: string | null): string | null { if (!header) return null; const utf8 = /filename\*=UTF-8''([^;]+)/i.exec(header); if (utf8?.[1]) { try { return decodeURIComponent(utf8[1].trim().replace(/^"|"$/g, '')); } catch { // fall through } } const plain = /filename="?([^";]+)"?/i.exec(header); if (!plain?.[1]) return null; const name = plain[1].trim(); if (!name || /[\\/\n\r"]/.test(name)) return null; return name; } function triggerBrowserDownload(blob: Blob, filename: string): void { const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; anchor.rel = 'noopener'; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); } /** * Binary XLSX download — bypasses JSON api.get helper. */ export async function downloadAdminUserAssignmentsExport( externalId: string, query: AdminAssignmentsQuery = {}, ): Promise { const path = `/admin/users/${encodeURIComponent(externalId)}/assignments/export${assignmentsQueryString(query)}`; const baseUrl = (import.meta.env.VITE_BACKEND_URL || '/api/backend').replace(/\/$/, ''); const token = getSessionToken(); const headers = new Headers(); if (token) { headers.set('Authorization', `Bearer ${token}`); } let response: Response; try { response = await fetch(`${baseUrl}${path}`, { method: 'GET', headers }); } catch { throw new BackendError( 0, 'NETWORK_ERROR', 'سرور در دسترس نیست. لطفاً چند لحظه بعد دوباره تلاش کنید.', ); } if (!response.ok) { let code = 'UNKNOWN'; let message = `Request failed with status ${response.status}`; try { const body = (await response.json()) as { error?: { code?: string; message?: string }; }; code = body.error?.code ?? code; message = body.error?.message ?? message; } catch { // non-JSON error body } if (response.status === 401) { clearSessionToken(); if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('novya:unauthorized')); } } throw new BackendError(response.status, code, message); } const blob = await response.blob(); const fromHeader = parseContentDispositionFilename( response.headers.get('Content-Disposition'), ); const safeFallback = `noya-assignments-${externalId.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64) || 'user'}.xlsx`; triggerBrowserDownload(blob, fromHeader ?? safeFallback); }