fix financial and certificates
This commit is contained in:
+122
-32
@@ -2,64 +2,154 @@ import { apiUrl } from '../lib/apiOrigin'
|
||||
import { ApiHttpError, apiFetch, getStoredToken } from './http'
|
||||
import type { ApiEnvelope } from './types'
|
||||
|
||||
export type CertClassSection = {
|
||||
class_section_id: number
|
||||
class_section_name: string
|
||||
export type CertificateDecisionRow = {
|
||||
decision: string
|
||||
source: string
|
||||
notes: string
|
||||
year_score: number | null
|
||||
}
|
||||
|
||||
export type CertStudent = {
|
||||
id: number
|
||||
export type CertificateStudentRow = {
|
||||
student_id: number
|
||||
firstname: string
|
||||
lastname: string
|
||||
grade: string
|
||||
class_section_id: number
|
||||
class_section_name: string
|
||||
year_score: number | null
|
||||
eligible: boolean
|
||||
decision_state: 'pending' | 'pass' | 'decision'
|
||||
decision_labels: string[]
|
||||
decision_rows: CertificateDecisionRow[]
|
||||
certificate_number: string | null
|
||||
}
|
||||
|
||||
export type CertFormOptions = {
|
||||
class_sections: CertClassSection[]
|
||||
students: CertStudent[]
|
||||
export type CertificateSectionRow = {
|
||||
section_id: number
|
||||
section_name: string
|
||||
student_count: number
|
||||
pass_count: number
|
||||
cert_count: number
|
||||
remaining_count: number
|
||||
students: CertificateStudentRow[]
|
||||
}
|
||||
|
||||
export type CertificateGradeGroup = {
|
||||
key: string
|
||||
label: string
|
||||
slug: string
|
||||
total: number
|
||||
pass: number
|
||||
cert: number
|
||||
fully_done: boolean
|
||||
has_pass: boolean
|
||||
sections: CertificateSectionRow[]
|
||||
}
|
||||
|
||||
export type CertificateDashboardPayload = {
|
||||
school_year: string
|
||||
selected_class_id: string | null
|
||||
cert_date: string
|
||||
grade_groups: CertificateGradeGroup[]
|
||||
stats_per_class: Record<string, { name: string; total: number; pass: number; cert: number }>
|
||||
default_group_key: string | null
|
||||
}
|
||||
|
||||
export async function fetchCertFormOptions(params?: {
|
||||
school_year?: string
|
||||
class_section_id?: string | number
|
||||
}): Promise<ApiEnvelope<CertFormOptions>> {
|
||||
export type CertificateAuditRecord = {
|
||||
certificate_number: string
|
||||
student_name: string
|
||||
grade?: string | null
|
||||
cert_date?: string | null
|
||||
school_year?: string | null
|
||||
admin_firstname?: string | null
|
||||
admin_lastname?: string | null
|
||||
issued_at?: string | null
|
||||
}
|
||||
|
||||
export type CertificateAuditPayload = {
|
||||
school_year: string
|
||||
records: CertificateAuditRecord[]
|
||||
year_summary: Array<{ school_year: string; total: number }>
|
||||
}
|
||||
|
||||
function authHeaders(accept: string) {
|
||||
const headers = new Headers({ Accept: accept })
|
||||
const token = getStoredToken()
|
||||
if (token) headers.set('Authorization', `Bearer ${token}`)
|
||||
return headers
|
||||
}
|
||||
|
||||
function withQuery(path: string, params?: Record<string, string | number | null | undefined>) {
|
||||
const qs = new URLSearchParams()
|
||||
if (params?.school_year) qs.set('school_year', params.school_year)
|
||||
if (params?.class_section_id != null && String(params.class_section_id) !== '')
|
||||
qs.set('class_section_id', String(params.class_section_id))
|
||||
const suffix = qs.toString() ? `?${qs}` : ''
|
||||
return apiFetch<ApiEnvelope<CertFormOptions>>(`/api/v1/administrator/certificates/form-options${suffix}`)
|
||||
Object.entries(params ?? {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||||
qs.set(key, String(value))
|
||||
}
|
||||
})
|
||||
return qs.size > 0 ? `${path}?${qs.toString()}` : path
|
||||
}
|
||||
|
||||
export async function fetchCertificatesDashboard(params?: {
|
||||
school_year?: string
|
||||
}): Promise<CertificateDashboardPayload> {
|
||||
const res = await apiFetch<ApiEnvelope<CertificateDashboardPayload>>(
|
||||
withQuery('/api/v1/administrator/certificates/dashboard', params),
|
||||
)
|
||||
return (res.data ?? {}) as CertificateDashboardPayload
|
||||
}
|
||||
|
||||
export async function fetchCertificatesAuditLog(params?: {
|
||||
school_year?: string
|
||||
}): Promise<CertificateAuditPayload> {
|
||||
const res = await apiFetch<ApiEnvelope<CertificateAuditPayload>>(
|
||||
withQuery('/api/v1/administrator/certificates/audit-log', params),
|
||||
)
|
||||
return (res.data ?? {}) as CertificateAuditPayload
|
||||
}
|
||||
|
||||
export async function postCertificateGenerate(payload: {
|
||||
student_ids: number[]
|
||||
cert_date: string
|
||||
class_section_id?: number | string | null
|
||||
class_section_id?: number | null
|
||||
school_year?: string | null
|
||||
}): Promise<Blob> {
|
||||
const headers = new Headers({
|
||||
Accept: 'application/pdf,*/*',
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
const token = getStoredToken()
|
||||
if (token) headers.set('Authorization', `Bearer ${token}`)
|
||||
|
||||
const res = await fetch(apiUrl('/api/v1/administrator/certificates/generate'), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
headers: (() => {
|
||||
const headers = authHeaders('application/pdf,*/*')
|
||||
headers.set('Content-Type', 'application/json')
|
||||
return headers
|
||||
})(),
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const errBody: unknown = await res.json().catch(() => null)
|
||||
const msg =
|
||||
const message =
|
||||
typeof errBody === 'object' && errBody !== null && 'message' in errBody
|
||||
? String((errBody as { message?: unknown }).message ?? '')
|
||||
: ''
|
||||
throw new ApiHttpError(msg || `HTTP ${res.status}`, res.status, errBody)
|
||||
throw new ApiHttpError(message || `HTTP ${res.status}`, res.status, errBody)
|
||||
}
|
||||
|
||||
const raw = await res.blob()
|
||||
return new Blob([await raw.arrayBuffer()], { type: 'application/pdf' })
|
||||
return res.blob()
|
||||
}
|
||||
|
||||
export async function fetchCertificateReprint(certificateNumber: string): Promise<Blob> {
|
||||
const res = await fetch(
|
||||
apiUrl(`/api/v1/administrator/certificates/reprint/${encodeURIComponent(certificateNumber)}`),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: authHeaders('application/pdf,*/*'),
|
||||
},
|
||||
)
|
||||
|
||||
if (!res.ok) {
|
||||
const errBody: unknown = await res.json().catch(() => null)
|
||||
const message =
|
||||
typeof errBody === 'object' && errBody !== null && 'message' in errBody
|
||||
? String((errBody as { message?: unknown }).message ?? '')
|
||||
: ''
|
||||
throw new ApiHttpError(message || `HTTP ${res.status}`, res.status, errBody)
|
||||
}
|
||||
|
||||
return res.blob()
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
import { apiUrl } from '../lib/apiOrigin'
|
||||
import { apiFetch, getStoredToken } from './http'
|
||||
|
||||
const BASE = '/api/v1/administrator/invoices'
|
||||
const BASE = '/api/v1/finance/invoices'
|
||||
|
||||
function q(searchParams: URLSearchParams): string {
|
||||
const s = searchParams.toString()
|
||||
@@ -35,7 +35,7 @@ export async function fetchInvoiceManagement(
|
||||
}
|
||||
|
||||
export async function postInvoiceGenerate(parentId: number): Promise<unknown> {
|
||||
return apiFetch(`${BASE}/generate-for-parent`, {
|
||||
return apiFetch(`${BASE}/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ parent_id: parentId }),
|
||||
|
||||
@@ -175,7 +175,17 @@ export async function fetchUnpaidParents(params?: {
|
||||
if (params?.school_year) qs.set('school_year', params.school_year)
|
||||
if (params?.semester) qs.set('semester', params.semester)
|
||||
const suffix = qs.toString() ? `?${qs}` : ''
|
||||
return apiFetch(`/api/v1/administrator/payments/unpaid-parents${suffix}`)
|
||||
return apiFetch<{
|
||||
rows?: UnpaidParentRow[]
|
||||
results?: UnpaidParentRow[]
|
||||
school_year?: string
|
||||
schoolYear?: string
|
||||
schoolYears?: string[]
|
||||
}>(`/api/v1/finance/unpaid-parents${suffix}`).then((body) => ({
|
||||
rows: body.rows ?? body.results ?? [],
|
||||
school_year: body.school_year ?? body.schoolYear,
|
||||
schoolYears: body.schoolYears ?? [],
|
||||
}))
|
||||
}
|
||||
|
||||
export async function sendPaymentReminders(payload: {
|
||||
@@ -287,6 +297,15 @@ export type FinancialDetailedJson = {
|
||||
eventFeesTotal?: number
|
||||
}
|
||||
|
||||
function unwrapFinancialReport(
|
||||
body: FinancialDetailedJson & { report?: FinancialDetailedJson },
|
||||
): FinancialDetailedJson {
|
||||
if (body && typeof body === 'object' && body.report && typeof body.report === 'object') {
|
||||
return body.report
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
export async function fetchFinancialReportDetailed(params: {
|
||||
school_year?: string
|
||||
date_from?: string
|
||||
@@ -297,7 +316,10 @@ export async function fetchFinancialReportDetailed(params: {
|
||||
if (params.date_from) qs.set('date_from', params.date_from)
|
||||
if (params.date_to) qs.set('date_to', params.date_to)
|
||||
qs.set('format', 'json')
|
||||
return apiFetch(`/api/v1/administrator/reports/financial-detailed?${qs}`)
|
||||
const body = await apiFetch<FinancialDetailedJson & { report?: FinancialDetailedJson }>(
|
||||
`/api/v1/finance/financial-report?${qs}`,
|
||||
)
|
||||
return unwrapFinancialReport(body)
|
||||
}
|
||||
|
||||
export async function downloadFinancialReportCsv(params: {
|
||||
@@ -309,7 +331,7 @@ export async function downloadFinancialReportCsv(params: {
|
||||
if (params.school_year) qs.set('school_year', params.school_year)
|
||||
if (params.date_from) qs.set('date_from', params.date_from)
|
||||
if (params.date_to) qs.set('date_to', params.date_to)
|
||||
const path = `/api/v1/administrator/reports/financial-detailed.csv?${qs}`
|
||||
const path = `/api/v1/finance/financial-report/csv?${qs}`
|
||||
const headers = new Headers()
|
||||
const token = getStoredToken()
|
||||
if (token) headers.set('Authorization', `Bearer ${token}`)
|
||||
@@ -341,19 +363,48 @@ export type FinancialSummaryJson = {
|
||||
totalPaid?: number
|
||||
}
|
||||
|
||||
function unwrapFinancialSummary(
|
||||
body: FinancialSummaryJson & {
|
||||
summary?: FinancialSummaryJson
|
||||
schoolYears?: string[]
|
||||
},
|
||||
): FinancialSummaryJson {
|
||||
const summary =
|
||||
body && typeof body === 'object' && body.summary && typeof body.summary === 'object'
|
||||
? body.summary
|
||||
: body
|
||||
|
||||
if (
|
||||
body &&
|
||||
typeof body === 'object' &&
|
||||
Array.isArray(body.schoolYears) &&
|
||||
!Array.isArray((summary as { schoolYears?: unknown }).schoolYears)
|
||||
) {
|
||||
return {
|
||||
...summary,
|
||||
schoolYears: body.schoolYears,
|
||||
} as FinancialSummaryJson
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
export async function fetchFinancialReportSummary(params: {
|
||||
school_year?: string
|
||||
}): Promise<FinancialSummaryJson> {
|
||||
const qs = new URLSearchParams()
|
||||
if (params.school_year) qs.set('school_year', params.school_year)
|
||||
qs.set('format', 'json')
|
||||
return apiFetch(`/api/v1/administrator/reports/financial-summary?${qs}`)
|
||||
const body = await apiFetch<
|
||||
FinancialSummaryJson & { summary?: FinancialSummaryJson; schoolYears?: string[] }
|
||||
>(`/api/v1/finance/financial-summary?${qs}`)
|
||||
return unwrapFinancialSummary(body)
|
||||
}
|
||||
|
||||
export async function downloadFinancialSummaryPdf(params: { school_year?: string }): Promise<void> {
|
||||
const qs = new URLSearchParams()
|
||||
if (params.school_year) qs.set('school_year', params.school_year)
|
||||
const path = `/api/v1/administrator/reports/financial-summary.pdf?${qs}`
|
||||
const path = `/api/v1/finance/financial-report/pdf?${qs}`
|
||||
const headers = new Headers()
|
||||
const token = getStoredToken()
|
||||
if (token) headers.set('Authorization', `Bearer ${token}`)
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Admin refunds list (legacy CI `refunds/list.php`).
|
||||
* Laravel: `GET /api/v1/administrator/refunds/list` with JWT.
|
||||
* Laravel: `GET /api/v1/finance/refunds` with JWT.
|
||||
*/
|
||||
import { apiFetch } from './http'
|
||||
|
||||
const BASE = '/api/v1/administrator/refunds'
|
||||
const BASE = '/api/v1/finance/refunds'
|
||||
|
||||
export type RefundListRow = Record<string, unknown>
|
||||
|
||||
@@ -42,6 +42,6 @@ export async function fetchRefundsList(params?: {
|
||||
if (params?.semester) qs.set('semester', params.semester)
|
||||
if (params?.page != null) qs.set('page', String(params.page))
|
||||
const suffix = qs.toString() ? `?${qs}` : ''
|
||||
const body = await apiFetch<unknown>(`${BASE}/list${suffix}`)
|
||||
const body = await apiFetch<unknown>(`${BASE}${suffix}`)
|
||||
return normalizeListPayload(body)
|
||||
}
|
||||
|
||||
+135
-11
@@ -89,7 +89,9 @@ import type {
|
||||
EnrollmentWithdrawalPageResponse,
|
||||
ExpensesListResponse,
|
||||
ExpenseCreateFormResponse,
|
||||
ExpenseDetail,
|
||||
ExpenseEditFormResponse,
|
||||
ExpenseUserOption,
|
||||
FamilyLegacyImportMetaResponse,
|
||||
FamilyLegacyImportResult,
|
||||
FamilySearchResponse,
|
||||
@@ -1039,12 +1041,16 @@ export async function openProtectedApiFile(path: string): Promise<void> {
|
||||
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000)
|
||||
}
|
||||
|
||||
async function fetchMultipartJson<T>(path: string, formData: FormData): Promise<T> {
|
||||
async function fetchMultipartJson<T>(
|
||||
path: string,
|
||||
formData: FormData,
|
||||
init: { method?: 'POST' | 'PUT' | 'PATCH' } = {},
|
||||
): Promise<T> {
|
||||
const headers = new Headers({ Accept: 'application/json' })
|
||||
const token = getStoredToken()
|
||||
if (token) headers.set('Authorization', `Bearer ${token}`)
|
||||
const res = await fetch(apiUrl(path), {
|
||||
method: 'POST',
|
||||
method: init.method ?? 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
})
|
||||
@@ -1193,6 +1199,68 @@ export async function fetchUsers(params?: {
|
||||
return apiFetch<UsersResponse>(`/api/v1/users${qs}`)
|
||||
}
|
||||
|
||||
let discountParentSchoolIdMapPromise: Promise<Map<string, string>> | null = null
|
||||
|
||||
function userRowHasParentRole(row: UserListRow): boolean {
|
||||
return (row.roles ?? []).some((role) => {
|
||||
const label =
|
||||
typeof role === 'string' ? role : `${role.name ?? ''} ${role.slug ?? ''}`
|
||||
return label.toLowerCase().includes('parent')
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchDiscountParentSchoolIdMap(): Promise<Map<string, string>> {
|
||||
if (!discountParentSchoolIdMapPromise) {
|
||||
discountParentSchoolIdMapPromise = (async () => {
|
||||
const data = await fetchUsers({ sort: 'lastname', order: 'asc' })
|
||||
const map = new Map<string, string>()
|
||||
|
||||
for (const row of data.users ?? []) {
|
||||
if (!userRowHasParentRole(row)) continue
|
||||
const user = row.user
|
||||
const userId = user?.id
|
||||
const schoolId = user?.school_id
|
||||
if (userId == null || schoolId == null) continue
|
||||
const normalizedSchoolId = String(schoolId).trim()
|
||||
if (normalizedSchoolId === '') continue
|
||||
map.set(String(userId), normalizedSchoolId)
|
||||
}
|
||||
|
||||
return map
|
||||
})().catch((error) => {
|
||||
discountParentSchoolIdMapPromise = null
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
return discountParentSchoolIdMapPromise
|
||||
}
|
||||
|
||||
async function enrichDiscountApplyContext(
|
||||
context: DiscountApplyContextResponse,
|
||||
): Promise<DiscountApplyContextResponse> {
|
||||
const parents = context.parents ?? []
|
||||
if (parents.length === 0) return context
|
||||
|
||||
try {
|
||||
const schoolIdMap = await fetchDiscountParentSchoolIdMap()
|
||||
return {
|
||||
...context,
|
||||
parents: parents.map((parent) => {
|
||||
if (typeof parent.school_id === 'string' && parent.school_id.trim() !== '') {
|
||||
return parent
|
||||
}
|
||||
|
||||
const schoolId = schoolIdMap.get(String(parent.id))
|
||||
if (!schoolId) return parent
|
||||
return { ...parent, school_id: schoolId }
|
||||
}),
|
||||
}
|
||||
} catch {
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapEnvelopeData(body: unknown): unknown {
|
||||
if (!body || typeof body !== 'object') return body
|
||||
const o = body as Record<string, unknown>
|
||||
@@ -2025,10 +2093,19 @@ export async function fetchDiscountApplyContext(params?: {
|
||||
if (params?.school_year) qs.set('school_year', params.school_year)
|
||||
if (params?.semester) qs.set('semester', params.semester)
|
||||
const suffix = qs.toString() ? `?${qs}` : ''
|
||||
try {
|
||||
const body = await apiFetch<DiscountApplyContextResponse & { data?: DiscountApplyContextResponse }>(
|
||||
`/api/v1/discounts/options${suffix}`,
|
||||
)
|
||||
return enrichDiscountApplyContext(unwrapData(body))
|
||||
} catch (err: unknown) {
|
||||
if (!(err instanceof ApiHttpError) || err.status !== 404) throw err
|
||||
}
|
||||
|
||||
const body = await apiFetch<DiscountApplyContextResponse & { data?: DiscountApplyContextResponse }>(
|
||||
`/api/v1/discounts/apply-context${suffix}`,
|
||||
)
|
||||
return unwrapData(body)
|
||||
return enrichDiscountApplyContext(unwrapData(body))
|
||||
}
|
||||
|
||||
export async function createDiscountVoucher(payload: Record<string, unknown>): Promise<{ ok?: boolean; message?: string }> {
|
||||
@@ -2194,11 +2271,31 @@ export async function fetchExpensesList(params?: {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExpenseFormOptions(
|
||||
body: Partial<ExpenseCreateFormResponse> & {
|
||||
retailors?: string[]
|
||||
staff?: ExpenseUserOption[]
|
||||
},
|
||||
): ExpenseCreateFormResponse {
|
||||
return {
|
||||
retailors: body.retailors ?? [],
|
||||
users: body.users ?? body.staff ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchExpenseCreateForm(): Promise<ExpenseCreateFormResponse> {
|
||||
const body = await apiFetch<ExpenseCreateFormResponse & { data?: ExpenseCreateFormResponse }>(
|
||||
'/api/v1/administrator/expenses/create-options',
|
||||
'/api/v1/administrator/expenses/options',
|
||||
)
|
||||
return normalizeExpenseFormOptions(
|
||||
unwrapData(
|
||||
body as ExpenseCreateFormResponse & {
|
||||
data?: ExpenseCreateFormResponse
|
||||
retailors?: string[]
|
||||
staff?: ExpenseUserOption[]
|
||||
},
|
||||
),
|
||||
)
|
||||
return unwrapData(body as ExpenseCreateFormResponse & { data?: ExpenseCreateFormResponse })
|
||||
}
|
||||
|
||||
export async function createExpense(
|
||||
@@ -2208,26 +2305,53 @@ export async function createExpense(
|
||||
}
|
||||
|
||||
export async function fetchExpenseEdit(expenseId: number): Promise<ExpenseEditFormResponse> {
|
||||
const body = await apiFetch<ExpenseEditFormResponse & { data?: ExpenseEditFormResponse }>(
|
||||
`/api/v1/administrator/expenses/${expenseId}/edit`,
|
||||
const [expenseBody, optionsBody] = await Promise.all([
|
||||
apiFetch<{ expense?: ExpenseDetail; data?: { expense?: ExpenseDetail } }>(
|
||||
`/api/v1/administrator/expenses/${expenseId}`,
|
||||
),
|
||||
apiFetch<ExpenseCreateFormResponse & { data?: ExpenseCreateFormResponse }>(
|
||||
'/api/v1/administrator/expenses/options',
|
||||
),
|
||||
])
|
||||
const expenseData = unwrapData(
|
||||
expenseBody as { expense?: ExpenseDetail; data?: { expense?: ExpenseDetail } },
|
||||
)
|
||||
return unwrapData(body as ExpenseEditFormResponse & { data?: ExpenseEditFormResponse })
|
||||
const optionsData = normalizeExpenseFormOptions(
|
||||
unwrapData(
|
||||
optionsBody as ExpenseCreateFormResponse & {
|
||||
data?: ExpenseCreateFormResponse
|
||||
retailors?: string[]
|
||||
staff?: ExpenseUserOption[]
|
||||
},
|
||||
),
|
||||
)
|
||||
return {
|
||||
expense: expenseData.expense ?? (expenseData as ExpenseDetail),
|
||||
retailors: optionsData.retailors,
|
||||
users: optionsData.users,
|
||||
receipt_url: expenseData.expense?.receipt_url ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateExpense(
|
||||
expenseId: number,
|
||||
formData: FormData,
|
||||
): Promise<{ ok?: boolean; message?: string }> {
|
||||
return fetchMultipartJson(`/api/v1/administrator/expenses/${expenseId}`, formData)
|
||||
const payload = new FormData()
|
||||
formData.forEach((value, key) => {
|
||||
payload.append(key, value)
|
||||
})
|
||||
payload.set('_method', 'PUT')
|
||||
return fetchMultipartJson(`/api/v1/administrator/expenses/${expenseId}`, payload)
|
||||
}
|
||||
|
||||
export async function updateExpenseStatus(payload: {
|
||||
id: number
|
||||
status: 'approved' | 'denied'
|
||||
}): Promise<{ success?: boolean; error?: string }> {
|
||||
return apiFetch(`/api/v1/administrator/expenses/update-status`, {
|
||||
return apiFetch(`/api/v1/administrator/expenses/${payload.id}/status`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify({ status: payload.status }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { apiFetch } from './http'
|
||||
|
||||
type TrophyApiEnvelope<T> = {
|
||||
ok?: boolean
|
||||
data?: T
|
||||
}
|
||||
|
||||
export type TrophyStudent = {
|
||||
student_id?: number
|
||||
class_section_id?: number
|
||||
name?: string
|
||||
firstname?: string | null
|
||||
lastname?: string | null
|
||||
school_id?: string | null
|
||||
gender?: string | null
|
||||
fall_score?: number | null
|
||||
spring_score?: number | null
|
||||
year_score?: number | null
|
||||
projected_trophy?: boolean
|
||||
predicted?: boolean
|
||||
actual?: boolean
|
||||
status?: string
|
||||
}
|
||||
|
||||
export type TrophyProjectionClassResult = {
|
||||
section_id: number
|
||||
section_name: string
|
||||
students: TrophyStudent[]
|
||||
threshold: number | null
|
||||
trophy_count: number
|
||||
student_count: number
|
||||
scored_count: number
|
||||
method: string
|
||||
boys: number
|
||||
girls: number
|
||||
trophy_boys: number
|
||||
trophy_girls: number
|
||||
pct_boys: number
|
||||
pct_girls: number
|
||||
pct_trophy_boys: number
|
||||
pct_trophy_girls: number
|
||||
pct_trophy_total: number
|
||||
}
|
||||
|
||||
export type TrophyProjectionPayload = {
|
||||
selected_year: string
|
||||
selected_percentile: number
|
||||
years: string[]
|
||||
class_results: TrophyProjectionClassResult[]
|
||||
summary: Record<string, number>
|
||||
}
|
||||
|
||||
export type TrophyWinnersClassResult = {
|
||||
section_id: number
|
||||
section_name: string
|
||||
threshold: number | null
|
||||
winners: TrophyStudent[]
|
||||
student_count: number
|
||||
boys: number
|
||||
girls: number
|
||||
trophy_boys: number
|
||||
trophy_girls: number
|
||||
pct_boys: number
|
||||
pct_girls: number
|
||||
pct_trophy_boys: number
|
||||
pct_trophy_girls: number
|
||||
pct_trophy_total: number
|
||||
}
|
||||
|
||||
export type TrophyWinnersPayload = {
|
||||
selected_year: string
|
||||
selected_percentile: number
|
||||
years: string[]
|
||||
class_results: TrophyWinnersClassResult[]
|
||||
summary: Record<string, number>
|
||||
}
|
||||
|
||||
export type TrophyFinalClassResult = {
|
||||
section_id: number
|
||||
section_name: string
|
||||
students: TrophyStudent[]
|
||||
student_count: number
|
||||
fall_threshold: number | null
|
||||
year_threshold: number | null
|
||||
predicted_count: number
|
||||
actual_count: number
|
||||
confirmed: number
|
||||
surprises: number
|
||||
missed: number
|
||||
accuracy: number
|
||||
}
|
||||
|
||||
export type TrophyGenderSummary = {
|
||||
total: number
|
||||
boys: number
|
||||
girls: number
|
||||
other: number
|
||||
pct_boys: number
|
||||
pct_girls: number
|
||||
pct_other: number
|
||||
}
|
||||
|
||||
export type TrophyFinalPayload = {
|
||||
selected_year: string
|
||||
selected_percentile: number
|
||||
years: string[]
|
||||
class_results: TrophyFinalClassResult[]
|
||||
summary: Record<string, number>
|
||||
winner_gender_summary: TrophyGenderSummary
|
||||
all_gender_summary: TrophyGenderSummary
|
||||
pass_gender_summary: TrophyGenderSummary
|
||||
winner_stickers: Array<{ name: string; section: string }>
|
||||
}
|
||||
|
||||
type TrophyFilters = {
|
||||
school_year?: string
|
||||
percentile?: string | number
|
||||
}
|
||||
|
||||
function withQuery(path: string, params?: TrophyFilters) {
|
||||
const qs = new URLSearchParams()
|
||||
if (params?.school_year) qs.set('school_year', String(params.school_year))
|
||||
if (params?.percentile !== undefined && String(params.percentile).trim() !== '') {
|
||||
qs.set('percentile', String(params.percentile))
|
||||
}
|
||||
return qs.size > 0 ? `${path}?${qs.toString()}` : path
|
||||
}
|
||||
|
||||
async function fetchTrophyPayload<T>(path: string, params?: TrophyFilters): Promise<T> {
|
||||
const res = await apiFetch<TrophyApiEnvelope<T>>(withQuery(path, params))
|
||||
return (res.data ?? {}) as T
|
||||
}
|
||||
|
||||
export function fetchTrophyProjection(params?: TrophyFilters) {
|
||||
return fetchTrophyPayload<TrophyProjectionPayload>('/api/v1/administrator/trophy', params)
|
||||
}
|
||||
|
||||
export function fetchTrophyWinners(params?: TrophyFilters) {
|
||||
return fetchTrophyPayload<TrophyWinnersPayload>('/api/v1/administrator/trophy/winners', params)
|
||||
}
|
||||
|
||||
export function fetchTrophyFinal(params?: TrophyFilters) {
|
||||
return fetchTrophyPayload<TrophyFinalPayload>('/api/v1/administrator/trophy/final', params)
|
||||
}
|
||||
@@ -639,6 +639,7 @@ export type UserListRow = {
|
||||
firstname?: string | null
|
||||
lastname?: string | null
|
||||
email?: string | null
|
||||
school_id?: string | number | null
|
||||
status?: string | null
|
||||
cellphone?: string | null
|
||||
gender?: string | null
|
||||
|
||||
Reference in New Issue
Block a user